diff --git a/Directory.Packages.props b/Directory.Packages.props index 6642d4eb7f..225f8d5eec 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -46,7 +46,7 @@ - + @@ -113,10 +113,10 @@ - - - - + + + + @@ -128,11 +128,11 @@ - - - - - + + + + + @@ -145,6 +145,7 @@ + @@ -155,8 +156,8 @@ - - + + diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index eebf13731a..62f8304467 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -721,6 +721,60 @@ "NuGetApiKey": "NuGet API key", "QuestionCount": "Question Count", "MakeAnnouncement": "Make Announcement", - "MakeAnnouncementInfo": "Check it if you want to make an announcement for this post" + "MakeAnnouncementInfo": "Check it if you want to make an announcement for this post", + "Permission:ViewCounts": "View counts", + "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 f5fea908de..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", @@ -534,7 +537,7 @@ "WhenShouldIRenewMyLicenseExplanation2": "{0} for Team Licenses;", "WhenShouldIRenewMyLicenseExplanation3": "{0} for Business and Enterprise Licenses;", "WhenShouldIRenewMyLicenseExplanation4": "However, if you renew your license more than {0} days after the expiry date, the renewal price will be the same as the initial purchase price of the license, with no discounts applied to your renewal.", - "DoesTheSubscriptionRenewAutomaticallyExplanationAutoRenewal": "ABP Platform allows you to auto-renew your license. This is an optional free service. You can toggle this feature when you purchase a new license or later enable it from your organization management page. If you want to turn on or off the auto-renewal, visit the organization management page, go to the 'Payments Method' section and either check or uncheck the 'Automatic Renewal' checkbox. When you turn off the auto-renewal feature, it will be your responsibility to renew your license manually.", + "DoesTheSubscriptionRenewAutomaticallyExplanationAutoRenewal": "ABP Platform allows you to auto-renew your license. This is an optional free service. You can toggle this feature when you purchase a new license or later enable it from your organization management page. If you want to turn on or off the auto-renewal, visit the organization management page, go to the 'Payments Method' section and either check or uncheck the 'Automatic Renewal' checkbox. When you turn off the auto-renewal feature, it will be your responsibility to renew your license manually.
The renewals (manual) are non-refundable. On the other hand, all subscription auto-renewals are non-refundable after 10 calendar days from the auto-renewal date. If you don't wish to continue your license, it is your responsibility to manage the renewal settings and cancel the subscription before the automatic renewal date.", "TrialPlanExplanation": "Yes, to start your free trial, please contact sales@volosoft.com. We also offer a 30-day money-back guarantee for the Team license, with no questions asked! You can request a full refund within the first 30 days of purchasing the license. For Business and Enterprise licenses, we provide a 60% refund if requested within 30 days of purchase. This policy is due to the inclusion of the full source code for all modules and themes in the Business and Enterprise licenses.", "BlazoriseLicenseExplanation": "We have an agreement between Volosoft and Megabit, according to which the Blazorise license is bundled with the ABP Platform’s commercial licenses. Therefore, our paid users do not need to purchase an additional Blazorise license.", "HowToUpgradeExplanation1": "When you create a new application using the ABP startup templates, all the modules and themes are used as NuGet and NPM packages. This setup allows for easy upgrades to newer versions of the packages.", @@ -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", @@ -1425,7 +1429,7 @@ "TotalDevelopers": "Total {0} developer(s)", "CustomPurchaseExplanation": "Tailored to your specific needs", "WhereDidYouHearAboutUs": "Where did you hear about us?", - "Twitter": "Twitter", + "Twitter": "Twitter (X)", "Facebook": "Facebook", "Youtube": "YouTube", "Google": "Google", @@ -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", @@ -1891,6 +1895,22 @@ "CreatePostSEOTitleInfo": "SEO URL is a clean, readable, keyword-rich URL that helps both users and search engines understand what this post is about. Keep it short with 60 characters. SEO titles over 60 characters will be truncated. Use hyphens (-) to separate words (not underscores). Include target keywords near the start. Lowercase only. No stop words unless needed (e.g: \"and\", \"or\", \"the\").", "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" + "SelectAnOption": "Select an option", + "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/common.props b/common.props index 5dd64ecc32..bf99a29ca2 100644 --- a/common.props +++ b/common.props @@ -1,8 +1,8 @@ latest - 9.3.4 - 4.3.4 + 10.0.0-preview + 5.0.0-preview $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/POST.md b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/POST.md new file mode 100644 index 0000000000..08c375eeb3 --- /dev/null +++ b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/POST.md @@ -0,0 +1,203 @@ +# ABP Platform 9.3 RC Has Been Released + +We are happy to release [ABP](https://abp.io) version **9.3 RC** (Release Candidate). This blog post introduces the new features and important changes in this new version. + +Try this version and provide feedback for a more stable version of ABP v9.3! Thanks to you in advance. + +## Get Started with the 9.3 RC + +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). + +By default, ABP Studio uses stable versions to create solutions. Therefore, if you want to create a solution with a preview version, first you need to create a solution and then switch your solution to the preview version from the ABP Studio UI: + +![studio-switch-to-preview.png](studio-switch-to-preview.png) + +## Migration Guide + +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 or earlier: [ABP Version 9.3 Migration Guide](https://abp.io/docs/9.3/release-info/migration-guides/abp-9-3) + +## What's New with ABP v9.3? + +In this section, I will introduce some major features released in this version. +Here is a brief list of titles explained in the next sections: + +* Cron Expression Support for Background Workers +* Docs Module: PDF Export +* Angular UI: Standalone Package Structure +* Upgraded to Blazorise v1.7.7 +* Audit Logging Module: Excel Export + +### Cron Expression Support for Background Workers + +We've enhanced the [Background Workers System](https://abp.io/docs/9.3/framework/infrastructure/background-workers) by adding support for Cron expressions when using [Hangfire](https://abp.io/docs/9.3/framework/infrastructure/background-workers/hangfire) or [Quartz](https://abp.io/docs/9.3/framework/infrastructure/background-workers/quartz) as the background worker manager. This new feature provides more flexibility in scheduling background tasks compared to the simple period-based timing system. + +Now you can define complex scheduling patterns using standard Cron expressions. For example, you can schedule a task to run: "Every day at midnight", "Every Monday at 9 AM", or "First day of every month". + +Here's how you can use it in your background worker: + +```csharp +public class MyPeriodicBackgroundWorker : AsyncPeriodicBackgroundWorkerBase +{ + public MyPeriodicBackgroundWorker( + AbpAsyncTimer timer, + IServiceScopeFactory serviceScopeFactory) + : base(timer, serviceScopeFactory) + { + // You can either use Period for simple intervals + Timer.Period = 600000; //10 minutes + + // 👇 or use CronExpression for more complex scheduling 👇 + CronExpression = "0 0/10 * * * ?"; //Run every 10 minutes + } + + protected async override Task DoWorkAsync( + PeriodicBackgroundWorkerContext context) + { + // Your background work... + } +} +``` + +The `CronExpression` property takes precedence over the `Period` property when both are set. This feature is available when you use either the [Hangfire](https://abp.io/docs/9.3/framework/infrastructure/background-workers/hangfire) or [Quartz](https://abp.io/docs/9.3/framework/infrastructure/background-workers/quartz) background worker managers. + +> See the [Background Workers documentation](https://abp.io/docs/9.3/framework/infrastructure/background-workers) for more information about configuring and using background workers with Cron expressions. + +### Docs Module: PDF Export + +We're excited to introduce a new feature in the Docs Module that allows users to export documentation as PDF files. This feature makes it easier for users to access documentation offline or share it with team members who might not have immediate access to the online documentation system. + +**Administrators can generate PDF files from the back-office side**: + +![PDF generation settings in the admin side](generate-pdf-docs.png) + +and **then a "Download PDF" button appears in the document system** (as shown in the image below - the bottom right of the navigation menu -), allowing users to download the compiled documentation as a PDF file: + +![Download PDF button in the documentation system](download-pdf-on-docs.png) + +The feature supports multiple versions of documentation, different language variants, and ensures proper formatting of all content including code blocks and technical documentation. + +### Angular UI: Standalone Package Structure + +ABP v9.3 introduces support for Angular's standalone components architecture while maintaining **full compatibility with existing module-based applications**. This update aligns with Angular's strategic direction toward standalone components as the recommended approach for building Angular applications. + +The key improvements include: + +* **Dual-support routing configurations** that work seamlessly with both module-based and standalone approaches +* **ABP Suite integration** for generating code that supports standalone components +* **Updated schematics** that provide templates for both development patterns + +This enhancement gives developers the flexibility to choose their preferred Angular architecture. Existing module-based applications **continue to work without modifications**, while new projects can leverage the standalone approach for simplified dependency management, reduced boilerplate code, and better lazy-loading capabilities. + +> For developers interested in migrating to standalone components or starting new projects, we'll be publishing a comprehensive blog post with detailed guidance and best practices. In the meantime, you can check [#22829](https://github.com/abpframework/abp/pull/22829) for implementation details of the standalone package structure and make the necessary changes to your project. + +### Upgraded to Blazorise v1.7.7 + +Upgraded the [Blazorise](https://blazorise.com/) library to v1.7.7 for Blazor UI. If you are upgrading your project to v9.3.0, please ensure that all the Blazorise-related packages are using v1.7.7 in your application. Otherwise, you might get errors due to incompatible versions. + +> See [#23013](https://github.com/abpframework/abp/pull/23013) for the updated NuGet packages. + +### Audit Logging Module: Excel Export + +In this version, we've added Excel export capabilities to the [Audit Logging Module](https://abp.io/docs/latest/modules/audit-logging-pro), allowing administrators to export audit logs and entity changes to Excel files for further analysis or reporting purposes. + +![](audit-logs-export-to-excel.png) + +This feature enables users to: + +- Export audit logs with filtering options +- Export entity changes with detailed information +- Receive email notifications when exports are completed or fail +- Download exported files via secure links + +The export process runs in the background, and once completed, users receive an email with a download link. This approach ensures that even large audit log exports don't block the UI or time out during processing. + +You can configure various aspects of this feature using the `AuditLogExcelFileOptions` in your module's configuration: + +```csharp +Configure(options => +{ + // How long to keep exported files before cleanup + options.FileRetentionHours = 48; + + // Base URL for download links in notification emails + options.DownloadBaseUrl = "https://yourdomain.com"; + + // Configure the cleanup worker schedule + options.ExcelFileCleanupOptions.Period = (int)TimeSpan.FromHours(24).TotalMilliseconds; + + // Use cron expression for more advanced scheduling (requires Hangfire or Quartz) + options.ExcelFileCleanupOptions.CronExpression = "0 2 * * *"; // Run at 2 AM daily +}); +``` + +The module includes pre-configured email templates for notifications about completed or failed exports, ensuring users are always informed about the status of their export requests. + +> **Note**: This feature requires a configured BLOB storage provider to store the generated Excel files. See the [BLOB Storing documentation](https://abp.io/docs/9.3/framework/infrastructure/blob-storing) for more information. + +For more details about the Audit Logging Module and its Excel export capabilities, please refer to the [official documentation](https://abp.io/docs/9.3/modules/audit-logging-pro). + +## Community News + +### Announcing ABP Studio 1.0 General Availability 🚀 + +![](abp-studio.png) + +We are thrilled to announce that ABP Studio has reached version 1.0 and is now generally available! This marks a significant milestone for our integrated development environment designed specifically for ABP developers. The stable release brings several powerful features including: + +* Enhanced Solution Runner with health monitoring capabilities +* Theme style selection during project creation (Basic, LeptonX Lite, and LeptonX Themes) +* New "Container" application type for better Docker container management +* Improved handling of multiple DbContexts for migration operations + +> For a detailed overview of these features and to learn more about what's coming next, check out our [announcement post](https://abp.io/community/articles/announcing-abp-studio-1-0-general-availability-82yw62bt). + +### ABP Community Talks 2025.05: Empower Elsa Workflows with AI in .NET + ABP Framework + +In this episode of ABP Community Talks, 2025.05, we are thrilled to host [**Sipke Schoorstra**](https://github.com/sfmskywalker), the creator of the [Elsa Workflows](https://docs.elsaworkflows.io/) library! This month's session is all about **"Empower Elsa Workflows with AI in .NET + ABP Framework"**. + +![](community-talk-2025-5.png) + +Sipke will join us to demonstrate how you can leverage AI within Elsa Workflows using .NET and the ABP Framework. The session will explore practical techniques and showcase how to integrate AI capabilities to enhance and automate your business processes within the Elsa workflow engine. + +> 👉 Don't miss this opportunity to learn directly from the creator of Elsa and see real-world examples of building intelligent, automated workflows! You can register from [here](https://kommunity.com/volosoft/events/abp-community-talks-202505empower-elsa-workflows-with-ai-in-netabp-framework-3965dd32). + +### ABP Bootcamp: Mastering Infrastructure & Features + +We are excited to announce the very first **ABP Bootcamp: Mastering Infrastructure & Features**! This is a live training program designed to give you hands-on, practical experience with ABP's core infrastructure and features. + +![ABP Bootcamp: Mastering Infrastructure & Features](bootcamp.png) + +Join the ABP Bootcamp to learn directly from the core team in a focused, hands-on program designed for busy developers. Over four days, you'll gain a deep understanding of ABP's infrastructure, best practices, and practical skills you can immediately apply to your projects. + +> **Seats are limited!** Don't miss this opportunity to level up your ABP skills with direct guidance from the experts. +> +> 👉 [See full details and reserve your seat!](https://abp.io/bootcamp) + +### New ABP Community Articles + +There are exciting articles contributed by the ABP community as always. I will highlight some of them here: + +* [Prabhjot Singh](https://abp.io/community/members/prabhjot) has published 3 new articles: + * [Accessing Multiple Remote ABP based Backends Using HttpApi.Client](https://abp.io/community/articles/consume-multi-backends-using-clients-6f4vcggh) + * [Adopting the new .slnx format to organize applications and services](https://abp.io/community/articles/adopting-the-new-.slnx-format-to-organize-applications-6cm3vl8k) + * [Replacing Dynamic client proxies with Static client proxies](https://abp.io/community/articles/replacing-dynamic-client-proxies-with-static-client-proxies-g30lf0vx) +* [Liming Ma](https://github.com/maliming) has published 2 new articles: + * [Resolving Tenant from Route in ABP Framework](https://abp.io/community/articles/resolving-tenant-from-route-in-abp-framework-ah7oru97) + * [Integrating .NET AI Chat Template with ABP Framework](https://abp.io/community/articles/integrating-.net-ai-chat-template-with-abp-framework-qavb5p2j) +* [Engincan Veske](https://engincanveske.substack.com/) has published 2 new articles: + * [Understanding HttpApi.Client Project & Remote Services in an ABP Based Application](https://abp.io/community/articles/http-api-client-and-remote-services-in-abp-based-application-xkknsp6m) + * [Using Elsa 3 with the ABP Framework: A Comprehensive Guide](https://abp.io/community/articles/using-elsa-3-workflow-with-abp-framework-usqk8afg) +* [Enis Necipoğlu](https://github.com/enisn) has published 2 new articles: + * [White Labeling in ABP Framework](https://abp.io/community/articles/white-labeling-in-abp-framework-5trwmrfm) by [Enis Necipoğlu](https://github.com/enisn) + * [You do it wrong! Customizing ABP Login Page Correctly](https://abp.io/community/articles/you-do-it-wrong-customizing-abp-login-page-correctly-bna7wzt5) +* [New in ABP Studio: Docker Container Management](https://abp.io/community/articles/abp-studio-docker-container-management-ex7r27y8) by [Yunus Emre Kalkan](https://github.com/yekalkan) +* [Solving MongoDB GUID Issues After an ABP Framework Upgrade](https://abp.io/community/articles/solving-mongodb-guid-issues-after-an-abp-framework-upgrade-tv8waw1n) by [Burak Demir](https://abp.io/community/members/burakdemir) + + +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. + +## Conclusion + +This version comes with some new features and a lot of enhancements to the existing features. You can see the [Road Map](https://abp.io/docs/9.3/release-info/road-map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v9.3 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! \ No newline at end of file diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/abp-studio.png b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/abp-studio.png new file mode 100644 index 0000000000..76b24a8573 Binary files /dev/null and b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/abp-studio.png differ diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/audit-logs-export-to-excel.png b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/audit-logs-export-to-excel.png new file mode 100644 index 0000000000..4c10f516db Binary files /dev/null and b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/audit-logs-export-to-excel.png differ diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/bootcamp.png b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/bootcamp.png new file mode 100644 index 0000000000..e07293c13c Binary files /dev/null and b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/bootcamp.png differ diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/community-talk-2025-5.png b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/community-talk-2025-5.png new file mode 100644 index 0000000000..3989dde47a Binary files /dev/null and b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/community-talk-2025-5.png differ diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/cover-image.png b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/cover-image.png new file mode 100644 index 0000000000..291da0d04f Binary files /dev/null and b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/cover-image.png differ diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/download-pdf-on-docs.png b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/download-pdf-on-docs.png new file mode 100644 index 0000000000..ce5b1b3727 Binary files /dev/null and b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/download-pdf-on-docs.png differ diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/generate-pdf-docs.png b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/generate-pdf-docs.png new file mode 100644 index 0000000000..2a1de4398d Binary files /dev/null and b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/generate-pdf-docs.png differ diff --git a/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/studio-switch-to-preview.png b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/studio-switch-to-preview.png new file mode 100644 index 0000000000..32f6d01edb Binary files /dev/null and b/docs/en/Blog-Posts/2025-06-18 v9_3_Preview/studio-switch-to-preview.png differ 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-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/POST.md b/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/POST.md new file mode 100644 index 0000000000..4fcb9860bc --- /dev/null +++ b/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/POST.md @@ -0,0 +1,250 @@ +# Using Hangfire Dashboard in ABP API Website 🚀 + +## Introduction + +In this article, I'll show you how to integrate and use the Hangfire Dashboard in an ABP API website. + +Typically, API websites use `JWT Bearer` authentication, but the Hangfire Dashboard isn't compatible with `JWT Bearer` authentication. Therefore, we need to implement `Cookies` and `OpenIdConnect` authentication for the Hangfire Dashboard access. + +## Creating a New ABP Demo Project 🛠️ + +We'll create a new ABP Demo `Tiered` project that includes `AuthServer`, `API`, and `Web` projects. + +```bash +abp new AbpHangfireDemoApp -t app --tiered +``` + +Now let's add the Hangfire Dashboard to the `API` project and configure it to use `Cookies` and `OpenIdConnect` authentication for accessing the dashboard. + +## Adding a New Hangfire Application 🔧 + +We need to add a new Hangfire application to the `appsettings.json` file in the `DbMigrator` project: + +> **Note:** Replace `44371` with your `API` project's port. + +```json +"OpenIddict": { + "Applications": { + //... + "AbpHangfireDemoApp_Hangfire": { + "ClientId": "AbpHangfireDemoApp_Hangfire", + "RootUrl": "https://localhost:44371/" + } + //... + } +} +``` + +2. Update the `OpenIddictDataSeedContributor`'s `CreateApplicationsAsync` method in the `Domain` project to seed the new Hangfire application. + +```csharp + //Hangfire Client +var hangfireClientId = configurationSection["AbpHangfireDemoApp_Hangfire:ClientId"]; +if (!hangfireClientId.IsNullOrWhiteSpace()) +{ + var hangfireClientRootUrl = configurationSection["AbpHangfireDemoApp_Hangfire:RootUrl"]!.EnsureEndsWith('/'); + + await CreateApplicationAsync( + applicationType: OpenIddictConstants.ApplicationTypes.Web, + name: hangfireClientId!, + type: OpenIddictConstants.ClientTypes.Confidential, + consentType: OpenIddictConstants.ConsentTypes.Implicit, + displayName: "Hangfire Application", + secret: configurationSection["AbpHangfireDemoApp_Hangfire:ClientSecret"] ?? "1q2w3e*", + grantTypes: new List //Hybrid flow + { + OpenIddictConstants.GrantTypes.AuthorizationCode, OpenIddictConstants.GrantTypes.Implicit + }, + scopes: commonScopes, + redirectUris: new List { $"{hangfireClientRootUrl}signin-oidc" }, + postLogoutRedirectUris: new List { $"{hangfireClientRootUrl}signout-callback-oidc" }, + clientUri: hangfireClientRootUrl, + logoUri: "/images/clients/aspnetcore.svg" + ); +} +``` + +3. Run the `DbMigrator` project to seed the new Hangfire application. + +### Adding Hangfire Dashboard to the `API` Project 📦 + +1. Add the following packages and modules dependencies to the `API` project: + +```bash + + + +``` + +```cs +typeof(AbpBackgroundJobsHangfireModule), +typeof(AbpAspNetCoreAuthenticationOpenIdConnectModule) +``` + +2. Add the `HangfireClientId` and `HangfireClientSecret` to the `appsettings.json` file in the `API` project: + +```csharp +"AuthServer": { + "Authority": "https://localhost:44358", + "RequireHttpsMetadata": true, + "MetaAddress": "https://localhost:44358", + "SwaggerClientId": "AbpHangfireDemoApp_Swagger", + "HangfireClientId": "AbpHangfireDemoApp_Hangfire", + "HangfireClientSecret": "1q2w3e*" +} +``` + +3. Add the `ConfigureHangfire` method to the `API` project to configure Hangfire: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var configuration = context.Services.GetConfiguration(); + var hostingEnvironment = context.Services.GetHostingEnvironment(); + + //... + + //Add Hangfire + ConfigureHangfire(context, configuration); + //... +} + +private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration) +{ + context.Services.AddHangfire(config => + { + config.UseSqlServerStorage(configuration.GetConnectionString("Default")); + }); +} +``` + +4. Modify the `ConfigureAuthentication` method to add new `Cookies` and `OpenIdConnect` authentication schemes: + +```csharp +private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration) +{ + context.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddAbpJwtBearer(options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = configuration.GetValue("AuthServer:RequireHttpsMetadata"); + options.Audience = "AbpHangfireDemoApp"; + + options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase) + ? CookieAuthenticationDefaults.AuthenticationScheme + : null; + }) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) + .AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => + { + options.Authority = configuration["AuthServer:Authority"]; + options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); + options.ResponseType = OpenIdConnectResponseType.Code; + + options.ClientId = configuration["AuthServer:HangfireClientId"]; + options.ClientSecret = configuration["AuthServer:HangfireClientSecret"]; + + options.UsePkce = true; + options.SaveTokens = true; + options.GetClaimsFromUserInfoEndpoint = true; + + options.Scope.Add("roles"); + options.Scope.Add("email"); + options.Scope.Add("phone"); + options.Scope.Add("AbpHangfireDemoApp"); + + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }); + + //... +} +``` + +5. Add a custom middleware and `UseAbpHangfireDashboard` after `UseAuthorization` in the `OnApplicationInitialization` method: + +```csharp +//... +app.UseAuthorization(); + +app.Use(async (httpContext, next) => +{ + if (httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase)) + { + var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + if (!authenticateResult.Succeeded) + { + await httpContext.ChallengeAsync( + OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties + { + RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString + }); + return; + } + } + await next.Invoke(); +}); +app.UseAbpHangfireDashboard("/hangfire", options => +{ + options.AsyncAuthorization = new[] + { + new AbpHangfireAuthorizationFilter() + }; +}); + +//... +``` + +Perfect! 🎉 Now you can run the `AuthServer` and `API` projects and access the Hangfire Dashboard at `https://localhost:44371/hangfire`. + +> **Note:** Replace `44371` with your `API` project's port. + +The first time you access the Hangfire Dashboard, you'll be redirected to the login page of the `AuthServer` project. After you log in, you'll be redirected back to the Hangfire Dashboard. + +![Hangfire Dashboard](gif.gif) + +## Key Points 🔑 + +### 1. Authentication Scheme Selection + +The default authentication scheme in API websites is `JWT Bearer`. We've implemented `Cookies` and `OpenIdConnect` specifically for the Hangfire Dashboard. + +We've configured the `JwtBearerOptions`'s `ForwardDefaultSelector` to use `CookieAuthenticationDefaults.AuthenticationScheme` for Hangfire Dashboard requests. + +This means that if the request path starts with `/hangfire`, the request will be authenticated using the `Cookies` authentication scheme; otherwise, it will use the `JwtBearer` authentication scheme. + +```csharp +options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase) + ? CookieAuthenticationDefaults.AuthenticationScheme + : null; +``` + +### 2. Custom Middleware for Authentication + +We've also implemented a custom middleware to handle `Cookies` authentication for the Hangfire Dashboard. If the current request isn't authenticated with the `Cookies` authentication scheme, it will be redirected to the login page. + +```csharp +app.Use(async (httpContext, next) => +{ + if (httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase)) + { + var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + if (!authenticateResult.Succeeded) + { + await httpContext.ChallengeAsync( + OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties + { + RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString + }); + return; + } + } + await next.Invoke(); +}); +``` + +## References 📚 + +- [ABP Hangfire Background Job Manager](https://abp.io/docs/latest/framework/infrastructure/background-jobs/hangfire) +- [Use cookie authentication in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-9.0) diff --git a/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/gif.gif b/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/gif.gif new file mode 100644 index 0000000000..e4fa879eb4 Binary files /dev/null and b/docs/en/Community-Articles/2025-06-20-Using-Hangfire-Dashboard-in-ABP-API-website/gif.gif differ 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/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/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 2340d24103..21bb054b6a 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -384,6 +384,19 @@ namespace MultiTenancyDemo.Web * A tenant resolver should set `context.TenantIdOrName` if it can determine it. If not, just leave it as is to allow the next resolver to determine it. * `context.ServiceProvider` can be used if you need to additional services to resolve from the [dependency injection](../../fundamentals/dependency-injection.md) system. +##### The Fallback Tenant + +If you want to always fallback to a tenant (in case of no tenant was found by the tenant resolution logic), you can set the `AbpTenantResolveOptions.FallbackTenant` option: + +```csharp +Configure(options => +{ + options.FallbackTenant = "acme"; +}); +``` + +The `FallbackTenant` value can be a tenant name or tenant's Id. This option can be helpful on development time or some specific scenarios to set a constant tenant for the application. It is a simple and consistent way to ensure that a tenant context is always available when needed. However, when you do that, no way to switch to the host side. It is not something you will need it most of the time, but here if you need such a resolution logic. + #### Multi-Tenancy Middleware Multi-Tenancy middleware is an ASP.NET Core request pipeline [middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware) that determines the current tenant from the HTTP request and sets the `ICurrentTenant` properties. 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/background-jobs/hangfire.md b/docs/en/framework/infrastructure/background-jobs/hangfire.md index f5991ee19b..8377f001c0 100644 --- a/docs/en/framework/infrastructure/background-jobs/hangfire.md +++ b/docs/en/framework/infrastructure/background-jobs/hangfire.md @@ -159,14 +159,24 @@ app.UseAbpHangfireDashboard("/hangfire", options => `AbpHangfireAuthorizationFilter` class has the following fields: * **`enableTenant` (`bool`, default: `false`):** Enables/disables accessing the Hangfire dashboard on tenant users. -* **`requiredPermissionName` (`string`, default: `null`):** Hangfire dashboard is accessible only if the current user has the specified permission. In this case, if we specify a permission name, we don't need to set `enableTenant` `true` because the permission system already does it. +* **`requiredPermissionName` (`string`, default: `null`):** Hangfire dashboard is accessible only if the current user has the specified permission. +* **`requiredRoleNames` (`string[]`, default: `[]`):** Hangfire dashboard is accessible only if the current user has one of the specified roles. -If you want to require an additional permission, you can pass it into the constructor as below: +If you want to require more policies, you can use the `PolicyBuilder` property of the `AbpHangfireAuthorizationFilter` class. ```csharp app.UseAbpHangfireDashboard("/hangfire", options => { - options.AsyncAuthorization = new[] { new AbpHangfireAuthorizationFilter(requiredPermissionName: "MyHangFireDashboardPermissionName") }; + var hangfireAuthorizationFilter = new AbpHangfireAuthorizationFilter(requiredPermissionName: "MyHangFireDashboardPermissionName"); + + //hangfireAuthorizationFilter.PolicyBuilder.AddRequirements(new PermissionRequirement("YourPermissionName")); + //hangfireAuthorizationFilter.PolicyBuilder.RequireRole("YourCustomRole"); + //hangfireAuthorizationFilter.PolicyBuilder.Requirements.Add(new YourCustomRequirement()); + + options.AsyncAuthorization = new[] + { + hangfireAuthorizationFilter + }; }); ``` @@ -190,18 +200,20 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi options.Authority = configuration["AuthServer:Authority"]; options.RequireHttpsMetadata = configuration.GetValue("AuthServer:RequireHttpsMetadata"); options.Audience = "MyProjectName"; - }); - context.Services.AddAuthentication() - .AddCookie("Cookies") - .AddOpenIdConnect("oidc", options => + options.ForwardDefaultSelector = httpContext => httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase) + ? CookieAuthenticationDefaults.AuthenticationScheme + : null; + }) + .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) + .AddAbpOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { options.Authority = configuration["AuthServer:Authority"]; - options.RequireHttpsMetadata = configuration.GetValue("AuthServer:RequireHttpsMetadata"); - options.ResponseType = OpenIdConnectResponseType.CodeIdToken; + options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]); + options.ResponseType = OpenIdConnectResponseType.Code; - options.ClientId = configuration["AuthServer:ClientId"]; - options.ClientSecret = configuration["AuthServer:ClientSecret"]; + options.ClientId = configuration["AuthServer:HangfireClientId"]; + options.ClientSecret = configuration["AuthServer:HangfireClientSecret"]; options.UsePkce = true; options.SaveTokens = true; @@ -211,6 +223,8 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi options.Scope.Add("email"); options.Scope.Add("phone"); options.Scope.Add("MyProjectName"); + + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; }); } ``` @@ -218,26 +232,27 @@ private void ConfigureAuthentication(ServiceConfigurationContext context, IConfi ```csharp app.Use(async (httpContext, next) => { - if (httpContext.Request.Path.StartsWithSegments("/hangfire")) + if (httpContext.Request.Path.StartsWithSegments("/hangfire", StringComparison.OrdinalIgnoreCase)) { - var result = await httpContext.AuthenticateAsync("Cookies"); - if (result.Succeeded) + var authenticateResult = await httpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme); + if (!authenticateResult.Succeeded) { - httpContext.User = result.Principal; - await next(httpContext); + await httpContext.ChallengeAsync( + OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties + { + RedirectUri = httpContext.Request.Path + httpContext.Request.QueryString + }); return; } - - await httpContext.ChallengeAsync("oidc"); - } - else - { - await next(httpContext); } + await next.Invoke(); }); - app.UseAbpHangfireDashboard("/hangfire", options => { - options.AsyncAuthorization = new[] {new AbpHangfireAuthorizationFilter()}; + options.AsyncAuthorization = new[] + { + new AbpHangfireAuthorizationFilter() + }; }); ``` 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/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/form-validation.md b/docs/en/framework/ui/angular/form-validation.md index 6a67018124..6e36945c96 100644 --- a/docs/en/framework/ui/angular/form-validation.md +++ b/docs/en/framework/ui/angular/form-validation.md @@ -170,3 +170,211 @@ export const appConfig: ApplicationConfig = { The error message will be bold and italic now: A required field is cleared and a bold and italic error message appears. + +## How to Validate Nested Form Groups + +There are multiple ways to validate nested form groups in ABP Angular UI. Below is the first and most common approach, using automatic validation and error messages with nested reactive forms. (A second method will be described in the next section.) + +### 1st Way: Automatic Validation and Error Message Using Nested Reactive Forms + +ABP Angular UI leverages Angular's reactive forms and the [ngx-validate](https://www.npmjs.com/package/@ngx-validate/core) library to provide a robust, flexible, and user-friendly form validation experience. Whether you build your forms manually or use ABP’s dynamic form generation features, validation and error messages are handled automatically. + +#### Key Features + +- **Automatic Validation:** + All validation rules defined in your DTOs (such as `[Required]`, `[StringLength]`, `[EmailAddress]`, etc.) are automatically reflected in the Angular form. Error messages are shown under each field without any extra markup. + +- **Nested Form Groups and Dynamic Fields:** + For complex data structures, you can group fields or manage dynamic lists using nested `FormGroup` and `FormArray` structures. Validation and error display work seamlessly for both parent and child controls. + +- **Dynamic and Extensible Forms:** + With ABP’s extensibility system, you can generate forms dynamically using helpers like `generateFormFromProps` and display them with the `abp-extensible-form` component. This ensures all entity properties (including extension properties) are included in the form and their validation rules are applied. + +- **No Extra Boilerplate:** + You do not need to add custom error components or directives for validation. The system works out of the box, including for nested and dynamically generated controls. + +#### Real-World Example: Nested Form Groups in the Users Form + +Below is a real example from the Users management form in ABP Angular UI, showing how nested form structures and validation are implemented. This example includes both dynamically generated fields (with `abp-extensible-form`) and a dynamic list of roles using `FormArray` and `FormGroup`. + +**TypeScript: Building the Form** + +```ts +buildForm() { + const data = new FormPropData(this.injector, this.selected); + this.form = generateFormFromProps(data); // Automatically creates form controls from entity and extension properties + + this.service.getAssignableRoles().subscribe(({ items }) => { + this.roles = items; + if (this.roles) { + // Dynamic roles list: nested FormArray and FormGroup + this.form.addControl( + 'roleNames', + this.fb.array( + this.roles.map(role => + this.fb.group({ + [role.name as string]: [ + this.selected?.id + ? !!this.selectedUserRoles?.find(userRole => userRole.id === role.id) + : role.isDefault, + ], + }), + ), + ), + ); + } + }); +} +``` + +**HTML: Displaying the Form** + +```html + + +

{{ (selected?.id ? 'AbpIdentity::Edit' : 'AbpIdentity::NewUser') | abpLocalization }}

+
+ + + @if (form) { +
+ +
+
+ } @else { +
+ } +
+
+``` + +**Explanation:** +- `abp-extensible-form` automatically generates and displays all entity fields and their validation. +- In the Roles tab, each role is represented by a checkbox, and these checkboxes are managed in a `FormArray`, with each as a `FormGroup`. This is a real-world example of a nested form structure. +- All validation and error messages are shown automatically for both the main form and nested groups. + + +### 2nd Way: Manual Nested Reactive Forms Without abp-extensible-form + +You can also build and validate nested form groups manually, without using `abp-extensible-form` or dynamic helpers. This approach gives you full control over the form structure and is useful for custom or non-entity-based forms. + +#### Example: Simple Manual Nested FormGroup + +Below is a simple, generic example of a nested reactive form. This form includes a nested `FormGroup` for profile information and demonstrates how to apply validation rules. + +**TypeScript: Building the Form** + +```ts +import { Component, OnInit, inject } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { NgxValidateCoreModule } from '@ngx-validate/core'; + +@Component({ + selector: 'app-nested-form', + templateUrl: './nested-form.component.html', + standalone: true, + imports: [NgxValidateCoreModule], +}) +export class NestedFormComponent implements OnInit { + form: FormGroup; + + private fb = inject(FormBuilder); + + ngOnInit() { + this.buildForm(); + } + + buildForm() { + this.form = this.fb.group({ + userName: ['', Validators.required], + email: ['', [Validators.required, Validators.email]], + profile: this.fb.group({ + firstName: ['', Validators.required], + lastName: ['', Validators.required], + }), + }); + } + + submit() { + if (this.form.invalid) { + return; + } + // handle submit + } +} +``` + +**HTML: Displaying the Form** + +```html +
+
+ + +
+ +
+ + +
+ +
+
+ Profile Details +
+
+
+ + +
+ +
+ + +
+
+
+ +
+ +
+ + Save + +
+
+``` + +**How it works:** +- The form contains main fields (`userName`, `email`) and a nested `FormGroup` (`profile`). +- The `profile` group includes `firstName` and `lastName` fields, each with their own validation rules. +- Validation rules are defined directly in the form builder. +- Error messages and validation feedback are handled automatically by ngx-validate and ABP Angular UI, just like with dynamic forms. +- This structure ensures that validation works automatically for both the main form and nested groups. + +> **Note:** This approach is ideal for custom forms or when you want full control over the form structure. It provides a user experience and validation behavior similar to ABP's dynamic forms, but with manual control over the form layout and logic. + +--- \ No newline at end of file diff --git a/docs/en/framework/ui/angular/modifying-the-menu.md b/docs/en/framework/ui/angular/modifying-the-menu.md index ee3997b9d7..44b2bb0bfd 100644 --- a/docs/en/framework/ui/angular/modifying-the-menu.md +++ b/docs/en/framework/ui/angular/modifying-the-menu.md @@ -269,7 +269,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/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/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/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/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/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 45444ee76f..e67353b1fa 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. @@ -1288,28 +1282,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-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 06db5f37d9..18ecd2726d 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); } ``` @@ -1220,13 +1202,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 d4137e3681..02d1f81aba 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 9c037617a1..e72e94fc66 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); } ``` 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/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 d2082667aa..46a693a963 100644 --- a/docs/en/tutorials/modular-crm/part-05.md +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -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/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 4167492d16..e304510e48 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -491,6 +491,10 @@ 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 @@ -1467,6 +1471,14 @@ 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 @@ -1718,6 +1730,8 @@ 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 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.Authentication.OpenIdConnect/Volo/Abp/AspNetCore/Authentication/OpenIdConnect/AbpAspNetCoreAuthenticationOpenIdConnectModule.cs b/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo/Abp/AspNetCore/Authentication/OpenIdConnect/AbpAspNetCoreAuthenticationOpenIdConnectModule.cs index ba21ef11fd..4fc7c4ca75 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo/Abp/AspNetCore/Authentication/OpenIdConnect/AbpAspNetCoreAuthenticationOpenIdConnectModule.cs +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Volo/Abp/AspNetCore/Authentication/OpenIdConnect/AbpAspNetCoreAuthenticationOpenIdConnectModule.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Authentication.OAuth; +using Volo.Abp.AspNetCore.Security; using Volo.Abp.Modularity; using Volo.Abp.MultiTenancy; using Volo.Abp.RemoteServices; @@ -16,5 +17,10 @@ public class AbpAspNetCoreAuthenticationOpenIdConnectModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddHttpClient(); + + Configure(options => + { + options.IgnoredScriptNoncePaths.Add("/signout-oidc"); + }); } } 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.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs index d24e5d6976..870d4a6e5f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly.Theming.Bundling/BlazorWebAssemblyScriptContributor.cs @@ -10,7 +10,6 @@ public class BlazorWebAssemblyScriptContributor : BundleContributor context.Files.AddIfNotContains("_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"); context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/abp.js"); context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); - context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/lang-utils.js"); context.Files.AddIfNotContains("_content/Volo.Abp.AspNetCore.Components.Web/libs/abp/js/authentication-state-listener.js"); } } 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.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcRemoteTenantStore.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcRemoteTenantStore.cs index ed4739c93c..aeb94bc533 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcRemoteTenantStore.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo/Abp/AspNetCore/Mvc/Client/MvcRemoteTenantStore.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Pages.Abp.MultiTenancy.ClientProxies; using Volo.Abp.Caching; @@ -13,6 +15,8 @@ namespace Volo.Abp.AspNetCore.Mvc.Client; public class MvcRemoteTenantStore : ITenantStore, ITransientDependency { + public ILogger Logger { get; set; } + protected AbpTenantClientProxy TenantAppService { get; } protected IHttpContextAccessor HttpContextAccessor { get; } protected IDistributedCache Cache { get; } @@ -24,6 +28,8 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency IDistributedCache cache, IOptions options) { + Logger = NullLogger.Instance; + TenantAppService = tenantAppService; HttpContextAccessor = httpContextAccessor; Cache = cache; @@ -45,6 +51,11 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency { var tenant = await TenantAppService.FindTenantByNameAsync(normalizedName); tenantConfiguration = await Cache.GetAsync(cacheKey); + if (tenant.Success && tenantConfiguration?.Value == null) + { + Logger.LogWarning($"Tenant with name '{normalizedName}' was found, but not present in the distributed cache, " + + "Ensure all applications use the same distributed cache and the same cache key prefix"); + } } if (httpContext != null) @@ -68,8 +79,13 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency var tenantConfiguration = await Cache.GetAsync(cacheKey); if (tenantConfiguration?.Value == null) { - await TenantAppService.FindTenantByIdAsync(id); + var tenant = await TenantAppService.FindTenantByIdAsync(id); tenantConfiguration = await Cache.GetAsync(cacheKey); + if (tenant.Success && tenantConfiguration?.Value == null) + { + Logger.LogWarning($"Tenant with ID '{id}' was found, but not present in the distributed cache, " + + "Ensure all applications use the same distributed cache and the same cache key prefix"); + } } if (httpContext != null) @@ -98,8 +114,13 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency var tenantConfiguration = Cache.Get(cacheKey); if (tenantConfiguration?.Value == null) { - AsyncHelper.RunSync(async () => await TenantAppService.FindTenantByNameAsync(normalizedName)); + var tenant = AsyncHelper.RunSync(async () => await TenantAppService.FindTenantByNameAsync(normalizedName)); tenantConfiguration = Cache.Get(cacheKey); + if (tenant.Success && tenantConfiguration?.Value == null) + { + Logger.LogWarning($"Tenant with name '{normalizedName}' was found, but not present in the distributed cache, " + + "Ensure all applications use the same distributed cache and the same cache key prefix"); + } } if (httpContext != null) @@ -123,8 +144,13 @@ public class MvcRemoteTenantStore : ITenantStore, ITransientDependency var tenantConfiguration = Cache.Get(cacheKey); if (tenantConfiguration?.Value == null) { - AsyncHelper.RunSync(async () => await TenantAppService.FindTenantByIdAsync(id)); + var tenant = AsyncHelper.RunSync(async () => await TenantAppService.FindTenantByIdAsync(id)); tenantConfiguration = Cache.Get(cacheKey); + if (tenant.Success && tenantConfiguration?.Value == null) + { + Logger.LogWarning($"Tenant with ID '{id}' was found, but not present in the distributed cache, " + + "Ensure all applications use the same distributed cache and the same cache key prefix"); + } } if (httpContext != null) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelperService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelperService.cs index 8b46f6e78c..3365ff6a85 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelperService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Modal/AbpModalFooterTagHelperService.cs @@ -96,7 +96,7 @@ public class AbpModalFooterTagHelperService : AbpTagHelperService var id = TagHelper.Name + "Content"; var wrapper = new TagBuilder("div"); - wrapper.AddCssClass("tab-content pt-3"); + wrapper.AddCssClass("tab-content"); wrapper.Attributes.Add("id", id); wrapper.InnerHtml.AppendHtml(contents); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs index 62f7950c5a..f65f8b13e9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleConfigurationExtensions.cs @@ -23,6 +23,18 @@ public static class BundleConfigurationExtensions return bundleConfiguration; } + public static BundleConfiguration RemoveFiles(this BundleConfiguration bundleConfiguration, params string[] files) + { + bundleConfiguration.Contributors.RemoveBundleFile(files.ToArray()); + return bundleConfiguration; + } + + public static BundleConfiguration RemoveFiles(this BundleConfiguration bundleConfiguration, Func predicate) + { + bundleConfiguration.Contributors.RemoveBundleFile(predicate); + return bundleConfiguration; + } + public static BundleConfiguration AddContributors(this BundleConfiguration bundleConfiguration, params IBundleContributor[] contributors) { Check.NotNull(contributors, nameof(contributors)); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollection.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollection.cs index 5f245c1827..e92538a23f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollection.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollection.cs @@ -76,6 +76,36 @@ public class BundleContributorCollection } } + public void RemoveBundleFile(string fileName) + { + RemoveBundleFile([fileName]); + } + + public void RemoveBundleFile(string[] fileNames) + { + var contributors = _contributors + .Where(x => x is BundleFileContributor bundleContributor && + bundleContributor.Files.Any(f => fileNames.Any(name => name.Equals(f.FileName, StringComparison.OrdinalIgnoreCase)))) + .Cast(); + foreach (var contributor in contributors) + { + contributor.Files.RemoveAll(x => fileNames.Any(name => name.Equals(x.FileName, StringComparison.OrdinalIgnoreCase))); + } + } + + public void RemoveBundleFile(Func predicate) + { + var contributors = _contributors + .Where(x => x is BundleFileContributor bundleContributor && + bundleContributor.Files.Any(f => predicate(f.FileName))) + .Cast(); + + foreach (var contributor in contributors) + { + contributor.Files.RemoveAll(x => predicate(x.FileName)); + } + } + public IReadOnlyList GetAll() { return _contributors.ToImmutableList(); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs index addbe7e2cd..74f73fa1f3 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bundling.Abstractions/Volo/Abp/AspNetCore/Mvc/UI/Bundling/BundleContributorCollectionExtensions.cs @@ -1,4 +1,7 @@ -namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; +using System; +using System.Linq; + +namespace Volo.Abp.AspNetCore.Mvc.UI.Bundling; public static class BundleContributorCollectionExtensions { @@ -16,4 +19,14 @@ public static class BundleContributorCollectionExtensions { contributors.Add(new BundleFileContributor(files)); } + + public static void RemoveFile(this BundleContributorCollection contributors, params string[] files) + { + contributors.RemoveBundleFile(files.ToArray()); + } + + public static void RemoveFile(this BundleContributorCollection contributors, Func predicate) + { + contributors.RemoveBundleFile(predicate); + } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JQueryValidation/JQueryValidationScriptContributor.cs b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JQueryValidation/JQueryValidationScriptContributor.cs index 7dd24fad23..cc5d45dcaa 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JQueryValidation/JQueryValidationScriptContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Packages/Volo/Abp/AspNetCore/Mvc/UI/Packages/JQueryValidation/JQueryValidationScriptContributor.cs @@ -15,6 +15,10 @@ public class JQueryValidationScriptContributor : BundleContributor public override void ConfigureBundle(BundleConfigurationContext context) { context.Files.AddIfNotContains("/libs/jquery-validation/jquery.validate.js"); + if (context.FileProvider.GetFileInfo("/libs/jquery-validation/abp.jquery.validate.js").Exists) + { + context.Files.AddIfNotContains("/libs/jquery-validation/abp.jquery.validate.js"); + } } public override void ConfigureDynamicResources(BundleConfigurationContext context) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index 081342b913..cb63a9e6c8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -1,5 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.Extensions.DependencyInjection; @@ -14,6 +18,7 @@ using Volo.Abp.AspNetCore.Mvc.Response; using Volo.Abp.AspNetCore.Mvc.Uow; using Volo.Abp.AspNetCore.Mvc.Validation; using Volo.Abp.Content; +using Volo.Abp.Json.SystemTextJson.JsonConverters; namespace Volo.Abp.AspNetCore.Mvc; @@ -32,6 +37,17 @@ internal static class AbpMvcOptionsExtensions private static void AddFormatters(MvcOptions options) { options.OutputFormatters.Insert(0, new RemoteStreamContentOutputFormatter()); + var systemTextJsonOutputFormatter = options.OutputFormatters + .Where(f => f is SystemTextJsonOutputFormatter) + .Cast().FirstOrDefault(); + + if (systemTextJsonOutputFormatter != null) + { + options.OutputFormatters.Remove(systemTextJsonOutputFormatter); + var jsonOptions = new JsonSerializerOptions(systemTextJsonOutputFormatter.SerializerOptions); + jsonOptions.Converters.RemoveAll(x => x is ObjectToInferredTypesConverter); + options.OutputFormatters.Add(new SystemTextJsonOutputFormatter(jsonOptions)); + } } private static void AddConventions(MvcOptions options, IServiceCollection services) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationPartSorter.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationPartSorter.cs index 9bc15ae617..d0ba8b98c0 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationPartSorter.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationPartSorter.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Volo.Abp.Modularity; @@ -15,152 +13,36 @@ public static class ApplicationPartSorter { public static void Sort(ApplicationPartManager partManager, IModuleContainer moduleContainer) { - /* Performing a double Reverse() to preserve the original order for non-sorted parts - */ + var orderedModuleAssemblies = moduleContainer.Modules + .Select((moduleDescriptor, index) => new { moduleDescriptor.Assembly, index }) + .ToDictionary(x => x.Assembly, x => x.index); - var dependencyDictionary = CreateDependencyDictionary(partManager, moduleContainer); + var modulesAssemblies = moduleContainer.Modules.Select(x => x.Assembly).ToList(); + var sortedTypes = partManager.ApplicationParts + .Where(x => modulesAssemblies.Contains(GetApplicationPartAssembly(x))) + .OrderBy(x => orderedModuleAssemblies[GetApplicationPartAssembly(x)]) + .ToList(); - var sortedParts = partManager - .ApplicationParts - .Reverse() //First Revers - .SortByDependencies(p => dependencyDictionary[p]); + var sortIndex = 0; + var sortedParts = partManager.ApplicationParts + .Select(x => modulesAssemblies.Contains(GetApplicationPartAssembly(x)) ? sortedTypes[sortIndex++] : x) + .ToList(); - sortedParts.Reverse(); //Reverse again - - //Replace the original parts with the sorted parts partManager.ApplicationParts.Clear(); + sortedParts.Reverse(); foreach (var applicationPart in sortedParts) { partManager.ApplicationParts.Add(applicationPart); } } - private static Dictionary> CreateDependencyDictionary( - ApplicationPartManager partManager, IModuleContainer moduleContainer) - { - var dependencyDictionary = new Dictionary>(); - - foreach (var applicationPart in partManager.ApplicationParts) - { - dependencyDictionary[applicationPart] = - CreateDependencyList(applicationPart, partManager, moduleContainer); - } - - return dependencyDictionary; - } - - private static List CreateDependencyList( - ApplicationPart applicationPart, - ApplicationPartManager partManager, - IModuleContainer moduleContainer) + private static Assembly GetApplicationPartAssembly(ApplicationPart part) { - var list = new List(); - - if (applicationPart is AssemblyPart assemblyPart) + return part switch { - AddDependencies(list, assemblyPart, partManager, moduleContainer); - } - else if (applicationPart is CompiledRazorAssemblyPart compiledRazorAssemblyPart) - { - AddDependencies(list, compiledRazorAssemblyPart, partManager, moduleContainer); - } - - return list; - } - - private static void AddDependencies( - List list, - AssemblyPart assemblyPart, - ApplicationPartManager partManager, - IModuleContainer moduleContainer) - { - var dependedAssemblyParts = GetDependedAssemblyParts( - partManager, - moduleContainer, - assemblyPart - ); - - list.AddRange(dependedAssemblyParts); - - foreach (var dependedAssemblyPart in dependedAssemblyParts) - { - var viewsPart = GetViewsPartOrNull(partManager, dependedAssemblyPart); - if (viewsPart != null) - { - list.Add(viewsPart); - } - } - } - - private static void AddDependencies( - List list, - CompiledRazorAssemblyPart compiledRazorAssemblyPart, - ApplicationPartManager partManager, - IModuleContainer moduleContainer) - { - if (!compiledRazorAssemblyPart.Name.EndsWith(".Views")) - { - return; - } - - var originalAssemblyPart = GetOriginalAssemblyPartOrNull(compiledRazorAssemblyPart, partManager); - if (originalAssemblyPart == null) - { - return; - } - - list.Add(originalAssemblyPart); - } - - private static AssemblyPart[] GetDependedAssemblyParts( - ApplicationPartManager partManager, - IModuleContainer moduleContainer, - AssemblyPart assemblyPart) - { - var moduleDescriptor = GetModuleDescriptorForAssemblyOrNull(moduleContainer, assemblyPart.Assembly); - if (moduleDescriptor == null) - { - return Array.Empty(); - } - - var moduleDependedAssemblies = moduleDescriptor - .Dependencies - .SelectMany(d => d.AllAssemblies) - .ToArray(); - - return partManager.ApplicationParts - .OfType() - .Where(a => a.Assembly.IsIn(moduleDependedAssemblies)) - .Distinct() - .ToArray(); - } - - private static CompiledRazorAssemblyPart? GetViewsPartOrNull(ApplicationPartManager partManager, - ApplicationPart assemblyPart) - { - var viewsAssemblyName = assemblyPart.Name + ".Views"; - return partManager - .ApplicationParts - .OfType() - .FirstOrDefault(p => p.Name == viewsAssemblyName); - } - - private static AssemblyPart? GetOriginalAssemblyPartOrNull( - CompiledRazorAssemblyPart compiledRazorAssemblyPart, - ApplicationPartManager partManager) - { - var originalAssemblyName = compiledRazorAssemblyPart.Name.RemovePostFix(".Views"); - return partManager.ApplicationParts - .OfType() - .FirstOrDefault(p => p.Name == originalAssemblyName); - } - - private static IAbpModuleDescriptor? GetModuleDescriptorForAssemblyOrNull( - IModuleContainer moduleContainer, - Assembly assembly) - { - return moduleContainer - .Modules - .FirstOrDefault(m => m.AllAssemblies.Contains(assembly)); + AssemblyPart assemblyPart => assemblyPart.Assembly, + CompiledRazorAssemblyPart compiledRazorAssemblyPart => compiledRazorAssemblyPart.Assembly, + _ => throw new AbpException("Unknown application part type") + }; } } diff --git a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs index 74def27c8e..c37e75ef32 100644 --- a/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore/Microsoft/Extensions/DependencyInjection/CookieAuthenticationOptionsExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Globalization; using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; diff --git a/framework/src/Volo.Abp.AspNetCore/Volo.Abp.AspNetCore.csproj b/framework/src/Volo.Abp.AspNetCore/Volo.Abp.AspNetCore.csproj index 886e1ed700..3300bce024 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo.Abp.AspNetCore.csproj +++ b/framework/src/Volo.Abp.AspNetCore/Volo.Abp.AspNetCore.csproj @@ -30,7 +30,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs index 2d231c46ee..f1f2c9016e 100644 --- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs +++ b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Security/AbpSecurityHeadersMiddleware.cs @@ -25,7 +25,9 @@ public class AbpSecurityHeadersMiddleware : AbpMiddlewareBase, ITransientDepende { var endpoint = context.GetEndpoint(); - if (endpoint?.Metadata.GetMetadata() != null) + if (endpoint?.Metadata.GetMetadata() != null || + await AlwaysIgnoreContentTypes(context) || + Options.Value.IgnoredScriptNoncePaths.Any(x => context.Request.Path.StartsWithSegments(x.EnsureStartsWith('/'), StringComparison.OrdinalIgnoreCase))) { await next.Invoke(context); return; @@ -41,13 +43,13 @@ public class AbpSecurityHeadersMiddleware : AbpMiddlewareBase, ITransientDepende AddHeader(context, "X-Frame-Options", "SAMEORIGIN"); var requestAcceptTypeHtml = context.Request.Headers["Accept"].Any(x => - x!.Contains("text/html") || x.Contains("*/*") || x.Contains("application/xhtml+xml")); + x!.Contains("text/html", StringComparison.OrdinalIgnoreCase) || + x.Contains("*/*", StringComparison.OrdinalIgnoreCase) || + x.Contains("application/xhtml+xml", StringComparison.OrdinalIgnoreCase)); if (!requestAcceptTypeHtml || !Options.Value.UseContentSecurityPolicyHeader - || await AlwaysIgnoreContentTypes(context) - || endpoint == null - || Options.Value.IgnoredScriptNoncePaths.Any(x => context.Request.Path.StartsWithSegments(x.EnsureStartsWith('/'), StringComparison.OrdinalIgnoreCase))) + || endpoint == null) { AddOtherHeaders(context); await next.Invoke(context); @@ -60,7 +62,6 @@ public class AbpSecurityHeadersMiddleware : AbpMiddlewareBase, ITransientDepende context.Items.Add(AbpAspNetCoreConsts.ScriptNonceKey, randomValue); } - context.Response.OnStarting(() => { if (context.Response.Headers.ContainsKey("Content-Security-Policy")) diff --git a/framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj b/framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj index 8d12eca643..e56ddd83a9 100644 --- a/framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj +++ b/framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj @@ -4,7 +4,7 @@ - net9.0 + net8.0;net9.0 enable Nullable Volo.Abp.AutoMapper diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs index e834b3d00d..1c9f4b2dd8 100644 --- a/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs +++ b/framework/src/Volo.Abp.Autofac/Autofac/Builder/AbpRegistrationBuilderExtensions.cs @@ -126,7 +126,8 @@ public static class AbpRegistrationBuilderExtensions } else { - if (serviceRegistrationActionList.IsClassInterceptorsDisabled) + if (serviceRegistrationActionList.IsClassInterceptorsDisabled || + serviceRegistrationActionList.DisabledClassInterceptorsSelectors.Any(selector => selector.Predicate(serviceType))) { return registrationBuilder; } diff --git a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ClientConfig.cs b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ClientConfig.cs index 02b8397917..19db1beef7 100644 --- a/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ClientConfig.cs +++ b/framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ClientConfig.cs @@ -11,5 +11,8 @@ public class ClientConfig public ServiceBusClientOptions Client { get; set; } = new(); - public ServiceBusProcessorOptions Processor { get; set; } = new(); + public ServiceBusProcessorOptions Processor { get; set; } = new () + { + AutoCompleteMessages = false + }; } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs index 367b0716a8..3b1b18e8b3 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/AbpBackgroundWorkersModule.cs @@ -1,5 +1,7 @@ -using System.Threading.Tasks; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Options; using Volo.Abp.Data; using Volo.Abp.Modularity; @@ -29,9 +31,11 @@ public class AbpBackgroundWorkersModule : AbpModule var options = context.ServiceProvider.GetRequiredService>().Value; if (options.IsEnabled) { + var hostApplicationLifetime = context.ServiceProvider.GetService(); + var cancellationToken = hostApplicationLifetime?.ApplicationStopping ?? CancellationToken.None; await context.ServiceProvider .GetRequiredService() - .StartAsync(); + .StartAsync(cancellationToken); } } @@ -40,9 +44,11 @@ public class AbpBackgroundWorkersModule : AbpModule var options = context.ServiceProvider.GetRequiredService>().Value; if (options.IsEnabled) { + var hostApplicationLifetime = context.ServiceProvider.GetService(); + var cancellationToken = hostApplicationLifetime?.ApplicationStopping ?? CancellationToken.None; await context.ServiceProvider .GetRequiredService() - .StopAsync(); + .StopAsync(cancellationToken); } } diff --git a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkersApplicationInitializationContextExtensions.cs b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkersApplicationInitializationContextExtensions.cs index 652aa35741..4f7a65226d 100644 --- a/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkersApplicationInitializationContextExtensions.cs +++ b/framework/src/Volo.Abp.BackgroundWorkers/Volo/Abp/BackgroundWorkers/BackgroundWorkersApplicationInitializationContextExtensions.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; namespace Volo.Abp.BackgroundWorkers; @@ -28,6 +29,15 @@ public static class BackgroundWorkersApplicationInitializationContextExtensions throw new AbpException($"Given type ({workerType.AssemblyQualifiedName}) must implement the {typeof(IBackgroundWorker).AssemblyQualifiedName} interface, but it doesn't!"); } + if (cancellationToken == default) + { + var hostApplicationLifetime = context.ServiceProvider.GetService(); + if (hostApplicationLifetime != null) + { + cancellationToken = hostApplicationLifetime.ApplicationStopping; + } + } + await context.ServiceProvider .GetRequiredService() .AddAsync((IBackgroundWorker)context.ServiceProvider.GetRequiredService(workerType), cancellationToken); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs index 4b8d71214b..de13a43e37 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Auth/AuthService.cs @@ -3,7 +3,7 @@ using System.IO; using System.Net.Http; using System.Text; using System.Threading.Tasks; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.Extensions.Logging; using Volo.Abp.Cli.Http; using Volo.Abp.Cli.ProjectBuilding; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs index 803f208d8a..1791a5e928 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/AddModuleCommand.cs @@ -69,6 +69,7 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency var newProTemplate = !string.IsNullOrEmpty(template) && template == ModuleProTemplate.TemplateName; var withSourceCode = newTemplate || newProTemplate || commandLineArgs.Options.ContainsKey(Options.SourceCode.Long); var addSourceCodeToSolutionFile = withSourceCode && commandLineArgs.Options.ContainsKey("add-to-solution-file"); + var skipOpeningDocumentation = commandLineArgs.Options.ContainsKey(Options.SkipOpeningDocumentation.Long); var skipDbMigrations = newTemplate || newProTemplate || commandLineArgs.Options.ContainsKey(Options.DbMigrations.Skip); var solutionFile = GetSolutionFile(commandLineArgs); @@ -98,7 +99,8 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency withSourceCode, addSourceCodeToSolutionFile, newTemplate, - newProTemplate + newProTemplate, + skipOpeningDocumentation ); _lastAddedModuleInfo = new AddModuleInfoOutput @@ -223,5 +225,10 @@ public class AddModuleCommand : IConsoleCommand, ITransientDependency public const string Short = "t"; public const string Long = "template"; } + + public class SkipOpeningDocumentation + { + public const string Long = "skip-opening-documentation"; + } } } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs index 71964653a7..092e996095 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/CleanCommand.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Cli.Args; +using Volo.Abp.Cli.Utils; using Volo.Abp.DependencyInjection; namespace Volo.Abp.Cli.Commands; @@ -13,11 +14,14 @@ namespace Volo.Abp.Cli.Commands; public class CleanCommand : IConsoleCommand, ITransientDependency { public const string Name = "clean"; - + public ILogger Logger { get; set; } - public CleanCommand() + protected ICmdHelper CmdHelper { get; } + + public CleanCommand(ICmdHelper cmdHelper) { + CmdHelper = cmdHelper; Logger = NullLogger.Instance; } @@ -26,6 +30,10 @@ public class CleanCommand : IConsoleCommand, ITransientDependency var binEntries = Directory.EnumerateDirectories(Directory.GetCurrentDirectory(), "bin", SearchOption.AllDirectories); var objEntries = Directory.EnumerateDirectories(Directory.GetCurrentDirectory(), "obj", SearchOption.AllDirectories); + Logger.LogInformation("Cleaning the solution with 'dotnet clean' command..."); + CmdHelper.RunCmd($"dotnet clean", workingDirectory: Directory.GetCurrentDirectory()); + + Logger.LogInformation($"Removing 'bin' and 'obj' folders..."); foreach (var path in binEntries.Concat(objEntries)) { if (path.IndexOf("node_modules", StringComparison.OrdinalIgnoreCase) > 0) @@ -38,9 +46,9 @@ public class CleanCommand : IConsoleCommand, ITransientDependency Directory.Delete(path, true); } } + Logger.LogInformation($"'bin' and 'obj' folders removed successfully!"); - Logger.LogInformation($"BIN and OBJ folders have been successfully deleted!"); - + Logger.LogInformation("Solution cleaned successfully!"); return Task.CompletedTask; } diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClientExtensions.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClientExtensions.cs index ab77468f3e..f6da2e46e4 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClientExtensions.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClientExtensions.cs @@ -5,7 +5,7 @@ using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Polly; using Polly.Extensions.Http; using Volo.Abp.Cli.Auth; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularSourceCodeAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularSourceCodeAdder.cs index f996247217..aecc1ad222 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularSourceCodeAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/AngularSourceCodeAdder.cs @@ -14,11 +14,11 @@ namespace Volo.Abp.Cli.ProjectModification; public class AngularSourceCodeAdder : ITransientDependency { - public ILogger Logger { get; set; } + public ILogger Logger { get; set; } public AngularSourceCodeAdder() { - Logger = NullLogger.Instance; + Logger = NullLogger.Instance; } public async Task AddFromModuleAsync(string solutionFilePath, string angularPath) diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs index 1c6a2abd3f..8a703a8deb 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs @@ -98,7 +98,8 @@ public class SolutionModuleAdder : ITransientDependency bool withSourceCode = false, bool addSourceCodeToSolutionFile = false, bool newTemplate = false, - bool newProTemplate = false) + bool newProTemplate = false, + bool skipOpeningDocumentation = false) { Check.NotNull(solutionFile, nameof(solutionFile)); Check.NotNull(moduleName, nameof(moduleName)); @@ -159,10 +160,13 @@ public class SolutionModuleAdder : ITransientDependency await SetLeptonXAbpVersionsAsync(solutionFile, Path.Combine(modulesFolderInSolution, module.Name)); } - var documentationLink = module.GetFirstDocumentationLinkOrNull(); - if (documentationLink != null) + if (!skipOpeningDocumentation) { - CmdHelper.Open(documentationLink); + var documentationLink = module.GetFirstDocumentationLinkOrNull(); + if (documentationLink != null) + { + CmdHelper.Open(documentationLink); + } } return module; diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/CSharp/CSharpServiceProxyGenerator.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/CSharp/CSharpServiceProxyGenerator.cs index e0dcc14fb1..64947e637f 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/CSharp/CSharpServiceProxyGenerator.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ServiceProxying/CSharp/CSharpServiceProxyGenerator.cs @@ -19,7 +19,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase" + $"{Environment.NewLine}" + $"{Environment.NewLine}// ReSharper disable once CheckNamespace" + @@ -44,7 +44,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase;" + $"{Environment.NewLine}" + @@ -53,7 +53,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase" + $"{Environment.NewLine}" + $"{Environment.NewLine}// ReSharper disable once CheckNamespace" + @@ -65,7 +65,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase" + $"{Environment.NewLine}" + $"{Environment.NewLine}// ReSharper disable once CheckNamespace" + @@ -77,7 +77,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase ClassUsingNamespaceList = new() + private static readonly List ClassUsingNamespaceList = new() { "using System;", "using System.Collections.Generic;", @@ -90,7 +90,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase InterfaceUsingNamespaceList = new() + private static readonly List InterfaceUsingNamespaceList = new() { "using System;", "using System.Collections.Generic;", @@ -100,7 +100,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase DtoUsingNamespaceList = new() + private static readonly List DtoUsingNamespaceList = new() { "using System;", "using System.Collections.Generic;", @@ -116,7 +116,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase usingNamespaceList) { - var returnSign = returnTypeName == "void" ? "Task" : $"Task<{returnTypeName}>"; + var isAsyncEnumerable = returnTypeName.StartsWith("IAsyncEnumerable<"); + var asyncEnumerableTypeName = isAsyncEnumerable + ? returnTypeName.Substring("IAsyncEnumerable<".Length, returnTypeName.Length - "IAsyncEnumerable<".Length - 1) + : null; + + var returnSign = isAsyncEnumerable ? returnTypeName : returnTypeName == "void" ? "Task" : $"Task<{returnTypeName}>"; - methodBuilder.AppendLine($"public virtual async {returnSign} {action.Name}()"); + methodBuilder.AppendLine(isAsyncEnumerable + ? $"public virtual {returnSign} {action.Name}()" + : $"public virtual async {returnSign} {action.Name}()"); foreach (var parameter in action.ParametersOnMethod) { @@ -325,9 +332,11 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase(nameof({action.Name}), {args});"); + methodBuilder.AppendLine(isAsyncEnumerable + ? $" return RequestAsyncEnumerable<{asyncEnumerableTypeName}>(nameof({action.Name}), {args});" + : returnTypeName == "void" + ? $" await RequestAsync(nameof({action.Name}), {args});" + : $" return await RequestAsync<{returnTypeName}>(nameof({action.Name}), {args});"); foreach (var parameter in action.ParametersOnMethod) { @@ -543,7 +552,7 @@ public class CSharpServiceProxyGenerator : ServiceProxyGeneratorBase exposeAction) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ClassInterceptorsSelectorList.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ClassInterceptorsSelectorList.cs new file mode 100644 index 0000000000..11abc597c6 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ClassInterceptorsSelectorList.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Volo.Abp.DependencyInjection; + +public class ClassInterceptorsSelectorList : List, IClassInterceptorsSelectorList +{ + +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs index d9fae16e16..0f83fcced7 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs @@ -19,16 +19,15 @@ public abstract class ConventionalRegistrarBase : IConventionalRegistrar { types = AssemblyHelper .GetAllTypes(assembly) - .Where( - type => type != null && - type.IsClass && - !type.IsAbstract && - !type.IsGenericType - ).ToArray(); + .Where(type => type != null && type.IsClass && !type.IsAbstract && !type.IsGenericType) + .ToArray(); } catch (ReflectionTypeLoadException e) { - types = e.Types.Select(x => x!).ToArray(); + types = e.Types + .Where(type => type != null && type.IsClass && !type.IsAbstract && !type.IsGenericType) + .Select(x => x!) + .ToArray(); logger.LogException(e); } catch (Exception e) diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IClassInterceptorsSelectorList.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IClassInterceptorsSelectorList.cs new file mode 100644 index 0000000000..77991d813e --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IClassInterceptorsSelectorList.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Volo.Abp.DependencyInjection; + +public interface IClassInterceptorsSelectorList : IList +{ + +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ServiceRegistrationActionList.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ServiceRegistrationActionList.cs index 8d4ce1b521..750a4c523e 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ServiceRegistrationActionList.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ServiceRegistrationActionList.cs @@ -6,4 +6,11 @@ namespace Volo.Abp.DependencyInjection; public class ServiceRegistrationActionList : List> { public bool IsClassInterceptorsDisabled { get; set; } + + public IClassInterceptorsSelectorList DisabledClassInterceptorsSelectors { get; } + + public ServiceRegistrationActionList() + { + DisabledClassInterceptorsSelectors = new ClassInterceptorsSelectorList(); + } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs index 8a67d20b31..0118fd9874 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeHelper.cs @@ -80,6 +80,37 @@ public static class TypeHelper return false; } + public static TProperty? ChangeTypePrimitiveExtended(object? value) + { + if (value == null) + { + return default; + } + + if (IsPrimitiveExtended(typeof(TProperty), includeEnums: true)) + { + var conversionType = typeof(TProperty); + if (IsNullable(conversionType)) + { + conversionType = conversionType.GetFirstGenericArgumentIfNullable(); + } + + if (conversionType == typeof(Guid)) + { + return (TProperty)TypeDescriptor.GetConverter(conversionType).ConvertFromInvariantString(value.ToString()!)!; + } + + if (conversionType.IsEnum) + { + return (TProperty)Enum.Parse(conversionType, value.ToString()!); + } + + return (TProperty)Convert.ChangeType(value, conversionType, CultureInfo.InvariantCulture); + } + + throw new AbpException("ChangeTypePrimitiveExtended does not support non-primitive types. Use non-generic GetProperty method and handle type casting manually."); + } + public static bool IsNullable(Type type) { return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); diff --git a/framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj b/framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj index 8be3e36e3a..699b187bfb 100644 --- a/framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj +++ b/framework/src/Volo.Abp.Dapr/Volo.Abp.Dapr.csproj @@ -18,7 +18,7 @@ - + diff --git a/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs b/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs index d78b65b5af..dcc59f0a69 100644 --- a/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs +++ b/framework/src/Volo.Abp.Dapr/Volo/Abp/Dapr/AbpDaprClientFactory.cs @@ -5,7 +5,7 @@ using System.Net.Http.Headers; using System.Text.Json; using System.Threading.Tasks; using Dapr.Client; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client; diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs index cd85acce50..6f8d626bbe 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/BasicRepositoryBase.cs @@ -1,9 +1,9 @@ -using JetBrains.Annotations; -using System; +using System; using System.Collections.Generic; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Volo.Abp.Data; @@ -44,9 +44,18 @@ public abstract class BasicRepositoryBase : public bool? IsChangeTrackingEnabled { get; protected set; } - protected BasicRepositoryBase() + public string? EntityName { get; set; } + + public void SetEntityName(string? name) { + EntityName = name; + } + public string ProviderName { get; } + + protected BasicRepositoryBase(string providerName) + { + ProviderName = Check.NotNullOrWhiteSpace(providerName, nameof(providerName)); } public abstract Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default); @@ -139,13 +148,19 @@ public abstract class BasicRepositoryBase : public abstract class BasicRepositoryBase : BasicRepositoryBase, IBasicRepository where TEntity : class, IEntity { + protected BasicRepositoryBase(string providerName) + : base(providerName) + { + + } + public virtual async Task GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default) { var entity = await FindAsync(id, includeDetails, cancellationToken); if (entity == null) { - throw new EntityNotFoundException(typeof(TEntity), id); + throw new EntityNotFoundException(id); } return entity; diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs index dc39255b25..cadc25da43 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IRepository.cs @@ -8,11 +8,15 @@ using Volo.Abp.Domain.Entities; namespace Volo.Abp.Domain.Repositories; /// -/// Just to mark a class as repository. +/// The base interface to implement a repository for an entity. /// public interface IRepository { bool? IsChangeTrackingEnabled { get; } + + string? EntityName { get; set; } + + string ProviderName { get; } } public interface IRepository : IReadOnlyRepository, IBasicRepository diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs index eb2457f8c8..62a13dd6b4 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs @@ -1,10 +1,10 @@ -using JetBrains.Annotations; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Volo.Abp.Domain.Entities; using Volo.Abp.MultiTenancy; using Volo.Abp.Uow; @@ -14,6 +14,11 @@ namespace Volo.Abp.Domain.Repositories; public abstract class RepositoryBase : BasicRepositoryBase, IRepository, IUnitOfWorkManagerAccessor where TEntity : class, IEntity { + protected RepositoryBase(string providerName) + : base(providerName) + { + } + [Obsolete("Use WithDetailsAsync method.")] public virtual IQueryable WithDetails() { @@ -55,7 +60,7 @@ public abstract class RepositoryBase : BasicRepositoryBase, IR if (entity == null) { - throw new EntityNotFoundException(typeof(TEntity)); + throw new EntityNotFoundException(); } return entity; @@ -92,6 +97,11 @@ public abstract class RepositoryBase : BasicRepositoryBase, IR public abstract class RepositoryBase : RepositoryBase, IRepository where TEntity : class, IEntity { + protected RepositoryBase(string providerName) + : base(providerName) + { + } + public abstract Task GetAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default); public abstract Task FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default); diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs index 3c43798c38..da0a25e4b6 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryExtensions.cs @@ -56,7 +56,7 @@ public static class RepositoryExtensions { if (!await repository.AnyAsync(x => x.Id!.Equals(id), cancellationToken)) { - throw new EntityNotFoundException(typeof(TEntity), id); + throw new EntityNotFoundException(id); } } @@ -69,7 +69,7 @@ public static class RepositoryExtensions { if (!await repository.AnyAsync(expression, cancellationToken)) { - throw new EntityNotFoundException(typeof(TEntity)); + throw new EntityNotFoundException(); } } @@ -250,4 +250,13 @@ public static class RepositoryExtensions hardDeleteEntities.Add(entity); await repository.DeleteAsync(entity, autoSave, cancellationToken); } + + public static TRepository SetEntityName( + this TRepository repository, + string name + ) where TRepository : class, IRepository + { + repository.EntityName = name; + return repository; + } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs index 0ed9a2e199..ff4aeabc44 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs @@ -1,6 +1,3 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Data; @@ -8,7 +5,10 @@ using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using Volo.Abp.Domain.Entities; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.DependencyInjection; @@ -61,7 +61,7 @@ public class EfCoreRepository : RepositoryBase, IE } [Obsolete("Use GetDbSetAsync() method.")] - public virtual DbSet DbSet => DbContext.Set(); + public virtual DbSet DbSet => GetDbSetInternal(DbContext); Task> IEfCoreRepository.GetDbSetAsync() { @@ -70,7 +70,7 @@ public class EfCoreRepository : RepositoryBase, IE protected async Task> GetDbSetAsync() { - return (await GetDbContextAsync()).Set(); + return GetDbSetInternal(await GetDbContextAsync()); } protected async Task GetDbConnectionAsync() @@ -93,6 +93,7 @@ public class EfCoreRepository : RepositoryBase, IE public IEfCoreBulkOperationProvider? BulkOperationProvider => LazyServiceProvider.LazyGetService(); public EfCoreRepository(IDbContextProvider dbContextProvider) + : base(AbpEfCoreConsts.ProviderName) { _dbContextProvider = dbContextProvider; @@ -110,7 +111,7 @@ public class EfCoreRepository : RepositoryBase, IE var dbContext = await GetDbContextAsync(); - var savedEntity = (await dbContext.Set().AddAsync(entity, GetCancellationToken(cancellationToken))).Entity; + var savedEntity = (await GetDbSetInternal(dbContext).AddAsync(entity, GetCancellationToken(cancellationToken))).Entity; if (autoSave) { @@ -120,6 +121,13 @@ public class EfCoreRepository : RepositoryBase, IE return savedEntity; } + private DbSet GetDbSetInternal(TDbContext dbContext) + { + return EntityName != null + ? dbContext.Set(EntityName) + : dbContext.Set(); + } + public async override Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { var entityArray = entities.ToArray(); @@ -147,7 +155,7 @@ public class EfCoreRepository : RepositoryBase, IE return; } - await dbContext.Set().AddRangeAsync(entityArray, cancellationToken); + await GetDbSetInternal(dbContext).AddRangeAsync(entityArray, cancellationToken); if (autoSave) { @@ -159,9 +167,10 @@ public class EfCoreRepository : RepositoryBase, IE { var dbContext = await GetDbContextAsync(); - if (dbContext.Set().Local.All(e => e != entity)) + var dbSet = GetDbSetInternal(dbContext); + if (dbSet.Local.All(e => e != entity)) { - dbContext.Set().Attach(entity); + dbSet.Attach(entity); dbContext.Update(entity); } @@ -197,7 +206,7 @@ public class EfCoreRepository : RepositoryBase, IE var dbContext = await GetDbContextAsync(); - dbContext.Set().UpdateRange(entityArray); + GetDbSetInternal(dbContext).UpdateRange(entityArray); if (autoSave) { @@ -209,7 +218,7 @@ public class EfCoreRepository : RepositoryBase, IE { var dbContext = await GetDbContextAsync(); - dbContext.Set().Remove(entity); + GetDbSetInternal(dbContext).Remove(entity); if (autoSave) { @@ -318,7 +327,7 @@ public class EfCoreRepository : RepositoryBase, IE public async override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { var dbContext = await GetDbContextAsync(); - var dbSet = dbContext.Set(); + var dbSet = GetDbSetInternal(dbContext); var entities = await dbSet .Where(predicate) @@ -335,8 +344,9 @@ public class EfCoreRepository : RepositoryBase, IE public async override Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default) { var dbContext = await GetDbContextAsync(); - var dbSet = dbContext.Set(); - await dbSet.Where(predicate).ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); + await GetDbSetInternal(dbContext) + .Where(predicate) + .ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); } public virtual async Task EnsureCollectionLoadedAsync( @@ -458,7 +468,7 @@ public class EfCoreRepository : EfCoreRepository(id); } return entity; @@ -470,7 +480,7 @@ public class EfCoreRepository : EfCoreRepository e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)) : !ShouldTrackingEntityChange() ? await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id!.Equals(id), GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()).FindAsync(new object[] {id!}, GetCancellationToken(cancellationToken)); + : await (await GetDbSetAsync()).FindAsync(new object[] { id! }, GetCancellationToken(cancellationToken)); } public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default) diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index e91d08b8c9..fceb338051 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -37,6 +37,7 @@ using Volo.Abp.Reflection; using Volo.Abp.Timing; using Volo.Abp.Uow; using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace Volo.Abp.EntityFrameworkCore; @@ -124,19 +125,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, TrySetDatabaseProvider(modelBuilder); - foreach (var entityType in modelBuilder.Model.GetEntityTypes()) + foreach (var entityType in modelBuilder.Model.GetEntityTypes().ToArray()) { - ConfigureBasePropertiesMethodInfo - .MakeGenericMethod(entityType.ClrType) - .Invoke(this, new object[] { modelBuilder, entityType }); - - ConfigureValueConverterMethodInfo - .MakeGenericMethod(entityType.ClrType) - .Invoke(this, new object[] { modelBuilder, entityType }); - - ConfigureValueGeneratedMethodInfo - .MakeGenericMethod(entityType.ClrType) - .Invoke(this, new object[] { modelBuilder, entityType }); + ConfigureEntityTypeProperties(modelBuilder, entityType); } if (LazyServiceProvider == null || Options == null) @@ -151,6 +142,23 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } + protected virtual void ConfigureEntityTypeProperties( + ModelBuilder modelBuilder, + IMutableEntityType entityType) + { + ConfigureBasePropertiesMethodInfo + .MakeGenericMethod(entityType.ClrType) + .Invoke(this, new object[] { modelBuilder, entityType }); + + ConfigureValueConverterMethodInfo + .MakeGenericMethod(entityType.ClrType) + .Invoke(this, new object[] { modelBuilder, entityType }); + + ConfigureValueGeneratedMethodInfo + .MakeGenericMethod(entityType.ClrType) + .Invoke(this, new object[] { modelBuilder, entityType }); + } + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { base.ConfigureConventions(configurationBuilder); @@ -762,7 +770,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, AuditPropertySetter?.IncrementEntityVersionProperty(entry.Entity); } - protected virtual void ConfigureBaseProperties(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) + protected virtual void ConfigureBaseProperties( + ModelBuilder modelBuilder, + IMutableEntityType mutableEntityType) where TEntity : class { if (mutableEntityType.IsOwned()) @@ -775,54 +785,82 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, return; } - modelBuilder.Entity().ConfigureByConvention(); + var entityTypeBuilder = CreateEntityTypeBuilderFromMutableEntityType( + modelBuilder, + mutableEntityType + ); + + entityTypeBuilder.ConfigureByConvention(); + + ConfigureGlobalFilters(modelBuilder, mutableEntityType, entityTypeBuilder); + } - ConfigureGlobalFilters(modelBuilder, mutableEntityType); + protected virtual EntityTypeBuilder CreateEntityTypeBuilderFromMutableEntityType( + ModelBuilder modelBuilder, + IMutableEntityType mutableEntityType) where TEntity : class + { + return mutableEntityType.HasSharedClrType + ? modelBuilder.SharedTypeEntity(mutableEntityType.Name) + : modelBuilder.Entity(); } - protected virtual void ConfigureGlobalFilters(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) + protected virtual void ConfigureGlobalFilters( + ModelBuilder modelBuilder, + IMutableEntityType mutableEntityType, + EntityTypeBuilder entityTypeBuilder) where TEntity : class { if (mutableEntityType.BaseType == null && ShouldFilterEntity(mutableEntityType)) { - var filterExpression = CreateFilterExpression(modelBuilder); + var filterExpression = CreateFilterExpression(modelBuilder, entityTypeBuilder); if (filterExpression != null) { - modelBuilder.Entity().HasAbpQueryFilter(filterExpression); + entityTypeBuilder.HasAbpQueryFilter(filterExpression); } } } - protected virtual void ConfigureValueConverter(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) + protected virtual void ConfigureValueConverter( + ModelBuilder modelBuilder, + IMutableEntityType mutableEntityType) where TEntity : class { - if (mutableEntityType.BaseType == null && - !typeof(TEntity).IsDefined(typeof(DisableDateTimeNormalizationAttribute), true) && - !typeof(TEntity).IsDefined(typeof(OwnedAttribute), true) && - !mutableEntityType.IsOwned()) + if (mutableEntityType.BaseType != null || + typeof(TEntity).IsDefined(typeof(DisableDateTimeNormalizationAttribute), true) || + typeof(TEntity).IsDefined(typeof(OwnedAttribute), true) || + mutableEntityType.IsOwned()) { - if (LazyServiceProvider == null || Clock == null) - { - return; - } + return; + } - foreach (var property in mutableEntityType.GetProperties(). - Where(property => property.PropertyInfo != null && - (property.PropertyInfo.PropertyType == typeof(DateTime) || property.PropertyInfo.PropertyType == typeof(DateTime?)) && - property.PropertyInfo.CanWrite && - ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(property.PropertyInfo) == null)) - { - modelBuilder - .Entity() - .Property(property.Name) - .HasConversion(property.ClrType == typeof(DateTime) - ? new AbpDateTimeValueConverter(Clock) - : new AbpNullableDateTimeValueConverter(Clock)); - } + if (LazyServiceProvider == null || Clock == null) + { + return; + } + + + foreach (var property in mutableEntityType.GetProperties(). + Where(property => property.PropertyInfo != null && + (property.PropertyInfo.PropertyType == typeof(DateTime) || property.PropertyInfo.PropertyType == typeof(DateTime?)) && + property.PropertyInfo.CanWrite && + ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(property.PropertyInfo) == null)) + { + var entityTypeBuilder = CreateEntityTypeBuilderFromMutableEntityType( + modelBuilder, + mutableEntityType + ); + + entityTypeBuilder + .Property(property.Name) + .HasConversion(property.ClrType == typeof(DateTime) + ? new AbpDateTimeValueConverter(Clock) + : new AbpNullableDateTimeValueConverter(Clock)); } } - protected virtual void ConfigureValueGenerated(ModelBuilder modelBuilder, IMutableEntityType mutableEntityType) + protected virtual void ConfigureValueGenerated( + ModelBuilder modelBuilder, + IMutableEntityType mutableEntityType) where TEntity : class { if (!typeof(IEntity).IsAssignableFrom(typeof(TEntity))) @@ -830,7 +868,8 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, return; } - var idPropertyBuilder = modelBuilder.Entity().Property(x => ((IEntity)x).Id); + var entityTypeBuilder = CreateEntityTypeBuilderFromMutableEntityType(modelBuilder, mutableEntityType); + var idPropertyBuilder = entityTypeBuilder.Property(x => ((IEntity)x).Id); if (idPropertyBuilder.Metadata.PropertyInfo!.IsDefined(typeof(DatabaseGeneratedAttribute), true)) { return; @@ -854,25 +893,30 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, return false; } - protected virtual Expression>? CreateFilterExpression(ModelBuilder modelBuilder) + protected virtual Expression>? CreateFilterExpression( + ModelBuilder modelBuilder, + EntityTypeBuilder entityTypeBuilder) where TEntity : class { Expression>? expression = null; if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity))) { - var softDeleteColumnName = modelBuilder.Entity().Metadata.FindProperty(nameof(ISoftDelete.IsDeleted))?.GetColumnName() ?? "IsDeleted"; + var softDeleteColumnName = entityTypeBuilder.Metadata.FindProperty(nameof(ISoftDelete.IsDeleted))?.GetColumnName() ?? "IsDeleted"; expression = e => !IsSoftDeleteFilterEnabled || !EF.Property(e, softDeleteColumnName); if (UseDbFunction()) { expression = e => AbpEfCoreDataFilterDbFunctionMethods.SoftDeleteFilter(((ISoftDelete)e).IsDeleted, true); - modelBuilder.ConfigureSoftDeleteDbFunction(AbpEfCoreDataFilterDbFunctionMethods.SoftDeleteFilterMethodInfo, this.GetService()); + modelBuilder.ConfigureSoftDeleteDbFunction( + AbpEfCoreDataFilterDbFunctionMethods.SoftDeleteFilterMethodInfo, + this.GetService() + ); } } if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity))) { - var multiTenantColumnName = modelBuilder.Entity().Metadata.FindProperty(nameof(IMultiTenant.TenantId))?.GetColumnName() ?? "TenantId"; + var multiTenantColumnName = entityTypeBuilder.Metadata.FindProperty(nameof(IMultiTenant.TenantId))?.GetColumnName() ?? "TenantId"; Expression> multiTenantFilter = e => !IsMultiTenantFilterEnabled || EF.Property(e, multiTenantColumnName) == CurrentTenantId; if (UseDbFunction()) { diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEfCoreConsts.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEfCoreConsts.cs new file mode 100644 index 0000000000..6d1f0c9dc6 --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpEfCoreConsts.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.EntityFrameworkCore; + +public class AbpEfCoreConsts +{ + public const string ProviderName = "Volo.Abp.EntityFrameworkCore"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs index 54ddfd0e5e..82fd3b5d2e 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/DbContextEventInbox.cs @@ -47,10 +47,12 @@ public class DbContextEventInbox : IDbContextEventInbox transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; } + var now = Clock.Now; var outgoingEventRecords = await dbContext .IncomingEvents .AsNoTracking() - .Where(x => !x.Processed) + .Where(x => x.Status == IncomingEventStatus.Pending) + .Where(x => x.NextRetryTime == null || x.NextRetryTime <= now) .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) @@ -66,7 +68,25 @@ public class DbContextEventInbox : IDbContextEventInbox { var dbContext = await DbContextProvider.GetDbContextAsync(); await dbContext.IncomingEvents.Where(x => x.Id == id).ExecuteUpdateAsync(x => - x.SetProperty(p => p.Processed, _ => true).SetProperty(p => p.ProcessedTime, _ => Clock.Now)); + x.SetProperty(p => p.Status, _ => IncomingEventStatus.Processed).SetProperty(p => p.HandledTime, _ => Clock.Now)); + } + + [UnitOfWork] + public virtual async Task RetryLaterAsync(Guid id, int retryCount, DateTime? nextRetryTime) + { + var dbContext = await DbContextProvider.GetDbContextAsync(); + await dbContext.IncomingEvents.Where(x => x.Id == id).ExecuteUpdateAsync(x => + x.SetProperty(p => p.RetryCount, _ => retryCount) + .SetProperty(p => p.NextRetryTime, _ => nextRetryTime) + .SetProperty(p => p.Status, _ => IncomingEventStatus.Pending)); + } + + [UnitOfWork] + public virtual async Task MarkAsDiscardAsync(Guid id) + { + var dbContext = await DbContextProvider.GetDbContextAsync(); + await dbContext.IncomingEvents.Where(x => x.Id == id).ExecuteUpdateAsync(x => + x.SetProperty(p => p.Status, _ => IncomingEventStatus.Discarded).SetProperty(p => p.HandledTime, _ => Clock.Now)); } [UnitOfWork] @@ -82,7 +102,7 @@ public class DbContextEventInbox : IDbContextEventInbox var dbContext = await DbContextProvider.GetDbContextAsync(); var timeToKeepEvents = Clock.Now - EventBusBoxesOptions.WaitTimeToDeleteProcessedInboxEvents; await dbContext.IncomingEvents - .Where(x => x.Processed && x.CreationTime < timeToKeepEvents) + .Where(x => (x.Status == IncomingEventStatus.Processed || x.Status == IncomingEventStatus.Discarded) && x.CreationTime < timeToKeepEvents) .ExecuteDeleteAsync(); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/EventInboxDbContextModelBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/EventInboxDbContextModelBuilderExtensions.cs index fbe91c2def..b7474c3b11 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/EventInboxDbContextModelBuilderExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/EventInboxDbContextModelBuilderExtensions.cs @@ -16,7 +16,7 @@ public static class EventInboxDbContextModelBuilderExtensions b.Property(x => x.EventName).IsRequired().HasMaxLength(IncomingEventRecord.MaxEventNameLength); b.Property(x => x.EventData).IsRequired(); - b.HasIndex(x => new { x.Processed, x.CreationTime }); + b.HasIndex(x => new { x.Status, x.CreationTime }); b.HasIndex(x => x.MessageId); }); } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs index be7da15890..3f06bf4ef9 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DistributedEvents/IncomingEventRecord.cs @@ -24,9 +24,13 @@ public class IncomingEventRecord : public DateTime CreationTime { get; private set; } - public bool Processed { get; set; } + public IncomingEventStatus Status { get; set; } = IncomingEventStatus.Pending; - public DateTime? ProcessedTime { get; set; } + public DateTime? HandledTime { get; set; } + + public int RetryCount { get; set; } = 0; + + public DateTime? NextRetryTime { get; set; } = null; protected IncomingEventRecord() { @@ -58,7 +62,11 @@ public class IncomingEventRecord : MessageId, EventName, EventData, - CreationTime + CreationTime, + Status, + HandledTime, + RetryCount, + NextRetryTime ); foreach (var property in ExtraProperties) @@ -71,7 +79,20 @@ public class IncomingEventRecord : public void MarkAsProcessed(DateTime processedTime) { - Processed = true; - ProcessedTime = processedTime; + Status = IncomingEventStatus.Processed; + HandledTime = processedTime; + } + + public void MarkAsDiscarded(DateTime discardedTime) + { + Status = IncomingEventStatus.Discarded; + HandledTime = discardedTime; + } + + public void RetryLater(int retryCount, DateTime nextRetryTime) + { + Status = IncomingEventStatus.Pending; + NextRetryTime = nextRetryTime; + RetryCount = retryCount; } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IEfCoreDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IEfCoreDbContext.cs index 833163d223..3097eba9cb 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IEfCoreDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IEfCoreDbContext.cs @@ -31,6 +31,9 @@ public interface IEfCoreDbContext : IDisposable, IInfrastructure Set() where T : class; + + DbSet Set(string name) + where T : class; DatabaseFacade Database { get; } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs index 5dd2b3a571..2e8557b27a 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/AbpEntityTypeBuilderExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -14,20 +15,69 @@ namespace Volo.Abp.EntityFrameworkCore.Modeling; public static class AbpEntityTypeBuilderExtensions { + public static List ConventionalConfigurers { get; } + + static AbpEntityTypeBuilderExtensions() + { + ConventionalConfigurers = new List + { + new NamedEntityConfigurer( + "ConcurrencyStamp", + b => b.TryConfigureConcurrencyStamp() + ), + new NamedEntityConfigurer( + "ExtraProperties", + b => b.TryConfigureExtraProperties() + ), + new NamedEntityConfigurer( + "ObjectExtensions", + b => b.TryConfigureObjectExtensions() + ), + new NamedEntityConfigurer( + "MayHaveCreator", + b => b.TryConfigureMayHaveCreator() + ), + new NamedEntityConfigurer( + "MustHaveCreator", + b => b.TryConfigureMustHaveCreator() + ), + new NamedEntityConfigurer( + "SoftDelete", + b => b.TryConfigureSoftDelete() + ), + new NamedEntityConfigurer( + "DeletionTime", + b => b.TryConfigureDeletionTime() + ), + new NamedEntityConfigurer( + "DeletionAudited", + b => b.TryConfigureDeletionAudited() + ), + new NamedEntityConfigurer( + "CreationTime", + b => b.TryConfigureCreationTime() + ), + new NamedEntityConfigurer( + "LastModificationTime", + b => b.TryConfigureLastModificationTime() + ), + new NamedEntityConfigurer( + "ModificationAudited", + b => b.TryConfigureModificationAudited() + ), + new NamedEntityConfigurer( + "MultiTenant", + b => b.TryConfigureMultiTenant() + ) + }; + } + public static void ConfigureByConvention(this EntityTypeBuilder b) { - b.TryConfigureConcurrencyStamp(); - b.TryConfigureExtraProperties(); - b.TryConfigureObjectExtensions(); - b.TryConfigureMayHaveCreator(); - b.TryConfigureMustHaveCreator(); - b.TryConfigureSoftDelete(); - b.TryConfigureDeletionTime(); - b.TryConfigureDeletionAudited(); - b.TryConfigureCreationTime(); - b.TryConfigureLastModificationTime(); - b.TryConfigureModificationAudited(); - b.TryConfigureMultiTenant(); + foreach (var configurer in ConventionalConfigurers) + { + configurer.ConfigureAction(b); + } } public static void ConfigureConcurrencyStamp(this EntityTypeBuilder b) @@ -308,6 +358,4 @@ public static class AbpEntityTypeBuilderExtensions b.As().TryConfigureExtraProperties(); b.As().TryConfigureConcurrencyStamp(); } - - //TODO: Add other interfaces (IAuditedObject...) -} +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/NamedEntityConfigurer.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/NamedEntityConfigurer.cs new file mode 100644 index 0000000000..055a54cf6a --- /dev/null +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/Modeling/NamedEntityConfigurer.cs @@ -0,0 +1,23 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Volo.Abp.EntityFrameworkCore.Modeling; + +public class NamedEntityConfigurer +{ + /// + /// Name of the configurer. + /// + public string Name { get; set; } + + /// + /// Action to configure the given . + /// + public Action ConfigureAction { get; } + + public NamedEntityConfigurer(string name, Action configureAction) + { + Name = Check.NotNullOrEmpty(name, nameof(name)); + ConfigureAction = Check.NotNull(configureAction, nameof(configureAction)); + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs index c154330495..66b605d576 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IEventInbox.cs @@ -14,6 +14,10 @@ public interface IEventInbox Task MarkAsProcessedAsync(Guid id); + Task RetryLaterAsync(Guid id, int retryCount, DateTime? nextRetryTime); + + Task MarkAsDiscardAsync(Guid id); + Task ExistsByMessageIdAsync(string messageId); Task DeleteOldEventsAsync(); diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs index 1be24a3d02..23e59ad61a 100644 --- a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventInfo.cs @@ -20,6 +20,14 @@ public class IncomingEventInfo : IIncomingEventInfo public DateTime CreationTime { get; } + public IncomingEventStatus Status { get; set; } = IncomingEventStatus.Pending; + + public DateTime? HandledTime { get; set; } + + public int RetryCount { get; set; } = 0; + + public DateTime? NextRetryTime { get; set; } = null; + protected IncomingEventInfo() { ExtraProperties = new ExtraPropertyDictionary(); @@ -31,13 +39,21 @@ public class IncomingEventInfo : IIncomingEventInfo string messageId, string eventName, byte[] eventData, - DateTime creationTime) + DateTime creationTime, + IncomingEventStatus status = IncomingEventStatus.Pending, + DateTime? handledTime = null, + int retryCount = 0, + DateTime? nextRetryTime = null) { Id = id; MessageId = messageId; EventName = Check.NotNullOrWhiteSpace(eventName, nameof(eventName), MaxEventNameLength); EventData = eventData; CreationTime = creationTime; + Status = status; + HandledTime = handledTime; + RetryCount = retryCount; + NextRetryTime = nextRetryTime; ExtraProperties = new ExtraPropertyDictionary(); this.SetDefaultsForExtraProperties(); } diff --git a/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventStatus.cs b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventStatus.cs new file mode 100644 index 0000000000..27a210770c --- /dev/null +++ b/framework/src/Volo.Abp.EventBus.Abstractions/Volo/Abp/EventBus/Distributed/IncomingEventStatus.cs @@ -0,0 +1,10 @@ +namespace Volo.Abp.EventBus.Distributed; + +public enum IncomingEventStatus +{ + Pending = 0, + + Discarded = 1, + + Processed = 2 +} diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs index cc91cad5df..6febf2f065 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/AbpEventBusBoxesOptions.cs @@ -36,6 +36,23 @@ public class AbpEventBusBoxesOptions /// public TimeSpan PeriodTimeSpan { get; set; } + /// + /// Default: + /// + public InboxProcessorFailurePolicy InboxProcessorFailurePolicy { get; set; } = InboxProcessorFailurePolicy.Retry; + + /// + /// Default: 10 + /// + public int InboxProcessorMaxRetryCount { get; set; } = 10; + + /// + /// Default value is 10 + /// The initial retry delay factor (double) when `InboxProcessorFailurePolicy` is `RetryLater`. + /// The delay is calculated as: `delay = InboxProcessorRetryBackoffFactor × 2^retryCount` + /// + public double InboxProcessorRetryBackoffFactor { get; set; } = 10; + /// /// Default: 15 seconds /// diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs index 06014701a2..b00ba81242 100644 --- a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -25,7 +26,6 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency protected IEventInbox Inbox { get; private set; } = default!; protected InboxConfig InboxConfig { get; private set; } = default!; protected AbpEventBusBoxesOptions EventBusBoxesOptions { get; } - protected DateTime? LastCleanTime { get; set; } protected string DistributedLockName { get; set; } = default!; @@ -103,18 +103,76 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency foreach (var waitingEvent in waitingEvents) { - using (var uow = UnitOfWorkManager.Begin(isTransactional: true, requiresNew: true)) + Logger.LogInformation($"Start processing the incoming event with id = {waitingEvent.Id:N}"); + + try { - await DistributedEventBus - .AsSupportsEventBoxes() - .ProcessFromInboxAsync(waitingEvent, InboxConfig); + using (var uow = UnitOfWorkManager.Begin(isTransactional: true, requiresNew: true)) + { + await DistributedEventBus + .AsSupportsEventBoxes() + .ProcessFromInboxAsync(waitingEvent, InboxConfig); - await Inbox.MarkAsProcessedAsync(waitingEvent.Id); + await Inbox.MarkAsProcessedAsync(waitingEvent.Id); - await uow.CompleteAsync(StoppingToken); - } + await uow.CompleteAsync(StoppingToken); + } - Logger.LogInformation($"Processed the incoming event with id = {waitingEvent.Id:N}"); + Logger.LogInformation($"Processed the incoming event with id = {waitingEvent.Id:N}"); + } + catch (Exception e) + { + Logger.LogError(e, $"Event with id = {waitingEvent.Id:N} processing failed."); + + if (EventBusBoxesOptions.InboxProcessorFailurePolicy == InboxProcessorFailurePolicy.Retry) + { + throw; + } + + if (EventBusBoxesOptions.InboxProcessorFailurePolicy == InboxProcessorFailurePolicy.RetryLater) + { + using (var uow = UnitOfWorkManager.Begin(isTransactional: true, requiresNew: true)) + { + if (waitingEvent.NextRetryTime != null) + { + waitingEvent.RetryCount++; + } + + if (waitingEvent.RetryCount >= EventBusBoxesOptions.InboxProcessorMaxRetryCount) + { + Logger.LogWarning($"Event with id = {waitingEvent.Id:N} has exceeded the maximum retry count. Marking it as discarded."); + + await Inbox.RetryLaterAsync(waitingEvent.Id, waitingEvent.RetryCount, null); + await Inbox.MarkAsDiscardAsync(waitingEvent.Id); + await uow.CompleteAsync(StoppingToken); + continue; + } + + waitingEvent.NextRetryTime = GetNextRetryTime(waitingEvent.RetryCount, EventBusBoxesOptions.InboxProcessorRetryBackoffFactor); + + Logger.LogInformation($"Event with id = {waitingEvent.Id:N} will retry later. " + + $"Current retry count: {waitingEvent.RetryCount}, " + + $"Next retry time: {waitingEvent.NextRetryTime}, " + + $"Max retry count: {EventBusBoxesOptions.InboxProcessorMaxRetryCount}."); + + await Inbox.RetryLaterAsync(waitingEvent.Id, waitingEvent.RetryCount, GetNextRetryTime(waitingEvent.RetryCount, EventBusBoxesOptions.InboxProcessorRetryBackoffFactor)); + await uow.CompleteAsync(StoppingToken); + } + continue; + } + + if (EventBusBoxesOptions.InboxProcessorFailurePolicy == InboxProcessorFailurePolicy.Discard) + { + using (var uow = UnitOfWorkManager.Begin(isTransactional: true, requiresNew: true)) + { + Logger.LogInformation($"Event with id = {waitingEvent.Id:N} will be discarded."); + + await Inbox.MarkAsDiscardAsync(waitingEvent.Id); + await uow.CompleteAsync(StoppingToken); + } + continue; + } + } } } } @@ -130,6 +188,12 @@ public class InboxProcessor : IInboxProcessor, ITransientDependency } } + protected virtual DateTime? GetNextRetryTime(int retryCount, double factor) + { + var delaySeconds = factor * Math.Pow(2, retryCount); + return DateTime.Now.AddSeconds(delaySeconds); + } + protected virtual async Task> GetWaitingEventsAsync() { return await Inbox.GetWaitingEventsAsync(EventBusBoxesOptions.InboxWaitingEventMaxCount, EventBusBoxesOptions.InboxProcessorFilter, StoppingToken); diff --git a/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessorFailurePolicy.cs b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessorFailurePolicy.cs new file mode 100644 index 0000000000..31095b62a3 --- /dev/null +++ b/framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/InboxProcessorFailurePolicy.cs @@ -0,0 +1,22 @@ +namespace Volo.Abp.EventBus.Distributed; + +public enum InboxProcessorFailurePolicy +{ + /// + /// Default behavior, retry the following event in next period time. + /// + Retry, + + /// + /// Skip the failed event and retry it after a delay. + /// The delay doubles with each retry, starting from the configured InboxProcessorRetryBackoffFactor + /// (e.g., 10, 20, 40, 80 seconds, etc.). + /// The event is discarded if it still fails after reaching the maximum retry count. + /// + RetryLater, + + /// + /// Skip the event and do not retry it. + /// + Discard, +} diff --git a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Domain/Entities/EntityNotFoundException.cs b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Domain/Entities/EntityNotFoundException.cs index 5f524f9578..ed10bf64c6 100644 --- a/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Domain/Entities/EntityNotFoundException.cs +++ b/framework/src/Volo.Abp.ExceptionHandling/Volo/Abp/Domain/Entities/EntityNotFoundException.cs @@ -2,6 +2,34 @@ namespace Volo.Abp.Domain.Entities; +/// +/// This exception is thrown if an entity is expected to be found but not found. +/// +public class EntityNotFoundException : EntityNotFoundException +{ + /// + /// Creates a new object. + /// + public EntityNotFoundException() + : base(typeof(TEntityType)) + { + } + /// + /// Creates a new object. + /// + public EntityNotFoundException(object? id) + : base(typeof(TEntityType), id) + { + } + /// + /// Creates a new object. + /// + public EntityNotFoundException(object? id, Exception? innerException) + : base(typeof(TEntityType), id, innerException) + { + } +} + /// /// This exception is thrown if an entity is expected to be found but not found. /// diff --git a/framework/src/Volo.Abp.HangFire/Volo/Abp/Hangfire/AbpHangfireAuthorizationFilter.cs b/framework/src/Volo.Abp.HangFire/Volo/Abp/Hangfire/AbpHangfireAuthorizationFilter.cs index bd65b22dba..d49c211ee5 100644 --- a/framework/src/Volo.Abp.HangFire/Volo/Abp/Hangfire/AbpHangfireAuthorizationFilter.cs +++ b/framework/src/Volo.Abp.HangFire/Volo/Abp/Hangfire/AbpHangfireAuthorizationFilter.cs @@ -1,53 +1,49 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Hangfire.Dashboard; +using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.Authorization.Permissions; -using Volo.Abp.Users; +using Volo.Abp.Authorization; +using Volo.Abp.MultiTenancy; namespace Volo.Abp.Hangfire; public class AbpHangfireAuthorizationFilter : IDashboardAsyncAuthorizationFilter { private readonly bool _enableTenant; - private readonly string? _requiredPermissionName; + private readonly AuthorizationPolicyBuilder _policyBuilder; - public AbpHangfireAuthorizationFilter(bool enableTenant = false, string? requiredPermissionName = null) - { - _enableTenant = requiredPermissionName.IsNullOrWhiteSpace() ? enableTenant : true; - _requiredPermissionName = requiredPermissionName; - } + public virtual AuthorizationPolicyBuilder PolicyBuilder => _policyBuilder; - public async Task AuthorizeAsync(DashboardContext context) + public AbpHangfireAuthorizationFilter(bool enableTenant = false, string? requiredPermissionName = null, params string[]? requiredRoleNames) { - if (!IsLoggedIn(context, _enableTenant)) + _enableTenant = enableTenant; + _policyBuilder = new AuthorizationPolicyBuilder().RequireAuthenticatedUser(); + if (!requiredPermissionName.IsNullOrWhiteSpace()) { - return false; + _policyBuilder.Requirements.Add(new PermissionRequirement(requiredPermissionName)); } - if (_requiredPermissionName.IsNullOrEmpty()) + if (!requiredRoleNames.IsNullOrEmpty()) { - return true; + foreach (var roleName in requiredRoleNames!) + { + _policyBuilder.RequireRole(roleName); + } } - - return await IsPermissionGrantedAsync(context, _requiredPermissionName!); } - private static bool IsLoggedIn(DashboardContext context, bool enableTenant) + public virtual async Task AuthorizeAsync(DashboardContext context) { - var currentUser = context.GetHttpContext().RequestServices.GetRequiredService(); - - if (!enableTenant) + var currentTenant = context.GetHttpContext().RequestServices.GetRequiredService(); + if (currentTenant.IsAvailable && !_enableTenant) { - return currentUser.IsAuthenticated && !currentUser.TenantId.HasValue; + return false; } - return currentUser.IsAuthenticated; - } - - private static async Task IsPermissionGrantedAsync(DashboardContext context, string requiredPermissionName) - { - var permissionChecker = context.GetHttpContext().RequestServices.GetRequiredService(); - return await permissionChecker.IsGrantedAsync(requiredPermissionName); + var authorizationService = context.GetHttpContext().RequestServices.GetRequiredService(); + var authorizationPolicy = _policyBuilder.Build(); + return (await authorizationService.AuthorizeAsync(context.GetHttpContext().User, authorizationPolicy)).Succeeded; } } diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor/Volo/Abp/Http/Client/IdentityModel/MauiBlazor/AbpHttpClientIdentityModelMauiBlazorModule.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor/Volo/Abp/Http/Client/IdentityModel/MauiBlazor/AbpHttpClientIdentityModelMauiBlazorModule.cs index f5df83e9d0..72dc3c1d68 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor/Volo/Abp/Http/Client/IdentityModel/MauiBlazor/AbpHttpClientIdentityModelMauiBlazorModule.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor/Volo/Abp/Http/Client/IdentityModel/MauiBlazor/AbpHttpClientIdentityModelMauiBlazorModule.cs @@ -1,4 +1,4 @@ -using IdentityModel; +using Duende.IdentityModel; using Volo.Abp.AspNetCore.Components.MauiBlazor; using Volo.Abp.Modularity; using Volo.Abp.Security.Claims; diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor/Volo/Abp/Http/Client/IdentityModel/MauiBlazor/MauiIBlazorIdentityModelRemoteServiceHttpClientAuthenticator.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor/Volo/Abp/Http/Client/IdentityModel/MauiBlazor/MauiIBlazorIdentityModelRemoteServiceHttpClientAuthenticator.cs index b1277373fa..8598009aac 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor/Volo/Abp/Http/Client/IdentityModel/MauiBlazor/MauiIBlazorIdentityModelRemoteServiceHttpClientAuthenticator.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.MauiBlazor/Volo/Abp/Http/Client/IdentityModel/MauiBlazor/MauiIBlazorIdentityModelRemoteServiceHttpClientAuthenticator.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Authentication; using Volo.Abp.IdentityModel; diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider.cs index 5044513438..b993b006e3 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextAbpAccessTokenProvider.cs @@ -1,8 +1,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Authentication; +using Volo.Abp.Users; namespace Volo.Abp.Http.Client.IdentityModel.Web; @@ -24,6 +26,11 @@ public class HttpContextAbpAccessTokenProvider : IAbpAccessTokenProvider, ITrans return null; } + if (!httpContext.RequestServices.GetRequiredService().IsAuthenticated) + { + return null; + } + return await httpContext.GetTokenAsync("access_token"); } } diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextIdentityModelRemoteServiceHttpClientAuthenticator.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextIdentityModelRemoteServiceHttpClientAuthenticator.cs index 364134348b..b79851f1df 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextIdentityModelRemoteServiceHttpClientAuthenticator.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo/Abp/Http/Client/IdentityModel/Web/HttpContextIdentityModelRemoteServiceHttpClientAuthenticator.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Authentication; using Volo.Abp.IdentityModel; diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AbpHttpClientIdentityModelWebAssemblyModule.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AbpHttpClientIdentityModelWebAssemblyModule.cs index 446011311b..4b59f2c661 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AbpHttpClientIdentityModelWebAssemblyModule.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AbpHttpClientIdentityModelWebAssemblyModule.cs @@ -1,4 +1,4 @@ -using IdentityModel; +using Duende.IdentityModel; using Volo.Abp.AspNetCore.Components.WebAssembly; using Volo.Abp.Modularity; using Volo.Abp.Security.Claims; diff --git a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AccessTokenProviderIdentityModelRemoteServiceHttpClientAuthenticator.cs b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AccessTokenProviderIdentityModelRemoteServiceHttpClientAuthenticator.cs index c16317e376..11fa61e05a 100644 --- a/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AccessTokenProviderIdentityModelRemoteServiceHttpClientAuthenticator.cs +++ b/framework/src/Volo.Abp.Http.Client.IdentityModel.WebAssembly/Volo/Abp/Http/Client/IdentityModel/WebAssembly/AccessTokenProviderIdentityModelRemoteServiceHttpClientAuthenticator.cs @@ -1,5 +1,5 @@ using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Client.Authentication; using Volo.Abp.IdentityModel; diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs index d09f97b24c..390adfac9c 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -17,6 +18,7 @@ using Volo.Abp.Http.Client.Proxying; using Volo.Abp.Http.Modeling; using Volo.Abp.Http.ProxyScripting.Generators; using Volo.Abp.Json; +using Volo.Abp.Json.SystemTextJson; using Volo.Abp.MultiTenancy; using Volo.Abp.Reflection; using Volo.Abp.Threading; @@ -44,6 +46,7 @@ public class ClientProxyBase : ITransientDependency protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService(); protected ICurrentApiVersionInfo CurrentApiVersionInfo => LazyServiceProvider.LazyGetRequiredService(); protected ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService(); + protected IOptions? SystemTextJsonSerializerOptions => LazyServiceProvider.LazyGetService>(); protected virtual async Task RequestAsync(string methodName, ClientProxyRequestTypeValue? arguments = null) { @@ -55,6 +58,21 @@ public class ClientProxyBase : ITransientDependency return await RequestAsync(BuildHttpProxyClientProxyContext(methodName, arguments)); } + protected virtual async IAsyncEnumerable RequestAsyncEnumerable(string methodName, ClientProxyRequestTypeValue? arguments = null) + { + var requestContext = BuildHttpProxyClientProxyContext(methodName, arguments); + var responseContent = await RequestAsync(requestContext); + var options = SystemTextJsonSerializerOptions?.Value.JsonSerializerOptions; + var stream = await responseContent.ReadAsStreamAsync(); + var items = options != null + ? System.Text.Json.JsonSerializer.DeserializeAsyncEnumerable(stream, options) + : System.Text.Json.JsonSerializer.DeserializeAsyncEnumerable(stream); + await foreach (var item in items) + { + yield return item!; + } + } + protected virtual ClientProxyRequestContext BuildHttpProxyClientProxyContext(string methodName, ClientProxyRequestTypeValue? arguments = null) { if (arguments == null) diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs index 09ec183929..69248680cc 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyUrlBuilder.cs @@ -224,7 +224,7 @@ public class ClientProxyUrlBuilder : ITransientDependency return true; } - protected virtual Task ConvertValueToStringAsync(object value) + protected virtual Task ConvertValueToStringAsync(object? value) { if (value is DateTime dateTimeValue) { @@ -236,6 +236,6 @@ public class ClientProxyUrlBuilder : ITransientDependency return Task.FromResult(dateTimeValue.ToString("yyyy-MM-ddTHH:mm:ss.fffffff").TrimEnd('0').TrimEnd('.')); } - return Task.FromResult(value.ToString()!); + return Task.FromResult(value?.ToString() ?? string.Empty); } } diff --git a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj index 8915614bfe..7f318d7215 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj +++ b/framework/src/Volo.Abp.IdentityModel/Volo.Abp.IdentityModel.csproj @@ -17,7 +17,7 @@ - + diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityClientConfiguration.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityClientConfiguration.cs index 011e40b4b3..63da858d06 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityClientConfiguration.cs +++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityClientConfiguration.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; -using IdentityModel; +using Duende.IdentityModel; namespace Volo.Abp.IdentityModel; diff --git a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs index 2f0fabdd80..396f2e8989 100644 --- a/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs +++ b/framework/src/Volo.Abp.IdentityModel/Volo/Abp/IdentityModel/IdentityModelAuthenticationService.cs @@ -1,5 +1,5 @@ -using IdentityModel; -using IdentityModel.Client; +using Duende.IdentityModel; +using Duende.IdentityModel.Client; using JetBrains.Annotations; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; diff --git a/framework/src/Volo.Abp.Imaging.Abstractions/Volo/Abp/Imaging/ImageCompressor.cs b/framework/src/Volo.Abp.Imaging.Abstractions/Volo/Abp/Imaging/ImageCompressor.cs index fb60e56a59..1d0e649d28 100644 --- a/framework/src/Volo.Abp.Imaging.Abstractions/Volo/Abp/Imaging/ImageCompressor.cs +++ b/framework/src/Volo.Abp.Imaging.Abstractions/Volo/Abp/Imaging/ImageCompressor.cs @@ -12,9 +12,9 @@ namespace Volo.Abp.Imaging; public class ImageCompressor : IImageCompressor, ITransientDependency { protected IEnumerable ImageCompressorContributors { get; } - + protected ICancellationTokenProvider CancellationTokenProvider { get; } - + public ImageCompressor(IEnumerable imageCompressorContributors, ICancellationTokenProvider cancellationTokenProvider) { ImageCompressorContributors = imageCompressorContributors.Reverse(); @@ -27,12 +27,12 @@ public class ImageCompressor : IImageCompressor, ITransientDependency CancellationToken cancellationToken = default) { Check.NotNull(stream, nameof(stream)); - + if(!stream.CanRead) { return new ImageCompressResult(stream, ImageProcessState.Unsupported); } - + if(!stream.CanSeek) { var memoryStream = new MemoryStream(); @@ -41,12 +41,12 @@ public class ImageCompressor : IImageCompressor, ITransientDependency stream = memoryStream; } - foreach (var imageCompressorContributor in ImageCompressorContributors) + foreach (var imageCompressorContributor in ImageCompressorContributors.Reverse()) { var result = await imageCompressorContributor.TryCompressAsync(stream, mimeType, CancellationTokenProvider.FallbackToProvider(cancellationToken)); - + SeekToBegin(stream); - + if (result.State == ImageProcessState.Unsupported) { continue; @@ -54,7 +54,7 @@ public class ImageCompressor : IImageCompressor, ITransientDependency return result; } - + return new ImageCompressResult(stream, ImageProcessState.Unsupported); } @@ -64,22 +64,22 @@ public class ImageCompressor : IImageCompressor, ITransientDependency CancellationToken cancellationToken = default) { Check.NotNull(bytes, nameof(bytes)); - - foreach (var imageCompressorContributor in ImageCompressorContributors) + + foreach (var imageCompressorContributor in ImageCompressorContributors.Reverse()) { var result = await imageCompressorContributor.TryCompressAsync(bytes, mimeType, CancellationTokenProvider.FallbackToProvider(cancellationToken)); - + if (result.State == ImageProcessState.Unsupported) { continue; } - + return result; } - + return new ImageCompressResult(bytes, ImageProcessState.Unsupported); } - + protected virtual void SeekToBegin(Stream stream) { if (stream.CanSeek) @@ -87,4 +87,4 @@ public class ImageCompressor : IImageCompressor, ITransientDependency stream.Seek(0, SeekOrigin.Begin); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Imaging.Abstractions/Volo/Abp/Imaging/ImageResizer.cs b/framework/src/Volo.Abp.Imaging.Abstractions/Volo/Abp/Imaging/ImageResizer.cs index 80d9ec8815..96bf69b2aa 100644 --- a/framework/src/Volo.Abp.Imaging.Abstractions/Volo/Abp/Imaging/ImageResizer.cs +++ b/framework/src/Volo.Abp.Imaging.Abstractions/Volo/Abp/Imaging/ImageResizer.cs @@ -13,36 +13,36 @@ namespace Volo.Abp.Imaging; public class ImageResizer : IImageResizer, ITransientDependency { protected IEnumerable ImageResizerContributors { get; } - + protected ImageResizeOptions ImageResizeOptions { get; } - + protected ICancellationTokenProvider CancellationTokenProvider { get; } - + public ImageResizer( - IEnumerable imageResizerContributors, - IOptions imageResizeOptions, + IEnumerable imageResizerContributors, + IOptions imageResizeOptions, ICancellationTokenProvider cancellationTokenProvider) { ImageResizerContributors = imageResizerContributors.Reverse(); CancellationTokenProvider = cancellationTokenProvider; ImageResizeOptions = imageResizeOptions.Value; } - + public virtual async Task> ResizeAsync( - [NotNull] Stream stream, - ImageResizeArgs resizeArgs, - string? mimeType = null, + [NotNull] Stream stream, + ImageResizeArgs resizeArgs, + string? mimeType = null, CancellationToken cancellationToken = default) { Check.NotNull(stream, nameof(stream)); - + ChangeDefaultResizeMode(resizeArgs); - + if(!stream.CanRead) { return new ImageResizeResult(stream, ImageProcessState.Unsupported); } - + if(!stream.CanSeek) { var memoryStream = new MemoryStream(); @@ -50,13 +50,13 @@ public class ImageResizer : IImageResizer, ITransientDependency SeekToBegin(memoryStream); stream = memoryStream; } - - foreach (var imageResizerContributor in ImageResizerContributors) + + foreach (var imageResizerContributor in ImageResizerContributors.Reverse()) { var result = await imageResizerContributor.TryResizeAsync(stream, resizeArgs, mimeType, CancellationTokenProvider.FallbackToProvider(cancellationToken)); SeekToBegin(stream); - + if (result.State == ImageProcessState.Unsupported) { continue; @@ -64,24 +64,24 @@ public class ImageResizer : IImageResizer, ITransientDependency return result; } - + return new ImageResizeResult(stream, ImageProcessState.Unsupported); } public virtual async Task> ResizeAsync( - [NotNull] byte[] bytes, - ImageResizeArgs resizeArgs, - string? mimeType = null, + [NotNull] byte[] bytes, + ImageResizeArgs resizeArgs, + string? mimeType = null, CancellationToken cancellationToken = default) { Check.NotNull(bytes, nameof(bytes)); - + ChangeDefaultResizeMode(resizeArgs); - - foreach (var imageResizerContributor in ImageResizerContributors) + + foreach (var imageResizerContributor in ImageResizerContributors.Reverse()) { var result = await imageResizerContributor.TryResizeAsync(bytes, resizeArgs, mimeType, CancellationTokenProvider.FallbackToProvider(cancellationToken)); - + if (result.State == ImageProcessState.Unsupported) { continue; @@ -89,10 +89,10 @@ public class ImageResizer : IImageResizer, ITransientDependency return result; } - + return new ImageResizeResult(bytes, ImageProcessState.Unsupported); } - + protected virtual void ChangeDefaultResizeMode(ImageResizeArgs resizeArgs) { if (resizeArgs.Mode == ImageResizeMode.Default) @@ -100,7 +100,7 @@ public class ImageResizer : IImageResizer, ITransientDependency resizeArgs.Mode = ImageResizeOptions.DefaultResizeMode; } } - + protected virtual void SeekToBegin(Stream stream) { if (stream.CanSeek) @@ -108,4 +108,4 @@ public class ImageResizer : IImageResizer, ITransientDependency stream.Seek(0, SeekOrigin.Begin); } } -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/ObjectToInferredTypesConverter.cs b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/ObjectToInferredTypesConverter.cs index bf017d041e..85c47c738d 100644 --- a/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/ObjectToInferredTypesConverter.cs +++ b/framework/src/Volo.Abp.Json.SystemTextJson/Volo/Abp/Json/SystemTextJson/JsonConverters/ObjectToInferredTypesConverter.cs @@ -26,6 +26,16 @@ public class ObjectToInferredTypesConverter : JsonConverter public override void Write( Utf8JsonWriter writer, object objectToWrite, - JsonSerializerOptions options) => - JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options); + JsonSerializerOptions options) + { + var runtimeType = objectToWrite.GetType(); + if (runtimeType == typeof(object)) + { + writer.WriteStartObject(); + writer.WriteEndObject(); + return; + } + + JsonSerializer.Serialize(writer, objectToWrite, runtimeType, options); + } } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs index e4b66148a5..f065f0e695 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.Json; using Microsoft.Extensions.Localization; @@ -47,12 +48,11 @@ public static class JsonLocalizationDictionaryBuilder { throw new AbpException("Can not parse json string. " + ex.Message); } - if (jsonFile == null) { return null; } - + var cultureCode = jsonFile.Culture; if (string.IsNullOrEmpty(cultureCode)) { @@ -61,18 +61,17 @@ public static class JsonLocalizationDictionaryBuilder var dictionary = new Dictionary(); var dublicateNames = new List(); - foreach (var item in jsonFile.Texts) + + foreach (var item in FlattenTexts(jsonFile.Texts)) { if (string.IsNullOrEmpty(item.Key)) { throw new AbpException("The key is empty in given json string."); } - if (dictionary.GetOrDefault(item.Key) != null) { dublicateNames.Add(item.Key); } - dictionary[item.Key] = new LocalizedString(item.Key, item.Value.NormalizeLineEndings()); } @@ -85,4 +84,67 @@ public static class JsonLocalizationDictionaryBuilder return new StaticLocalizationDictionary(cultureCode, dictionary); } + + private static Dictionary FlattenTexts(Dictionary texts, string prefix = "") + { + var result = new Dictionary(); + foreach (var text in texts) + { + var currentKey = string.IsNullOrEmpty(prefix) ? text.Key : $"{prefix}__{text.Key}"; + switch (text.Value) + { + case JsonElement jsonElement: + foreach (var item in FlattenJsonElement(jsonElement, currentKey)) + { + result[item.Key] = item.Value; + } + break; + case string str: + result[currentKey] = str; + break; + case null: + result[currentKey] = ""; + break; + default: + result[currentKey] = text.Value.ToString() ?? ""; + break; + } + } + return result; + } + + private static IEnumerable> FlattenJsonElement(JsonElement element, string prefix) + { + switch (element.ValueKind) + { + case JsonValueKind.String: + yield return new KeyValuePair(prefix, element.GetString() ?? ""); + break; + case JsonValueKind.Object: + foreach (var prop in element.EnumerateObject()) + { + var newKey = $"{prefix}__{prop.Name}"; + foreach (var item in FlattenJsonElement(prop.Value, newKey)) + { + yield return item; + } + } + break; + case JsonValueKind.Array: + var i = 0; + foreach (var prop in element.EnumerateArray()) + { + var newKey = $"{prefix}__{i}"; + foreach (var item in FlattenJsonElement(prop, newKey)) + { + yield return item; + } + i++; + } + break; + default: + yield return new KeyValuePair(prefix, element.ToString()); + break; + } + } } diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationFile.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationFile.cs index 8470e2696f..337a03a31e 100644 --- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationFile.cs +++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationFile.cs @@ -9,10 +9,5 @@ public class JsonLocalizationFile /// public string Culture { get; set; } = default!; - public Dictionary Texts { get; set; } - - public JsonLocalizationFile() - { - Texts = new Dictionary(); - } + public Dictionary Texts { get; set; } = []; } diff --git a/framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs b/framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs new file mode 100644 index 0000000000..516e7b969f --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Microsoft/Extensions/DependencyInjection/AbpAutoMapperServiceCollectionExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp.Mapperly; +using Volo.Abp.ObjectMapping; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class AbpAutoMapperServiceCollectionExtensions +{ + public static IServiceCollection AddMapperlyObjectMapper(this IServiceCollection services) + { + return services.Replace( + ServiceDescriptor.Transient() + ); + } + + public static IServiceCollection AddMapperlyObjectMapper(this IServiceCollection services) + { + return services.Replace( + ServiceDescriptor.Transient, MapperlyAutoObjectMappingProvider>() + ); + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj b/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj new file mode 100644 index 0000000000..f32da75c72 --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo.Abp.Mapperly.csproj @@ -0,0 +1,29 @@ + + + + + + + netstandard2.0;netstandard2.1;net8.0;net9.0 + enable + Nullable + Volo.Abp.Mapperly + Volo.Abp.Mapperly + $(AssetTargetFallback);portable-net45+win8+wp8+wpa81; + false + false + false + + + + + + + + + + + + + + diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs new file mode 100644 index 0000000000..028a72bb67 --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyConventionalRegistrar.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Mapperly; + +public class AbpMapperlyConventionalRegistrar : DefaultConventionalRegistrar +{ + protected override bool IsConventionalRegistrationDisabled(Type type) + { + return !type.GetInterfaces().Any(x => x.IsGenericType && typeof(IAbpMapperlyMapper<,>) == x.GetGenericTypeDefinition()) || + base.IsConventionalRegistrationDisabled(type); + } + + protected override List GetExposedServiceTypes(Type type) + { + var exposedServiceTypes = base.GetExposedServiceTypes(type); + var mapperlyInterfaces = type.GetInterfaces().Where(x => + x.IsGenericType && (typeof(IAbpMapperlyMapper<,>) == x.GetGenericTypeDefinition() || + typeof(IAbpReverseMapperlyMapper<,>) == x.GetGenericTypeDefinition())); + return exposedServiceTypes + .Union(mapperlyInterfaces) + .Distinct() + .ToList(); + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs new file mode 100644 index 0000000000..60381163eb --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/AbpMapperlyModule.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Auditing; +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly; + +[DependsOn( + typeof(AbpObjectMappingModule), + typeof(AbpObjectExtendingModule), + typeof(AbpAuditingModule) +)] +public class AbpMapperlyModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddConventionalRegistrar(new AbpMapperlyConventionalRegistrar()); + } + + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddMapperlyObjectMapper(); + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs new file mode 100644 index 0000000000..eac0f6644c --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/IAbpMapperlyMapper.cs @@ -0,0 +1,23 @@ +namespace Volo.Abp.Mapperly; + +public interface IAbpMapperlyMapper +{ + TDestination Map(TSource source); + + void Map(TSource source, TDestination destination); + + void BeforeMap(TSource source); + + void AfterMap(TSource source, TDestination destination); +} + +public interface IAbpReverseMapperlyMapper : IAbpMapperlyMapper +{ + TSource ReverseMap(TDestination destination); + + void ReverseMap(TDestination destination, TSource source); + + void BeforeReverseMap(TDestination destination); + + void AfterReverseMap(TDestination destination, TSource source); +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs new file mode 100644 index 0000000000..705a1af72d --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapExtraPropertiesAttribute.cs @@ -0,0 +1,14 @@ +using System; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.Mapperly; + +[AttributeUsage(AttributeTargets.Class)] +public class MapExtraPropertiesAttribute : Attribute +{ + public MappingPropertyDefinitionChecks DefinitionChecks { get; set; } = MappingPropertyDefinitionChecks.Null; + + public string[]? IgnoredProperties { get; set; } + + public bool MapToRegularProperties { get; set; } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs new file mode 100644 index 0000000000..39d9dce995 --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperBase.cs @@ -0,0 +1,32 @@ +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Mapperly; + +public abstract class MapperBase : IAbpMapperlyMapper, ITransientDependency +{ + public abstract TDestination Map(TSource source); + + public abstract void Map(TSource source, TDestination destination); + + public virtual void BeforeMap(TSource source) + { + } + public virtual void AfterMap(TSource source, TDestination destination) + { + } +} + +public abstract class TwoWayMapperBase : MapperBase, IAbpReverseMapperlyMapper +{ + public abstract TSource ReverseMap(TDestination destination); + + public abstract void ReverseMap(TDestination destination, TSource source); + + public virtual void BeforeReverseMap(TDestination destination) + { + } + + public virtual void AfterReverseMap(TDestination destination, TSource source) + { + } +} diff --git a/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs new file mode 100644 index 0000000000..3876ee09a1 --- /dev/null +++ b/framework/src/Volo.Abp.Mapperly/Volo/Abp/Mapperly/MapperlyAutoObjectMappingProvider.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Data; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly; + +public class MapperlyAutoObjectMappingProvider : MapperlyAutoObjectMappingProvider, IAutoObjectMappingProvider +{ + public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider) + : base(serviceProvider) + { + } +} + +public class MapperlyAutoObjectMappingProvider : IAutoObjectMappingProvider +{ + protected static readonly ConcurrentDictionary> MapCache = new(); + protected static readonly List MapMethods = typeof(MapperlyAutoObjectMappingProvider) + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.Name == nameof(Map)).ToList(); + + protected IServiceProvider ServiceProvider { get; } + + public MapperlyAutoObjectMappingProvider(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + } + + public virtual TDestination Map(object source) + { + if (TryToMapCollection((TSource)source, default, out var collectionResult)) + { + return collectionResult; + } + + var mapper = ServiceProvider.GetService>(); + if (mapper != null) + { + mapper.BeforeMap((TSource)source); + var destination = mapper.Map((TSource)source); + TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, GetExtraProperties(destination)); + mapper.AfterMap((TSource)source, destination); + return destination; + } + + var reverseMapper = ServiceProvider.GetService>(); + if (reverseMapper != null) + { + reverseMapper.BeforeReverseMap((TSource)source); + var destination = reverseMapper.ReverseMap((TSource)source); + TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), (TSource)source, destination, GetExtraProperties(destination)); + reverseMapper.AfterReverseMap((TSource)source, destination); + return destination; + } + + throw GetNoMapperFoundException(); + } + + public virtual TDestination Map(TSource source, TDestination destination) + { + if (TryToMapCollection(source, destination, out var collectionResult)) + { + return collectionResult; + } + + var mapper = ServiceProvider.GetService>(); + if (mapper != null) + { + mapper.BeforeMap(source); + var destinationExtraProperties = GetExtraProperties(destination); + mapper.Map(source, destination); + TryMapExtraProperties(mapper.GetType().GetSingleAttributeOrNull(), source, destination, destinationExtraProperties); + mapper.AfterMap(source, destination); + return destination; + } + + var reverseMapper = ServiceProvider.GetService>(); + if (reverseMapper != null) + { + reverseMapper.BeforeReverseMap(source); + var destinationExtraProperties = GetExtraProperties(destination); + reverseMapper.ReverseMap(source, destination); + TryMapExtraProperties(reverseMapper.GetType().GetSingleAttributeOrNull(), source, destination, destinationExtraProperties); + reverseMapper.AfterReverseMap(source, destination); + return destination; + } + + throw GetNoMapperFoundException(); + } + + protected virtual AbpException GetNoMapperFoundException() + { + var newLine = Environment.NewLine; + var message = "No object mapping was found for the specified source and destination types." + + newLine + + newLine + + "Mapping attempted:" + + newLine + + $"{typeof(TSource).Name} -> {typeof(TDestination).Name}" + + newLine + + $"{typeof(TSource).FullName} -> {typeof(TDestination).FullName}" + + newLine + + newLine + + "How to fix:" + + newLine + + "Define a mapping class for these types:" + + newLine + + " - Use MapperBase for one-way mapping." + + newLine + + " - Use TwoWayMapperBase for two-way mapping." + + newLine + + newLine + + "For details, see the Mapperly integration document https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping#mapperly-integration"; + + return new AbpException(message); + } + + protected virtual bool TryToMapCollection(TSource source, TDestination? destination, out TDestination collectionResult) + { + if (!ObjectMappingHelper.IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) + { + collectionResult = default!; + return false; + } + + var mapperType = typeof(IAbpMapperlyMapper<,>).MakeGenericType(sourceArgumentType, destinationArgumentType); + var mapper = ServiceProvider.GetService(mapperType); + if (mapper == null) + { + mapperType = typeof(IAbpReverseMapperlyMapper<,>).MakeGenericType(destinationArgumentType, sourceArgumentType); + mapper = ServiceProvider.GetService(mapperType); + if (mapper == null) + { + //skip, no specific mapper + collectionResult = default!; + return false; + } + } + + var invoker = MapCache.GetOrAdd( + $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}", + _ => CreateMapDelegate(mapperType, sourceArgumentType, destinationArgumentType, destination != null)); + + var sourceList = source!.As(); + var result = definitionGenericType.IsGenericType + ? Activator.CreateInstance(definitionGenericType.MakeGenericType(destinationArgumentType))!.As() + : Array.CreateInstance(destinationArgumentType, sourceList.Count); + + if (destination != null && !destination.GetType().IsArray) + { + //Clear destination collection if destination not an array, We won't change array just same behavior as AutoMapper. + destination.As().Clear(); + } + + for (var i = 0; i < sourceList.Count; i++) + { + var invokeResult = destination == null + ? invoker(this, sourceList[i]!, null!) + : invoker(this, sourceList[i]!, Activator.CreateInstance(destinationArgumentType)!); + + if (definitionGenericType.IsGenericType) + { + result.Add(invokeResult); + destination?.As().Add(invokeResult); + } + else + { + result[i] = invokeResult; + } + } + + if (destination != null && destination.GetType().IsArray) + { + //Return the new collection if destination is an array, We won't change array just same behavior as AutoMapper. + collectionResult = (TDestination)result; + return true; + } + + //Return the destination if destination exists. The parameter reference equals with return object. + collectionResult = destination ?? (TDestination)result; + return true; + } + + protected virtual Func CreateMapDelegate( + Type mapperType, + Type sourceArgumentType, + Type destinationArgumentType, + bool hasDestination) + { + var method = !hasDestination + ? MapMethods.First(x => x.GetParameters().Length == 1).MakeGenericMethod(sourceArgumentType, destinationArgumentType) + : MapMethods.First(x => x.GetParameters().Length == 2).MakeGenericMethod(sourceArgumentType, destinationArgumentType); + var instanceParam = Expression.Parameter(typeof(object), "mapper"); + var sourceParam = Expression.Parameter(typeof(object), "source"); + var destinationParam = Expression.Parameter(typeof(object), "destination"); + + var instanceCast = Expression.Convert(instanceParam, method.DeclaringType!); + var callParams = new List + { + Expression.Convert(sourceParam, sourceArgumentType) + }; + + if (hasDestination) + { + callParams.Add(Expression.Convert(destinationParam, destinationArgumentType)); + } + + var call = Expression.Call(instanceCast, method, callParams); + var callConvert = Expression.Convert(call, typeof(object)); + + return Expression.Lambda>(callConvert, instanceParam, sourceParam, destinationParam).Compile(); + } + + protected virtual ExtraPropertyDictionary GetExtraProperties(TDestination destination) + { + var extraProperties = new ExtraPropertyDictionary(); + if (destination is not IHasExtraProperties hasExtraProperties) + { + return extraProperties; + } + + foreach (var property in hasExtraProperties.ExtraProperties) + { + extraProperties.Add(property.Key, property.Value); + } + return extraProperties; + } + + protected virtual void TryMapExtraProperties(MapExtraPropertiesAttribute? mapExtraPropertiesAttribute, TSource source, TDestination destination, ExtraPropertyDictionary destinationExtraProperty) + { + if(source is not IHasExtraProperties sourceHasExtraProperties) + { + return; + } + + if (destination is not IHasExtraProperties destinationHasExtraProperties) + { + return; + } + + if (sourceHasExtraProperties.ExtraProperties != null && sourceHasExtraProperties.ExtraProperties == + destinationHasExtraProperties.ExtraProperties) + { + ObjectHelper.TrySetProperty(destinationHasExtraProperties, x => x.ExtraProperties, () => new ExtraPropertyDictionary(destinationHasExtraProperties.ExtraProperties));; + } + + if (mapExtraPropertiesAttribute != null) + { + MapExtraProperties( + sourceHasExtraProperties, + destinationHasExtraProperties, + destinationExtraProperty, + mapExtraPropertiesAttribute.DefinitionChecks, + mapExtraPropertiesAttribute.IgnoredProperties, + mapExtraPropertiesAttribute.MapToRegularProperties + ); + } + } + protected virtual void MapExtraProperties( + IHasExtraProperties source, + IHasExtraProperties destination, + ExtraPropertyDictionary destinationExtraProperty, + MappingPropertyDefinitionChecks? definitionChecks = null, + string[]? ignoredProperties = null, + bool mapToRegularProperties = false) + { + var result = destinationExtraProperty.IsNullOrEmpty() + ? new Dictionary() + : new Dictionary(destinationExtraProperty); + + if (source.ExtraProperties != null && destination.ExtraProperties != null) + { + ExtensibleObjectMapper + .MapExtraPropertiesTo( + typeof(TSource), + typeof(TDestination), + source.ExtraProperties, + result, + definitionChecks, + ignoredProperties + ); + } + + ObjectHelper.TrySetProperty(destination, x => x.ExtraProperties, () => new ExtraPropertyDictionary(result)); + if (mapToRegularProperties) + { + destination.SetExtraPropertiesToRegularProperties(); + } + } +} diff --git a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs index 058508a30a..c103c76b51 100644 --- a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs +++ b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs @@ -50,6 +50,7 @@ public class MemoryDbRepository : RepositoryBase LazyServiceProvider.LazyGetRequiredService(); public MemoryDbRepository(IMemoryDatabaseProvider databaseProvider) + : base(AbpMemoryDbConsts.ProviderName) { DatabaseProvider = databaseProvider; } @@ -333,7 +334,7 @@ public class MemoryDbRepository : MemoryDbRepos if (entity == null) { - throw new EntityNotFoundException(typeof(TEntity), id); + throw new EntityNotFoundException(id); } return entity; diff --git a/framework/src/Volo.Abp.MemoryDb/Volo/Abp/MemoryDb/AbpMemoryDbConsts.cs b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/MemoryDb/AbpMemoryDbConsts.cs new file mode 100644 index 0000000000..a15563c68f --- /dev/null +++ b/framework/src/Volo.Abp.MemoryDb/Volo/Abp/MemoryDb/AbpMemoryDbConsts.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.MemoryDb; + +public class AbpMemoryDbConsts +{ + public const string ProviderName = "Volo.Abp.MemoryDb"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs index fdc7464d6b..4beb71d188 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs @@ -1,12 +1,12 @@ -using JetBrains.Annotations; -using MongoDB.Driver; -using MongoDB.Driver.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; +using MongoDB.Driver; +using MongoDB.Driver.Linq; using Volo.Abp.Auditing; using Volo.Abp.Data; using Volo.Abp.Domain.Entities; @@ -101,6 +101,7 @@ public class MongoDbRepository public IMongoDbRepositoryFilterer RepositoryFilterer => LazyServiceProvider.LazyGetService>()!; public MongoDbRepository(IMongoDbContextProvider dbContextProvider) + : base(AbpMongoDbConsts.ProviderName) { DbContextProvider = dbContextProvider; } @@ -802,7 +803,7 @@ public class MongoDbRepository if (entity == null) { - throw new EntityNotFoundException(typeof(TEntity), id); + throw new EntityNotFoundException(id); } return entity; diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpBsonSerializer.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpBsonSerializer.cs new file mode 100644 index 0000000000..59b24e6063 --- /dev/null +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpBsonSerializer.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Concurrent; + +using System.Reflection; +using MongoDB.Bson.Serialization; + +namespace Volo.Abp.MongoDB; + +public static class AbpBsonSerializer +{ + private static readonly ConcurrentDictionary Cache; + + static AbpBsonSerializer() + { + var registry = BsonSerializer.SerializerRegistry; + var type = typeof(BsonSerializerRegistry); + var cacheField = type.GetField("_cache", BindingFlags.NonPublic | BindingFlags.Instance) ?? + throw new AbpException($"Cannot find _cache field of {type.FullName}."); + Cache = (ConcurrentDictionary)cacheField.GetValue(registry)!; + } + + public static void RemoveSerializer() + { + Cache.TryRemove(typeof(T), out _); + } + + public static ConcurrentDictionary GetSerializerCache() + { + return Cache; + } +} diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbConsts.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbConsts.cs new file mode 100644 index 0000000000..e107d31bf0 --- /dev/null +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbConsts.cs @@ -0,0 +1,6 @@ +namespace Volo.Abp.MongoDB; + +public class AbpMongoDbConsts +{ + public const string ProviderName = "Volo.Abp.MongoDB"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs index 195491d989..fb9d6eaea1 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/AbpMongoDbModule.cs @@ -19,7 +19,8 @@ public class AbpMongoDbModule : AbpModule { static AbpMongoDbModule() { - BsonSerializer.TryRegisterSerializer(new GuidSerializer(GuidRepresentation.Standard)); + AbpBsonSerializer.RemoveSerializer(); + BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard)); BsonTypeMapper.RegisterCustomTypeMapper(typeof(Guid), new AbpGuidCustomBsonTypeMapper()); } diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/IncomingEventRecord.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/IncomingEventRecord.cs index a48492d1da..0a3a1d4dd2 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/IncomingEventRecord.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/IncomingEventRecord.cs @@ -23,9 +23,13 @@ public class IncomingEventRecord : public DateTime CreationTime { get; private set; } - public bool Processed { get; set; } + public IncomingEventStatus Status { get; set; } = IncomingEventStatus.Pending; - public DateTime? ProcessedTime { get; set; } + public DateTime? HandledTime { get; set; } + + public int RetryCount { get; set; } = 0; + + public DateTime? NextRetryTime { get; set; } = null; protected IncomingEventRecord() { @@ -41,7 +45,10 @@ public class IncomingEventRecord : EventName = eventInfo.EventName; EventData = eventInfo.EventData; CreationTime = eventInfo.CreationTime; - + Status = eventInfo.Status; + HandledTime = eventInfo.HandledTime; + RetryCount = eventInfo.RetryCount; + NextRetryTime = eventInfo.NextRetryTime; ExtraProperties = new ExtraPropertyDictionary(); this.SetDefaultsForExtraProperties(); foreach (var property in eventInfo.ExtraProperties) @@ -57,7 +64,11 @@ public class IncomingEventRecord : MessageId, EventName, EventData, - CreationTime + CreationTime, + Status, + HandledTime, + RetryCount, + NextRetryTime ); foreach (var property in ExtraProperties) @@ -70,7 +81,20 @@ public class IncomingEventRecord : public void MarkAsProcessed(DateTime processedTime) { - Processed = true; - ProcessedTime = processedTime; + Status = IncomingEventStatus.Processed; + HandledTime = processedTime; + } + + public void MarkAsDiscarded(DateTime discardedTime) + { + Status = IncomingEventStatus.Discarded; + HandledTime = discardedTime; + } + + public void RetryLater(int retryCount, DateTime nextRetryTime) + { + Status = IncomingEventStatus.Pending; + NextRetryTime = nextRetryTime; + RetryCount = retryCount; } } diff --git a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs index e5d726526b..8baeb82f3a 100644 --- a/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs +++ b/framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/DistributedEvents/MongoDbContextEventInbox.cs @@ -61,10 +61,12 @@ public class MongoDbContextEventInbox : IMongoDbContextEventInb transformedFilter = InboxOutboxFilterExpressionTransformer.Transform(filter)!; } + var now = Clock.Now; var outgoingEventRecords = await dbContext .IncomingEvents .AsQueryable() - .Where(x => x.Processed == false) + .Where(x => x.Status == IncomingEventStatus.Pending) + .Where(x => x.NextRetryTime == null || x.NextRetryTime <= now) .WhereIf(transformedFilter != null, transformedFilter!) .OrderBy(x => x.CreationTime) .Take(maxCount) @@ -81,7 +83,43 @@ public class MongoDbContextEventInbox : IMongoDbContextEventInb var dbContext = await DbContextProvider.GetDbContextAsync(); var filter = Builders.Filter.Eq(x => x.Id, id); - var update = Builders.Update.Set(x => x.Processed, true).Set(x => x.ProcessedTime, Clock.Now); + var update = Builders.Update.Set(x => x.Status, IncomingEventStatus.Processed).Set(x => x.HandledTime, Clock.Now); + + if (dbContext.SessionHandle != null) + { + await dbContext.IncomingEvents.UpdateOneAsync(dbContext.SessionHandle, filter, update); + } + else + { + await dbContext.IncomingEvents.UpdateOneAsync(filter, update); + } + } + + [UnitOfWork] + public virtual async Task RetryLaterAsync(Guid id, int retryCount, DateTime? nextRetryTime) + { + var dbContext = await DbContextProvider.GetDbContextAsync(); + + var filter = Builders.Filter.Eq(x => x.Id, id); + var update = Builders.Update.Set(x => x.RetryCount, retryCount).Set(x => x.NextRetryTime, nextRetryTime); + + if (dbContext.SessionHandle != null) + { + await dbContext.IncomingEvents.UpdateOneAsync(dbContext.SessionHandle, filter, update); + } + else + { + await dbContext.IncomingEvents.UpdateOneAsync(filter, update); + } + } + + [UnitOfWork] + public virtual async Task MarkAsDiscardAsync(Guid id) + { + var dbContext = await DbContextProvider.GetDbContextAsync(); + + var filter = Builders.Filter.Eq(x => x.Id, id); + var update = Builders.Update.Set(x => x.Status, IncomingEventStatus.Discarded).Set(x => x.HandledTime, Clock.Now); if (dbContext.SessionHandle != null) { @@ -108,11 +146,11 @@ public class MongoDbContextEventInbox : IMongoDbContextEventInb if (dbContext.SessionHandle != null) { - await dbContext.IncomingEvents.DeleteManyAsync(dbContext.SessionHandle, x => x.Processed && x.CreationTime < timeToKeepEvents); + await dbContext.IncomingEvents.DeleteManyAsync(dbContext.SessionHandle, x => (x.Status == IncomingEventStatus.Processed || x.Status == IncomingEventStatus.Discarded) && x.CreationTime < timeToKeepEvents); } else { - await dbContext.IncomingEvents.DeleteManyAsync(x => x.Processed && x.CreationTime < timeToKeepEvents); + await dbContext.IncomingEvents.DeleteManyAsync(x => (x.Status == IncomingEventStatus.Processed || x.Status == IncomingEventStatus.Discarded) && x.CreationTime < timeToKeepEvents); } } } diff --git a/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs index 414983c87a..26640c3dfd 100644 --- a/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs +++ b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/AbpTenantResolveOptions.cs @@ -8,6 +8,11 @@ public class AbpTenantResolveOptions [NotNull] public List TenantResolvers { get; } + /// + /// Fallback tenant to use when no other resolver resolves a tenant. + /// + public string? FallbackTenant { get; set; } + public AbpTenantResolveOptions() { TenantResolvers = new List(); diff --git a/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverNames.cs b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverNames.cs new file mode 100644 index 0000000000..3542d3a948 --- /dev/null +++ b/framework/src/Volo.Abp.MultiTenancy.Abstractions/Volo/Abp/MultiTenancy/TenantResolverNames.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.MultiTenancy; + +public static class TenantResolverNames +{ + public const string FallbackTenant = "FallbackTenant"; +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs index 3ae9ae67b1..4351ef62bb 100644 --- a/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs +++ b/framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/TenantResolver.cs @@ -39,6 +39,12 @@ public class TenantResolver : ITenantResolver, ITransientDependency } } + if (result.TenantIdOrName.IsNullOrEmpty() && !string.IsNullOrWhiteSpace(_options.FallbackTenant)) + { + result.TenantIdOrName = _options.FallbackTenant; + result.AppliedResolvers.Add(TenantResolverNames.FallbackTenant); + } + return result; } } diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs index 75f5672a29..54cdc2bc94 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/Data/HasExtraPropertiesExtensions.cs @@ -26,34 +26,9 @@ public static class HasExtraPropertiesExtensions public static TProperty? GetProperty(this IHasExtraProperties source, string name, TProperty? defaultValue = default) { - var value = source.GetProperty(name); - if (value == null) - { - return defaultValue; - } - - if (TypeHelper.IsPrimitiveExtended(typeof(TProperty), includeEnums: true)) - { - var conversionType = typeof(TProperty); - if (TypeHelper.IsNullable(conversionType)) - { - conversionType = conversionType.GetFirstGenericArgumentIfNullable(); - } - - if (conversionType == typeof(Guid)) - { - return (TProperty)TypeDescriptor.GetConverter(conversionType).ConvertFromInvariantString(value.ToString()!)!; - } - - if (conversionType.IsEnum) - { - return (TProperty)Enum.Parse(conversionType, value.ToString()!); - } - - return (TProperty)Convert.ChangeType(value, conversionType, CultureInfo.InvariantCulture); - } - - throw new AbpException("GetProperty does not support non-primitive types. Use non-generic GetProperty method and handle type casting manually."); + return TypeHelper.ChangeTypePrimitiveExtended( + source.GetProperty(name, (object?) defaultValue) + ) ?? defaultValue; } public static TSource SetProperty( diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs index eae85f77d7..48a3d37916 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs @@ -188,7 +188,7 @@ public static class ExtensibleObjectMapper return false; } - if (definitionChecks != null) + if (definitionChecks != null && definitionChecks.Value != MappingPropertyDefinitionChecks.Null) { if (definitionChecks.Value.HasFlag(MappingPropertyDefinitionChecks.Source)) { diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs index 04668476a0..f717e97714 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/MappingPropertyDefinitionChecks.cs @@ -5,20 +5,25 @@ namespace Volo.Abp.ObjectExtending; [Flags] public enum MappingPropertyDefinitionChecks : byte { + /// + /// Same as Null, We need to use this in Attribute to avoid null checks. + /// + Null = 0, + /// /// No check. Copy all extra properties from the source to the destination. /// - None = 0, + None = 1 << 0, /// /// Copy the extra properties defined for the source class. /// - Source = 1, + Source = 1 << 1, /// /// Copy the extra properties defined for the destination class. /// - Destination = 2, + Destination = 1 << 2, /// /// Copy extra properties defined for both of the source and destination classes. diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs index 30733b1e24..faf8c4e258 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -5,6 +5,7 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using Volo.Abp.DependencyInjection; @@ -25,7 +26,7 @@ public class DefaultObjectMapper : DefaultObjectMapper, IObjectMapper< public class DefaultObjectMapper : IObjectMapper, ITransientDependency { - protected static ConcurrentDictionary MethodInfoCache { get; } = new ConcurrentDictionary(); + protected static readonly ConcurrentDictionary> MapCache = new(); public IAutoObjectMappingProvider AutoObjectMappingProvider { get; } protected IServiceProvider ServiceProvider { get; } @@ -122,7 +123,7 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency protected virtual bool TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination, out TDestination collectionResult) { - if (!IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) + if (!ObjectMappingHelper.IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) { collectionResult = default!; return false; @@ -137,46 +138,9 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return false; } - var cacheKey = $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}"; - var method = MethodInfoCache.GetOrAdd( - cacheKey, - _ => - { - var methods = specificMapper - .GetType() - .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Where(x => x.Name == nameof(IObjectMapper.Map)) - .Where(x => - { - var parameters = x.GetParameters(); - if (destination == null && parameters.Length != 1 || - destination != null && parameters.Length != 2 || - parameters[0].ParameterType != sourceArgumentType) - { - return false; - } - - return destination == null || parameters[1].ParameterType == destinationArgumentType; - }) - .ToList(); - - if (methods.IsNullOrEmpty()) - { - throw new AbpException($"Could not find a method named '{nameof(IObjectMapper.Map)}'" + - $" with parameters({(destination == null ? sourceArgumentType.ToString() : sourceArgumentType + "," + destinationArgumentType)})" + - $" in the type '{mapperType}'."); - } - - if (methods.Count > 1) - { - throw new AbpException($"Found more than one method named '{nameof(IObjectMapper.Map)}'" + - $" with parameters({(destination == null ? sourceArgumentType.ToString() : sourceArgumentType + "," + destinationArgumentType)})" + - $" in the type '{mapperType}'."); - } - - return methods.First(); - } - ); + var invoker = MapCache.GetOrAdd( + $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}", + _ => CreateMapDelegate(mapperType, sourceArgumentType, destinationArgumentType, destination != null)); var sourceList = source!.As(); var result = definitionGenericType.IsGenericType @@ -192,8 +156,8 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency for (var i = 0; i < sourceList.Count; i++) { var invokeResult = destination == null - ? method.Invoke(specificMapper, new [] { sourceList[i] })! - : method.Invoke(specificMapper, new [] { sourceList[i], Activator.CreateInstance(destinationArgumentType)! })!; + ? invoker(specificMapper, sourceList[i]!, null!) + : invoker(specificMapper, sourceList[i]!, Activator.CreateInstance(destinationArgumentType)!); if (definitionGenericType.IsGenericType) { @@ -218,61 +182,64 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return true; } - protected virtual bool IsCollectionGenericType(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType) + protected virtual Func CreateMapDelegate( + Type mapperType, + Type sourceArgumentType, + Type destinationArgumentType, + bool hasDestination) { - sourceArgumentType = default!; - destinationArgumentType = default!; - definitionGenericType = default!; - - if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) || - (!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray)) - { - return false; - } + var methods = mapperType + .GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(x => x.Name == nameof(IObjectMapper.Map)) + .Where(x => + { + var parameters = x.GetParameters(); + if (!hasDestination && parameters.Length != 1 || + hasDestination && parameters.Length != 2 || + parameters[0].ParameterType != sourceArgumentType) + { + return false; + } - var supportedCollectionTypes = new[] - { - typeof(IEnumerable<>), - typeof(ICollection<>), - typeof(Collection<>), - typeof(IList<>), - typeof(List<>) - }; + return !hasDestination || parameters[1].ParameterType == destinationArgumentType; + }) + .ToList(); - if (typeof(TSource).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TSource).GetGenericTypeDefinition())) + if (methods.Count == 0) { - sourceArgumentType = typeof(TSource).GenericTypeArguments[0]; + throw new AbpException($"Could not find a method named '{nameof(IObjectMapper.Map)}'" + + $" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" + + $" in the type '{mapperType}'."); } - if (typeof(TSource).IsArray) + if (methods.Count > 1) { - sourceArgumentType = typeof(TSource).GetElementType()!; + throw new AbpException($"Found more than one method named '{nameof(IObjectMapper.Map)}'" + + $" with parameters({(hasDestination ? sourceArgumentType + ", " + destinationArgumentType : sourceArgumentType.ToString())})" + + $" in the type '{mapperType}'."); } - if (sourceArgumentType == default!) - { - return false; - } + var method = methods[0]; - definitionGenericType = typeof(List<>); - if (typeof(TDestination).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TDestination).GetGenericTypeDefinition())) - { - destinationArgumentType = typeof(TDestination).GenericTypeArguments[0]; + var instanceParam = Expression.Parameter(typeof(object), "mapper"); + var sourceParam = Expression.Parameter(typeof(object), "source"); + var destinationParam = Expression.Parameter(typeof(object), "destination"); - if (typeof(TDestination).GetGenericTypeDefinition() == typeof(ICollection<>) || - typeof(TDestination).GetGenericTypeDefinition() == typeof(Collection<>)) - { - definitionGenericType = typeof(Collection<>); - } - } + var instanceCast = Expression.Convert(instanceParam, method.DeclaringType!); + var callParams = new List + { + Expression.Convert(sourceParam, sourceArgumentType) + }; - if (typeof(TDestination).IsArray) + if (hasDestination) { - destinationArgumentType = typeof(TDestination).GetElementType()!; - definitionGenericType = typeof(Array); + callParams.Add(Expression.Convert(destinationParam, destinationArgumentType)); } - return destinationArgumentType != default!; + var call = Expression.Call(instanceCast, method, callParams); + var callConvert = Expression.Convert(call, typeof(object)); + + return Expression.Lambda>(callConvert, instanceParam, sourceParam, destinationParam).Compile(); } protected virtual TDestination AutoMap(object source) diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs new file mode 100644 index 0000000000..5949edaba9 --- /dev/null +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/ObjectMappingHelper.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +namespace Volo.Abp.ObjectMapping; + +public static class ObjectMappingHelper +{ + private static readonly ConcurrentDictionary<(Type, Type), (Type sourceArgumentType, Type destinationArgumentType, Type definitionGenericType)?> Cache = new(); + + public static bool IsCollectionGenericType( + out Type sourceArgumentType, + out Type destinationArgumentType, + out Type definitionGenericType) + { + var cached = Cache.GetOrAdd((typeof(TSource), typeof(TDestination)), _ => IsCollectionGenericTypeInternal()); + if (cached == null) + { + sourceArgumentType = destinationArgumentType = definitionGenericType = null!; + return false; + } + + (sourceArgumentType, destinationArgumentType, definitionGenericType) = cached.Value; + return true; + } + + private static (Type, Type, Type)? IsCollectionGenericTypeInternal() + { + if (!IsCollectionGenericTypeInternal(typeof(TSource), out var sourceArgumentType, out _) || + !IsCollectionGenericTypeInternal(typeof(TDestination), out var destinationArgumentType, out var definitionGenericType)) + { + return null; + } + + return (sourceArgumentType, destinationArgumentType, definitionGenericType); + } + + private static bool IsCollectionGenericTypeInternal(Type type, out Type elementType, out Type definitionGenericType) + { + var supportedCollectionTypes = new[] + { + typeof(IEnumerable<>), + typeof(ICollection<>), + typeof(Collection<>), + typeof(IList<>), + typeof(List<>) + }; + + if (type.IsArray) + { + elementType = type.GetElementType()!; + definitionGenericType = type; + return true; + } + + if (type.IsGenericType && + supportedCollectionTypes.Contains(type.GetGenericTypeDefinition()) || + type.GetInterfaces().Any(i => i.IsGenericType && supportedCollectionTypes.Contains(i.GetGenericTypeDefinition()))) + { + elementType = type.GetGenericArguments()[0]; + definitionGenericType = type.GetGenericTypeDefinition(); + if (definitionGenericType == typeof(IEnumerable<>) || + definitionGenericType == typeof(IList<>)) + { + definitionGenericType = typeof(List<>); + } + + if (definitionGenericType == typeof(ICollection<>)) + { + definitionGenericType = typeof(Collection<>); + } + return true; + } + + elementType = null!; + definitionGenericType = null!; + return false; + } +} diff --git a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/RemoteDynamicClaimsPrincipalContributorCacheBase.cs b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/RemoteDynamicClaimsPrincipalContributorCacheBase.cs index 331a3c7bdf..71fdbf0c17 100644 --- a/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/RemoteDynamicClaimsPrincipalContributorCacheBase.cs +++ b/framework/src/Volo.Abp.Security/Volo/Abp/Security/Claims/RemoteDynamicClaimsPrincipalContributorCacheBase.cs @@ -42,7 +42,8 @@ public abstract class RemoteDynamicClaimsPrincipalContributorCacheBase(); + var partManager = new ApplicationPartManager(); + + for (var i = 0; i < 10; i++) + { + var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName($"ModuleA{i}.dll"), AssemblyBuilderAccess.Run); + partManager.ApplicationParts.Add(new AssemblyPart(assembly)); + moduleDescriptors.Add(CreateModuleDescriptor(assembly)); + } + var randomApplicationParts = partManager.ApplicationParts.OrderBy(x => Guid.NewGuid()).ToList(); // Shuffle the parts + + // Additional part + randomApplicationParts.AddFirst(new CompiledRazorAssemblyPart(typeof(AbpAspNetCoreModule).Assembly)); + randomApplicationParts.Insert(5, new CompiledRazorAssemblyPart(typeof(AbpAspNetCoreMvcModule).Assembly)); + randomApplicationParts.AddLast(new CompiledRazorAssemblyPart(typeof(AbpVirtualFileSystemModule).Assembly)); + + partManager.ApplicationParts.Clear(); + foreach (var part in randomApplicationParts) + { + partManager.ApplicationParts.Add(part); + } + + var moduleContainer = CreateFakeModuleContainer(moduleDescriptors); + + ApplicationPartSorter.Sort(partManager, moduleContainer); + + // Act + partManager.ApplicationParts.Count.ShouldBe(13); // 10 modules + 3 additional parts + + var applicationParts = partManager.ApplicationParts.Reverse().ToList(); // Reverse the order to match the expected output + + applicationParts[0].ShouldBeOfType().Assembly.ShouldBe(typeof(AbpAspNetCoreModule).Assembly); + applicationParts[1].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA0"); + applicationParts[2].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA1"); + applicationParts[3].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA2"); + applicationParts[4].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA3"); + applicationParts[5].ShouldBeOfType().Assembly.ShouldBe(typeof(AbpAspNetCoreMvcModule).Assembly); + applicationParts[6].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA4"); + applicationParts[7].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA5"); + applicationParts[8].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA6"); + applicationParts[9].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA7"); + applicationParts[10].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA8"); + applicationParts[11].ShouldBeOfType().Assembly.GetName().Name.ShouldStartWith("ModuleA9"); + applicationParts[12].ShouldBeOfType().Assembly.ShouldBe(typeof(AbpVirtualFileSystemModule).Assembly); + } + + private static IModuleContainer CreateFakeModuleContainer(List moduleDescriptors) + { + var fakeModuleContainer = Substitute.For(); + fakeModuleContainer.Modules.Returns(moduleDescriptors); + return fakeModuleContainer; + } + + private static IAbpModuleDescriptor CreateModuleDescriptor(Assembly assembly) + { + var moduleDescriptor = Substitute.For(); + moduleDescriptor.Assembly.Returns(assembly); + return moduleDescriptor; + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Security/Headers/SecurityHeadersTestController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Security/Headers/SecurityHeadersTestController.cs index 8e92558cb8..aa6462996a 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Security/Headers/SecurityHeadersTestController.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Security/Headers/SecurityHeadersTestController.cs @@ -2,10 +2,18 @@ namespace Volo.Abp.AspNetCore.Mvc.Security.Headers; +[Route("SecurityHeadersTest")] public class SecurityHeadersTestController : AbpController { + [HttpGet("Get")] public ActionResult Get() { return Content("OK"); } + + [HttpGet("ignored")] + public ActionResult Get_Ignored() + { + return Content("OK"); + } } diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Security/Headers/SecurityHeadersTestController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Security/Headers/SecurityHeadersTestController_Tests.cs index 336c7fe8db..b71103620b 100644 --- a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Security/Headers/SecurityHeadersTestController_Tests.cs +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Security/Headers/SecurityHeadersTestController_Tests.cs @@ -15,6 +15,8 @@ public class SecurityHeadersTestController_Tests : AspNetCoreMvcTestBase { options.UseContentSecurityPolicyHeader = true; options.Headers["Referrer-Policy"] = "no-referrer"; + + options.IgnoredScriptNoncePaths.Add("/SecurityHeadersTest/ignored"); }); base.ConfigureServices(services); @@ -27,7 +29,6 @@ public class SecurityHeadersTestController_Tests : AspNetCoreMvcTestBase responseMessage.Headers.ShouldContain(x => x.Key == "X-Content-Type-Options" & x.Value.First().ToString() == "nosniff"); responseMessage.Headers.ShouldContain(x => x.Key == "X-XSS-Protection" & x.Value.First().ToString() == "1; mode=block"); responseMessage.Headers.ShouldContain(x => x.Key == "X-Frame-Options" & x.Value.First().ToString() == "SAMEORIGIN"); - responseMessage.Headers.ShouldContain(x => x.Key == "X-Content-Type-Options" & x.Value.First().ToString() == "nosniff"); } [Fact] @@ -37,4 +38,15 @@ public class SecurityHeadersTestController_Tests : AspNetCoreMvcTestBase responseMessage.Headers.ShouldNotBeEmpty(); responseMessage.Headers.ShouldContain(x => x.Key == "Referrer-Policy" && x.Value.First().ToString() == "no-referrer"); } + + [Fact] + public async Task SecurityHeaders_Should_Be_Ignored() + { + var responseMessage = await GetResponseAsync("/SecurityHeadersTest/ignored"); + responseMessage.Headers.ShouldNotBeEmpty(); + responseMessage.Headers.ShouldNotContain(x => x.Key == "X-Content-Type-Options" & x.Value.First().ToString() == "nosniff"); + responseMessage.Headers.ShouldNotContain(x => x.Key == "X-XSS-Protection" & x.Value.First().ToString() == "1; mode=block"); + responseMessage.Headers.ShouldNotContain(x => x.Key == "X-Frame-Options" & x.Value.First().ToString() == "SAMEORIGIN"); + responseMessage.Headers.ShouldNotContain(x => x.Key == "Referrer-Policy" && x.Value.First().ToString() == "no-referrer"); + } } diff --git a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs index 4a7a9fb28f..bb2e9eeecd 100644 --- a/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs +++ b/framework/test/Volo.Abp.Ddd.Tests/Volo/Abp/Domain/Repositories/RepositoryRegistration_Tests.cs @@ -295,6 +295,10 @@ public class RepositoryRegistration_Tests public class MyTestDefaultRepository : RepositoryBase where TEntity : class, IEntity { + public MyTestDefaultRepository() + : base("MyTestDefault") + { + } [Obsolete("Use GetQueryableAsync method.")] protected override IQueryable GetQueryable() @@ -408,6 +412,8 @@ public class RepositoryRegistration_Tests public class MyTestAggregateRootWithDefaultPkEmptyRepository : IMyTestAggregateRootWithDefaultPkEmptyRepository { public bool? IsChangeTrackingEnabled { get; set; } + public string EntityName { get; set; } + public string ProviderName { get; } = "MyFakeProvider"; } public class TestDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/SharedEntity_Repository_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/SharedEntity_Repository_Tests.cs new file mode 100644 index 0000000000..869069debe --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/SharedEntity_Repository_Tests.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.MultiTenancy; +using Volo.Abp.TestApp.Domain; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.Repositories; + +public class SharedEntity_Repository_Tests : EntityFrameworkCoreTestBase +{ + protected readonly IRepository TestSharedTypeEntityRepository; + protected readonly ICurrentTenant CurrentTenant; + protected readonly IDataFilter DataFilter; + + public SharedEntity_Repository_Tests() + { + TestSharedTypeEntityRepository = GetRequiredService>(); + CurrentTenant = GetRequiredService(); + DataFilter = GetRequiredService>(); + } + + [Fact] + public async Task SharedEntity_Test() + { + await WithUnitOfWorkAsync(async () => + { + TestSharedTypeEntityRepository.SetEntityName("TestSharedEntity1"); + + var tenantId = Guid.NewGuid(); + await TestSharedTypeEntityRepository.InsertManyAsync(new List() + { + new TestSharedEntity(Guid.NewGuid()) + { + TenantId = null, + IsDeleted = false, + Name = "Test Person1", + Age = 10, + Birthday = DateTime.Now + }.SetProperty("testProperty", "Test Value1"), + new TestSharedEntity(Guid.NewGuid()) + { + TenantId = tenantId, + IsDeleted = false, + Name = "Test Person2", + Age = 20, + Birthday = DateTime.Now + }, + new TestSharedEntity(Guid.NewGuid()) + { + TenantId = tenantId, + IsDeleted = true, + Name = "Test Person3", + Age = 30, + Birthday = DateTime.Now + }, + new TestSharedEntity(Guid.NewGuid()) + { + TenantId = null, + IsDeleted = true, + Name = "Test Person4", + Age = 40, + Birthday = DateTime.Now + } + }, true); + + var entities = (await TestSharedTypeEntityRepository.GetListAsync()).OrderBy(x => x.Name).ToList(); + entities.Count.ShouldBe(1); + entities[0].TenantId.ShouldBeNull(); + entities[0].IsDeleted.ShouldBe(false); + entities[0].Name.ShouldBe("Test Person1"); + entities[0].Age.ShouldBe(10); + entities[0].GetProperty("testProperty").ShouldBe("Test Value1"); + + using (CurrentTenant.Change(tenantId)) + { + entities = (await TestSharedTypeEntityRepository.GetListAsync()).OrderBy(x => x.Name).ToList(); + entities.Count.ShouldBe(1); + entities[0].TenantId.ShouldBe(tenantId); + entities[0].IsDeleted.ShouldBe(false); + entities[0].Name.ShouldBe("Test Person2"); + entities[0].Age.ShouldBe(20); + } + + using (DataFilter.Disable()) + { + entities = (await TestSharedTypeEntityRepository.GetListAsync()).OrderBy(x => x.Name).ToList(); + entities.Count.ShouldBe(2); + + entities[0].TenantId.ShouldBeNull(); + entities[0].IsDeleted.ShouldBe(false); + entities[0].Name.ShouldBe("Test Person1"); + entities[0].Age.ShouldBe(10); + + entities[1].TenantId.ShouldBeNull(); + entities[1].IsDeleted.ShouldBe(true); + entities[1].Name.ShouldBe("Test Person4"); + entities[1].Age.ShouldBe(40); + } + + using (CurrentTenant.Change(tenantId)) + { + using (DataFilter.Disable()) + { + entities = (await TestSharedTypeEntityRepository.GetListAsync()).OrderBy(x => x.Name).ToList(); + entities.Count.ShouldBe(2); + + entities[0].TenantId.ShouldBe(tenantId); + entities[0].IsDeleted.ShouldBe(false); + entities[0].Name.ShouldBe("Test Person2"); + entities[0].Age.ShouldBe(20); + + entities[1].TenantId.ShouldBe(tenantId); + entities[1].IsDeleted.ShouldBe(true); + entities[1].Name.ShouldBe("Test Person3"); + entities[1].Age.ShouldBe(30); + } + } + + TestSharedTypeEntityRepository.SetEntityName("TestSharedEntity2"); + await TestSharedTypeEntityRepository.InsertManyAsync(new List() + { + new TestSharedEntity(Guid.NewGuid()) + { + Name = "Test Person1 from Second Table", + Age = 110, + Birthday = DateTime.Now + } + }, true); + + var entitiesFromSecondTable = (await TestSharedTypeEntityRepository.GetListAsync()).OrderBy(x => x.Name).ToList(); + entitiesFromSecondTable.Count.ShouldBe(1); + entitiesFromSecondTable[0].TenantId.ShouldBeNull(); + entitiesFromSecondTable[0].IsDeleted.ShouldBe(false); + entitiesFromSecondTable[0].Name.ShouldBe("Test Person1 from Second Table"); + entitiesFromSecondTable[0].Age.ShouldBe(110); + }); + } + + [Fact] + public async Task SharedEntity_DynamicProperty_Test() + { + await WithUnitOfWorkAsync(async () => + { + TestSharedTypeEntityRepository.SetEntityName("TestSharedEntity1"); + + var entity = new TestSharedEntity(Guid.NewGuid()) + { + TenantId = null, + IsDeleted = false, + Name = "Test Person1", + Age = 10, + Birthday = DateTime.Now + }; + + entity["DynamicProperty"] = "Test Value1"; + + await TestSharedTypeEntityRepository.InsertAsync(entity, true); + + entity = await TestSharedTypeEntityRepository.FindAsync(x => x.Id == entity.Id!); + entity.ShouldNotBeNull(); + + entity.Name.ShouldBe("Test Person1"); + entity.Age.ShouldBe(10); + entity.Birthday.ShouldNotBeNull(); + entity["DynamicProperty"].ShouldBe("Test Value1"); + + TestSharedTypeEntityRepository.SetEntityName("TestSharedEntity2"); + entity = await TestSharedTypeEntityRepository.FindAsync(x => x.Id == entity.Id!); + entity.ShouldBeNull(); + }); + } +} diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs index d05e367ff7..0efef5ba1f 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/TestMigrationsDbContext.cs @@ -1,5 +1,6 @@ using System; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Volo.Abp.EntityFrameworkCore.Modeling; using Volo.Abp.EntityFrameworkCore.TestApp.SecondContext; using Volo.Abp.EntityFrameworkCore.TestApp.ThirdDbContext; @@ -33,6 +34,9 @@ public class TestMigrationsDbContext : AbpDbContext public DbSet Blogs { get; set; } public DbSet BlogPosts { get; set; } + public DbSet TestSharedEntity => Set("TestSharedEntity1"); + public DbSet TestSharedEntity2 => Set("TestSharedEntity2"); + public TestMigrationsDbContext(DbContextOptions options) : base(options) { @@ -41,8 +45,25 @@ public class TestMigrationsDbContext : AbpDbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { + // Owned and SharedTypeEntity should be configured before the base OnModelCreating call + modelBuilder.Owned(); + Action> sharedEntityBuildAction = b => + { + b.ConfigureByConvention(); + b.Property(x => x.Id); + b.Property(x => x.TenantId); + b.Property(x => x.IsDeleted); + b.Property(x => x.Name); + b.Property(x => x.Age); + b.Property(x => x.Birthday); + + b.Property("DynamicProperty"); + }; + modelBuilder.SharedTypeEntity("TestSharedEntity1", sharedEntityBuildAction); + modelBuilder.SharedTypeEntity("TestSharedEntity2", sharedEntityBuildAction); + base.OnModelCreating(modelBuilder); modelBuilder.Entity(b => diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs index 242809bf7c..b2680abdff 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/TestApp/EntityFrameworkCore/TestAppDbContext.cs @@ -1,7 +1,7 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.Extensions.Logging; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using Volo.Abp.DependencyInjection; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.Modeling; @@ -40,6 +40,9 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, public DbSet Blogs { get; set; } public DbSet BlogPosts { get; set; } + public DbSet TestSharedEntity => Set("TestSharedEntity1"); + public DbSet TestSharedEntity2 => Set("TestSharedEntity2"); + public TestAppDbContext(DbContextOptions options) : base(options) { @@ -54,8 +57,25 @@ public class TestAppDbContext : AbpDbContext, IThirdDbContext, protected override void OnModelCreating(ModelBuilder modelBuilder) { + // Owned and SharedTypeEntity should be configured before the base OnModelCreating call + modelBuilder.Owned(); + Action> sharedEntityBuildAction = b => + { + b.ConfigureByConvention(); + b.Property(x => x.Id); + b.Property(x => x.TenantId); + b.Property(x => x.IsDeleted); + b.Property(x => x.Name); + b.Property(x => x.Age); + b.Property(x => x.Birthday); + + b.Property("DynamicProperty"); + }; + modelBuilder.SharedTypeEntity("TestSharedEntity1", sharedEntityBuildAction); + modelBuilder.SharedTypeEntity("TestSharedEntity2", sharedEntityBuildAction); + base.OnModelCreating(modelBuilder); modelBuilder.Entity(b => diff --git a/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/ObjectToInferredTypesConverter_Tests.cs b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/ObjectToInferredTypesConverter_Tests.cs new file mode 100644 index 0000000000..b5d5c3b394 --- /dev/null +++ b/framework/test/Volo.Abp.Json.Tests/Volo/Abp/Json/ObjectToInferredTypesConverter_Tests.cs @@ -0,0 +1,54 @@ +using System.Text.Json; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Json; + +public class ObjectToInferredTypesConverter_Tests : AbpJsonSystemTextJsonTestBase +{ + private readonly IJsonSerializer _jsonSerializer; + + public ObjectToInferredTypesConverter_Tests() + { + _jsonSerializer = GetRequiredService(); + } + + [Fact] + public void Test() + { + var objString = _jsonSerializer.Serialize(new object()); + objString.ShouldBe("{}"); + var obj = _jsonSerializer.Deserialize(objString); + obj.ShouldBeOfType(); + + var booleanString = _jsonSerializer.Serialize(true); + booleanString.ShouldBe("true"); + var boolean = _jsonSerializer.Deserialize(booleanString); + boolean.ShouldBe(true); + + var booleanString2 = _jsonSerializer.Serialize(false); + booleanString2.ShouldBe("false"); + var boolean2 = _jsonSerializer.Deserialize(booleanString2); + boolean2.ShouldBe(false); + + var numberString = _jsonSerializer.Serialize(1); + numberString.ShouldBe("1"); + var number = _jsonSerializer.Deserialize(numberString); + number.ShouldBe(1); + + var numberString2 = _jsonSerializer.Serialize(1.1); + numberString2.ShouldBe("1.1"); + var number2 = _jsonSerializer.Deserialize(numberString2); + number2.ShouldBe(1.1); + + var dateString = _jsonSerializer.Serialize(System.DateTime.Parse("2024-01-01")); + dateString.ShouldBe("\"2024-01-01T00:00:00\""); + var date = _jsonSerializer.Deserialize(dateString); + date.ShouldBe(System.DateTime.Parse("2024-01-01")); + + var textString = _jsonSerializer.Serialize("text"); + textString.ShouldBe("\"text\""); + var text = _jsonSerializer.Deserialize(textString); + text.ShouldBe("text"); + } +} diff --git a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs index e5262bc8a2..189c1b8614 100644 --- a/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs +++ b/framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs @@ -196,7 +196,7 @@ public class AbpLocalization_Tests : AbpIntegratedTest +{ + private readonly Volo.Abp.ObjectMapping.IObjectMapper _objectMapper; + + public AbpAutoMapperExtensibleDtoExtensions_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void MapExtraPropertiesTo_Should_Only_Map_Defined_Properties_By_Default() + { + var person = new ExtensibleTestPerson() + .SetProperty("Name", "John") + .SetProperty("Age", 42) + .SetProperty("ChildCount", 2) + .SetProperty("Sex", "male") + .SetProperty("CityName", "Adana"); + + var personDto = new ExtensibleTestPersonDto() + .SetProperty("ExistingDtoProperty", "existing-value"); + + _objectMapper.Map(person, personDto); + + personDto.GetProperty("Name").ShouldBe("John"); //Defined in both classes + personDto.GetProperty("ExistingDtoProperty").ShouldBe("existing-value"); //Should not clear existing values + personDto.GetProperty("ChildCount").ShouldBe(0); //Not defined in the source, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.GetProperty("CityName").ShouldBeNull(); //Ignored, but was set to the default value by ExtensibleTestPersonDto constructor + personDto.HasProperty("Age").ShouldBeFalse(); //Not defined on the destination + personDto.HasProperty("Sex").ShouldBeFalse(); //Not defined in both classes + } + + [Fact] + public void MapExtraProperties_Also_Should_Map_To_RegularProperties() + { + var person = new ExtensibleTestPerson() + .SetProperty("Name", "John") + .SetProperty("Age", 42); + + var personDto = new ExtensibleTestPersonWithRegularPropertiesDto() + .SetProperty("IsActive", true); + + _objectMapper.Map(person, personDto); + + //Defined in both classes + personDto.HasProperty("Name").ShouldBe(false); + personDto.Name.ShouldBe("John"); + + //Defined in both classes + personDto.HasProperty("Age").ShouldBe(false); + personDto.Age.ShouldBe(42); + + //Should not clear existing values + personDto.HasProperty("IsActive").ShouldBe(false); + personDto.IsActive.ShouldBe(true); + } + + [Fact(Skip = "Mapperly requires IHasExtraProperties.ExtraPropertyDictionary to be marked as nullable")] + public void MapExtraPropertiesTo_Should_Ignored_If_ExtraProperties_Is_Null() + { + var person = new ExtensibleTestPerson(); + person.SetExtraPropertiesAsNull(); + + var personDto = new ExtensibleTestPersonDto(); + personDto.SetExtraPropertiesAsNull(); + + Should.NotThrow(() => _objectMapper.Map(person, personDto)); + + person.ExtraProperties.ShouldBe(null); + personDto.ExtraProperties.ShouldBeEmpty(); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj b/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj new file mode 100644 index 0000000000..2ea799a4c9 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo.Abp.Mapperly.Tests.csproj @@ -0,0 +1,17 @@ + + + + + + net9.0 + Volo.Abp.Mapperly.Tests + Volo.Abp.Mapperly.Tests + + + + + + + + + diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs new file mode 100644 index 0000000000..1c07bff75b --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyBeforeAndAfterMethod_Tests.cs @@ -0,0 +1,59 @@ +using Microsoft.Extensions.DependencyInjection; +using Riok.Mapperly.Abstractions; +using Shouldly; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class MyClass +{ + public string Id { get; set; } + + public string Name { get; set; } +} + +public class MyClassDto +{ + public string Id { get; set; } + + public string Name { get; set; } +} + +[Mapper] +public partial class MyClassMapper : MapperBase +{ + public override partial MyClassDto Map(MyClass source); + + public override partial void Map(MyClass source, MyClassDto destination); + + public override void BeforeMap(MyClass source) + { + source.Name = "BeforeMap " + source.Name; + } + + public override void AfterMap(MyClass source, MyClassDto destination) + { + destination.Name = source.Name + " AfterMap"; + } +} + +public class AbpMapperlyBeforeAndAfterMethod_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpMapperlyBeforeAndAfterMethod_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void BeforeAndAfterMethods_Should_Be_Called_When_Mapping() + { + var myClass = new MyClass { Id = "1", Name = "Test" }; + + var myClassDto = _objectMapper.Map(myClass); + myClassDto.Name.ShouldBe("BeforeMap Test AfterMap"); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs new file mode 100644 index 0000000000..b5987769be --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Basic_Tests.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class AbpMapperlyModule_Basic_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpMapperlyModule_Basic_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Replace_IAutoObjectMappingProvider() + { + Assert.True(ServiceProvider.GetRequiredService() is MapperlyAutoObjectMappingProvider); + } + + [Fact] + public void Should_Map_Objects_With_AutoMap_Attributes() + { + var dto = _objectMapper.Map(new MyEntity { Number = 42 }); + dto.Number.ShouldBe(42); + } + + [Fact] + public void Should_Map_Objects_With_Existing_Target_Object() + { + var dto = new MyEntityDto {Id = Guid.Empty, Number = 42}; + + _objectMapper.Map(new MyEntity { Id = Guid.NewGuid(), Number = 43 }, dto); + + dto.Number.ShouldBe(43); + dto.Id.ShouldNotBe(Guid.Empty); + } + + [Fact] + public void Should_Map_Collection() + { + var dto = _objectMapper.Map, List>(new List + { + new MyEntity { Number = 42 }, + new MyEntity { Number = 43 } + }); + + dto.Count.ShouldBe(2); + dto[0].Number.ShouldBe(42); + dto[1].Number.ShouldBe(43); + + var dto2 = _objectMapper.Map, MyEntityDto[]>(new List + { + new MyEntity { Number = 42 }, + new MyEntity { Number = 43 } + }.AsReadOnly()); + + dto2.Length.ShouldBe(2); + dto2[0].Number.ShouldBe(42); + dto2[1].Number.ShouldBe(43); + + var dtoList = new List(); + { + new MyEntityDto() { Number = 44 }; + new MyEntityDto() { Number = 45 }; + } + + _objectMapper.Map, List>(new List + { + new MyEntity { Number = 42 }, + new MyEntity { Number = 43 } + }, dtoList); + + dtoList.Count.ShouldBe(2); + dtoList[0].Number.ShouldBe(42); + dtoList[1].Number.ShouldBe(43); + + var dtoArray = dtoList.ToArray(); + _objectMapper.Map, MyEntityDto[]>(new List + { + new MyEntity { Number = 42 }, + new MyEntity { Number = 43 } + }.AsReadOnly(), dtoArray); + + dtoArray.Length.ShouldBe(2); + dtoArray[0].Number.ShouldBe(42); + dtoArray[1].Number.ShouldBe(43); + } + + [Fact] + public void Should_Map_Enum() + { + var dto = _objectMapper.Map(MyEnum.Value3); + dto.ShouldBe(MyEnumDto.Value2); //Value2 is same as Value3 + } + + [Fact] + public void Should_Throw_Exception_If_Mapper_Is_Not_Found() + { + var exception = Assert.Throws(() =>_objectMapper.Map(new MyEntity())); + exception.Message.ShouldBe("No object mapping was found for the specified source and destination types.\n\n" + + "Mapping attempted:\n" + + "MyEntity -> MyClassDto\n" + + "Volo.Abp.Mapperly.SampleClasses.MyEntity -> Volo.Abp.Mapperly.MyClassDto\n\n" + + "How to fix:\n" + + "Define a mapping class for these types:" + "\n" + + " - Use MapperBase for one-way mapping.\n" + + " - Use TwoWayMapperBase for two-way mapping.\n\n" + + "For details, see the Mapperly integration document https://abp.io/docs/latest/framework/infrastructure/object-to-object-mapping#mapperly-integration"); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs new file mode 100644 index 0000000000..60f4294aeb --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperlyModule_Specific_ObjectMapper_Tests.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class AbpMapperlyModule_Specific_ObjectMapper_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpMapperlyModule_Specific_ObjectMapper_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Use_Specific_Object_Mapper_If_Registered() + { + var dto = _objectMapper.Map(new MyEntity { Number = 42 }); + dto.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + } + + [Fact] + public void Specific_Object_Mapper_Should_Be_Used_For_Collections_If_Registered() + { + // IEnumerable + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destination = new List() + { + new MyEntityDto2 { Number = 44 } + }; + var returnIEnumerable = _objectMapper.Map, IEnumerable>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnIEnumerable.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnIEnumerable).ShouldBeTrue(); + + // ICollection + _objectMapper.Map, ICollection>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnICollection = _objectMapper.Map, ICollection>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnICollection.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnICollection).ShouldBeTrue(); + + // Collection + _objectMapper.Map, Collection>(new Collection() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destination2 = new Collection() + { + new MyEntityDto2 { Number = 44 } + }; + var returnCollection = _objectMapper.Map, Collection>( + new Collection() + { + new MyEntity { Number = 42 } + }, destination2); + returnCollection.First().Number.ShouldBe(43); + ReferenceEquals(destination2, returnCollection).ShouldBeTrue(); + + // IList + _objectMapper.Map, IList>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnIList = _objectMapper.Map, IList>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnIList.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnIList).ShouldBeTrue(); + + // List + _objectMapper.Map, List>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var returnList = _objectMapper.Map, List>( + new List() + { + new MyEntity { Number = 42 } + }, destination); + returnList.First().Number.ShouldBe(43); + ReferenceEquals(destination, returnList).ShouldBeTrue(); + + // Array + _objectMapper.Map(new MyEntity[] + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var destinationArray = new MyEntityDto2[] + { + new MyEntityDto2 { Number = 40 } + }; + var returnArray = _objectMapper.Map(new MyEntity[] + { + new MyEntity { Number = 42 } + }, destinationArray); + + returnArray.First().Number.ShouldBe(43); + + // array should not be changed. Same as Mapperly. + destinationArray.First().Number.ShouldBe(40); + ReferenceEquals(returnArray, destinationArray).ShouldBeFalse(); + } + + [Fact] + public void Specific_Object_Mapper_Should_Support_Multiple_IObjectMapper_Interfaces() + { + var myEntityDto2 = _objectMapper.Map(new MyEntity { Number = 42 }); + myEntityDto2.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + var myEntity = _objectMapper.Map(new MyEntityDto2 { Number = 42 }); + myEntity.Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + // IEnumerable + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntity { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + + _objectMapper.Map, IEnumerable>(new List() + { + new MyEntityDto2 { Number = 42 } + }).First().Number.ShouldBe(43); //MyEntityToMyEntityDto2Mapper adds 1 to number of the source. + } + + [Fact] + public void Should_Use_Destination_Object_Constructor_If_Available() + { + var id = Guid.NewGuid(); + var dto = _objectMapper.Map(new MyEntity { Number = 42, Id = id }); + dto.Key.ShouldBe(id); + dto.No.ShouldBe(42); + } + + [Fact] + public void Should_Use_Destination_Object_MapFrom_Method_If_Available() + { + var id = Guid.NewGuid(); + var dto = new MyEntityDtoWithMappingMethods(); + _objectMapper.Map(new MyEntity { Number = 42, Id = id }, dto); + dto.Key.ShouldBe(id); + dto.No.ShouldBe(42); + } + + [Fact] + public void Should_Use_Source_Object_Method_If_Available_To_Create_New_Object() + { + var id = Guid.NewGuid(); + var entity = _objectMapper.Map(new MyEntityDtoWithMappingMethods { Key = id, No = 42 }); + entity.Id.ShouldBe(id); + entity.Number.ShouldBe(42); + } + + [Fact] + public void Should_Use_Source_Object_Method_If_Available_To_Map_Existing_Object() + { + var id = Guid.NewGuid(); + var entity = new MyEntity(); + _objectMapper.Map(new MyEntityDtoWithMappingMethods { Key = id, No = 42 }, entity); + entity.Id.ShouldBe(id); + entity.Number.ShouldBe(42); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperly_Dependency_Injection_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperly_Dependency_Injection_Tests.cs new file mode 100644 index 0000000000..1aa6288f3e --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpMapperly_Dependency_Injection_Tests.cs @@ -0,0 +1,68 @@ +using System; +using Riok.Mapperly.Abstractions; +using Shouldly; +using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class MyDIClass +{ + public string Id { get; set; } + + public DateTime Birthday { get; set; } +} + +public class MyDIClassDto +{ + public string Id { get; set; } + + public DateTime Birthday { get; set; } +} + +public class BirthdayCalculatorService : ITransientDependency +{ + public DateTime Birthday => DateTime.Parse("2025-01-01"); +} + +[Mapper] +public partial class MyDIClassMapper : MapperBase +{ + private readonly BirthdayCalculatorService _birthdayCalculatorService; + + public MyDIClassMapper(BirthdayCalculatorService birthdayCalculatorService) + { + _birthdayCalculatorService = birthdayCalculatorService; + } + + public override partial MyDIClassDto Map(MyDIClass source); + + public override partial void Map(MyDIClass source, MyDIClassDto destination); + + public override void AfterMap(MyDIClass source, MyDIClassDto destination) + { + destination.Birthday = _birthdayCalculatorService.Birthday; + } +} + +public class AbpMapperly_Dependency_Injection_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + private readonly BirthdayCalculatorService _birthdayCalculatorService; + + public AbpMapperly_Dependency_Injection_Tests() + { + _objectMapper = GetRequiredService(); + _birthdayCalculatorService = GetRequiredService(); + } + + [Fact] + public void DI_Test() + { + var myClass = new MyDIClass { Id = "1", Birthday = DateTime.Now }; + var myClassDto = _objectMapper.Map(myClass); + myClassDto.Birthday.ShouldBe(_birthdayCalculatorService.Birthday); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs new file mode 100644 index 0000000000..5eeebe6b82 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/AbpReverseMapperly_Tests.cs @@ -0,0 +1,86 @@ +using Microsoft.Extensions.DependencyInjection; +using Riok.Mapperly.Abstractions; +using Shouldly; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class MyReverseClass +{ + public string Id { get; set; } + + public string Name { get; set; } +} + +public class MyReverseClassDto +{ + public string Id { get; set; } + + public string Name { get; set; } +} + +[Mapper] +public partial class MyReverseClassMapper : TwoWayMapperBase +{ + public override partial MyReverseClassDto Map(MyReverseClass source); + + public override partial void Map(MyReverseClass source, MyReverseClassDto destination); + + public override partial MyReverseClass ReverseMap(MyReverseClassDto destination); + + public override partial void ReverseMap(MyReverseClassDto destination, MyReverseClass source); + + public override void BeforeReverseMap(MyReverseClassDto destination) + { + destination.Name = "BeforeReverseMap " + destination.Name; + } + + public override void AfterReverseMap(MyReverseClassDto destination, MyReverseClass source) + { + source.Name = destination.Name + " AfterReverseMap"; + } +} + +public class AbpReverseMapperly_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public AbpReverseMapperly_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Map_Test() + { + var myClass = new MyReverseClass { Id = "1", Name = "Test" }; + var myClassDto = _objectMapper.Map(myClass); + myClassDto.Name.ShouldBe("Test"); + + myClass.Id = "2"; + myClass.Name = "Test2"; + + _objectMapper.Map(myClass, myClassDto); + + myClassDto.Id.ShouldBe("2"); + myClassDto.Name.ShouldBe("Test2"); + } + + [Fact] + public void ReverseMap_Test() + { + var myClassDto = new MyReverseClassDto { Id = "1", Name = "Test" }; + var myClass = _objectMapper.Map(myClassDto); + myClass.Name.ShouldBe("BeforeReverseMap Test AfterReverseMap"); + + myClassDto.Id = "2"; + myClassDto.Name = "Test2"; + + _objectMapper.Map(myClassDto, myClass); + + myClass.Id.ShouldBe("2"); + myClass.Name.ShouldBe("BeforeReverseMap Test2 AfterReverseMap"); + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ExtraProperties_Dictionary_Reference_Tests.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ExtraProperties_Dictionary_Reference_Tests.cs new file mode 100644 index 0000000000..e5453d09a3 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/ExtraProperties_Dictionary_Reference_Tests.cs @@ -0,0 +1,158 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Riok.Mapperly.Abstractions; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectMapping; +using Volo.Abp.Testing; +using Xunit; + +namespace Volo.Abp.Mapperly; + +public class ExtraProperties_Dictionary_Reference_Tests : AbpIntegratedTest +{ + private readonly IObjectMapper _objectMapper; + + public ExtraProperties_Dictionary_Reference_Tests() + { + _objectMapper = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Create_New_ExtraProperties_Dictionary_When_Same_Reference() + { + // Arrange: Create a shared ExtraProperties dictionary + var sharedExtraProperties = new ExtraPropertyDictionary + { + {"TestProperty", "TestValue"}, + {"NumberProperty", 42} + }; + + var source = new TestEntityWithExtraProperties + { + Id = Guid.NewGuid(), + Name = "Source Entity" + }; + + var destination = new TestEntityDtoWithExtraProperties + { + Id = Guid.NewGuid(), + Name = "Destination DTO" + }; + + // Make both source and destination reference the same ExtraProperties dictionary + SetExtraPropertiesReference(source, sharedExtraProperties); + SetExtraPropertiesReference(destination, sharedExtraProperties); + + // Verify they have the same reference before mapping + ReferenceEquals(source.ExtraProperties, destination.ExtraProperties).ShouldBeTrue(); + source.ExtraProperties.Count.ShouldBe(2); + destination.ExtraProperties.Count.ShouldBe(2); + + // Act: Perform mapping + _objectMapper.Map(source, destination); + + // Assert: After mapping, they should have different references + // This is the key fix: when ExtraProperties references are the same, + // a new dictionary should be created for the destination + ReferenceEquals(source.ExtraProperties, destination.ExtraProperties).ShouldBeFalse(); + + // But content should be preserved + destination.ExtraProperties["TestProperty"].ShouldBe("TestValue"); + destination.ExtraProperties["NumberProperty"].ShouldBe(42); + destination.ExtraProperties.Count.ShouldBe(source.ExtraProperties.Count); + } + + [Fact] + public void Should_Not_Create_New_Dictionary_When_Different_References() + { + // Arrange: Create source and destination with different ExtraProperties references + var source = new TestEntityWithExtraProperties + { + Id = Guid.NewGuid(), + Name = "Source Entity" + }; + source.SetProperty("SourceProperty", "SourceValue"); + + var destination = new TestEntityDtoWithExtraProperties + { + Id = Guid.NewGuid(), + Name = "Destination DTO" + }; + destination.SetProperty("DestinationProperty", "DestinationValue"); + + var originalSourceReference = source.ExtraProperties; + + // Verify they have different references before mapping + ReferenceEquals(source.ExtraProperties, destination.ExtraProperties).ShouldBeFalse(); + + // Act: Perform mapping + _objectMapper.Map(source, destination); + + // Assert: Source reference should remain unchanged + ReferenceEquals(source.ExtraProperties, originalSourceReference).ShouldBeTrue(); + + // Destination reference may change due to normal mapping process, but should not be same as source + ReferenceEquals(source.ExtraProperties, destination.ExtraProperties).ShouldBeFalse(); + + destination.ExtraProperties["SourceProperty"].ShouldBe("SourceValue"); + destination.ExtraProperties["DestinationProperty"].ShouldBe("DestinationValue"); + } + + [Fact] + public void Should_Handle_Readonly_ExtraProperties_Gracefully() + { + // Arrange: Create entities with readonly ExtraProperties + var source = new TestEntityWithReadonlyExtraProperties + { + Id = Guid.NewGuid(), + Name = "Source Entity" + }; + source.ExtraProperties.Add("TestProperty", "TestValue"); + + var destination = new TestEntityWithReadonlyExtraProperties + { + Id = Guid.NewGuid(), + Name = "Destination Entity" + }; + + // Make them reference the same ExtraProperties + var sharedExtraProperties = new ExtraPropertyDictionary + { + {"SharedProperty", "SharedValue"} + }; + SetReadonlyExtraPropertiesReference(source, sharedExtraProperties); + SetReadonlyExtraPropertiesReference(destination, sharedExtraProperties); + + // Verify they have the same reference + ReferenceEquals(source.ExtraProperties, destination.ExtraProperties).ShouldBeTrue(); + + // Act & Assert: Should not throw exception even if setter is not available + Should.NotThrow(() => _objectMapper.Map(source, destination)); + } + + private static void SetExtraPropertiesReference(TestEntityWithExtraProperties entity, ExtraPropertyDictionary extraProperties) + { + // Use reflection to set the protected setter from ExtensibleObject + var propertyInfo = typeof(ExtensibleObject).GetProperty(nameof(ExtensibleObject.ExtraProperties)); + propertyInfo?.SetValue(entity, extraProperties); + } + + private static void SetExtraPropertiesReference(TestEntityDtoWithExtraProperties entity, ExtraPropertyDictionary extraProperties) + { + // Use reflection to set the protected setter from ExtensibleObject + var propertyInfo = typeof(ExtensibleObject).GetProperty(nameof(ExtensibleObject.ExtraProperties)); + propertyInfo?.SetValue(entity, extraProperties); + } + + private static void SetReadonlyExtraPropertiesReference(TestEntityWithReadonlyExtraProperties entity, ExtraPropertyDictionary extraProperties) + { + // Use reflection to set the private field + var fieldInfo = typeof(TestEntityWithReadonlyExtraProperties).GetField("_extraProperties", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + fieldInfo?.SetValue(entity, extraProperties); + } +} + diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs new file mode 100644 index 0000000000..8ff5ca2eb1 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/MapperlyTestModule.cs @@ -0,0 +1,13 @@ +using Volo.Abp.Modularity; +using Volo.Abp.ObjectExtending; + +namespace Volo.Abp.Mapperly; + +[DependsOn( + typeof(AbpMapperlyModule), + typeof(AbpObjectExtendingTestModule) +)] +public class MapperlyTestModule : AbpModule +{ + +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs new file mode 100644 index 0000000000..eb11d16be1 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MapperlyMappers.cs @@ -0,0 +1,85 @@ +using System; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Data; +using Volo.Abp.Mapperly; +using Volo.Abp.Mapperly.SampleClasses; +using Volo.Abp.ObjectExtending; +using Volo.Abp.ObjectExtending.TestObjects; + +[Mapper] +public partial class MyEntityMapper : MapperBase +{ + public override partial MyEntityDto Map(MyEntity source); + + public override partial void Map(MyEntity source, MyEntityDto destination); +} + +[Mapper] +public partial class MyEnumMapper : MapperBase +{ + public override partial MyEnumDto Map(MyEnum source); + + public override void Map(MyEnum source, MyEnumDto destination) + { + destination = Map(source); + } +} + +[Mapper] +[MapExtraProperties(IgnoredProperties = ["CityName"])] +public partial class ExtensibleTestPersonMapper : MapperBase +{ + public override partial ExtensibleTestPersonDto Map(ExtensibleTestPerson source); + + public override partial void Map(ExtensibleTestPerson source, ExtensibleTestPersonDto destination); +} + +[Mapper] +[MapExtraProperties(MapToRegularProperties = true)] +public partial class ExtensibleTestPersonWithRegularPropertiesDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Name))] + [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.Age))] + [MapperIgnoreTarget(nameof(ExtensibleTestPersonWithRegularPropertiesDto.IsActive))] + public override partial ExtensibleTestPersonWithRegularPropertiesDto Map(ExtensibleTestPerson source); + + public override partial void Map(ExtensibleTestPerson source, ExtensibleTestPersonWithRegularPropertiesDto destination); +} + +// Test entities for ExtraProperties dictionary reference tests +public class TestEntityWithExtraProperties : ExtensibleObject +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +public class TestEntityDtoWithExtraProperties : ExtensibleObject +{ + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; +} + +public class TestEntityWithReadonlyExtraProperties : IHasExtraProperties +{ + private readonly ExtraPropertyDictionary _extraProperties = new(); + + public Guid Id { get; set; } + public string Name { get; set; } = string.Empty; + public ExtraPropertyDictionary ExtraProperties => _extraProperties; +} + +[Mapper] +public partial class TestEntityWithExtraPropertiesMapper : MapperBase +{ + public override partial TestEntityDtoWithExtraProperties Map(TestEntityWithExtraProperties source); + + public override partial void Map(TestEntityWithExtraProperties source, TestEntityDtoWithExtraProperties destination); +} + +[Mapper] +public partial class TestEntityWithReadonlyExtraPropertiesMapper : MapperBase +{ + public override partial TestEntityWithReadonlyExtraProperties Map(TestEntityWithReadonlyExtraProperties source); + + public override partial void Map(TestEntityWithReadonlyExtraProperties source, TestEntityWithReadonlyExtraProperties destination); +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs new file mode 100644 index 0000000000..f137e476f1 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntity.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyEntity +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs new file mode 100644 index 0000000000..7630b2d14e --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyEntityDto +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs new file mode 100644 index 0000000000..9d00eff9c4 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDto2.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyEntityDto2 +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs new file mode 100644 index 0000000000..bf8c774599 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityDtoWithMappingMethods.cs @@ -0,0 +1,43 @@ +using System; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly.SampleClasses; + +//TODO: Move tests to Volo.Abp.ObjectMapping test project +public class MyEntityDtoWithMappingMethods : IMapFrom, IMapTo +{ + public Guid Key { get; set; } + + public int No { get; set; } + + public MyEntityDtoWithMappingMethods() + { + + } + + public MyEntityDtoWithMappingMethods(MyEntity entity) + { + MapFrom(entity); + } + + public void MapFrom(MyEntity source) + { + Key = source.Id; + No = source.Number; + } + + MyEntity IMapTo.MapTo() + { + return new MyEntity + { + Id = Key, + Number = No + }; + } + + void IMapTo.MapTo(MyEntity destination) + { + destination.Id = Key; + destination.Number = No; + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs new file mode 100644 index 0000000000..cd73c98747 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEntityToMyEntityDto2Mapper.cs @@ -0,0 +1,39 @@ +using Volo.Abp.DependencyInjection; +using Volo.Abp.ObjectMapping; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyEntityToMyEntityDto2Mapper : IObjectMapper, IObjectMapper, ITransientDependency +{ + public MyEntityDto2 Map(MyEntity source) + { + return new MyEntityDto2 + { + Id = source.Id, + Number = source.Number + 1 + }; + } + + public MyEntityDto2 Map(MyEntity source, MyEntityDto2 destination) + { + destination.Id = source.Id; + destination.Number = source.Number + 1; + return destination; + } + + public MyEntity Map(MyEntityDto2 source) + { + return new MyEntity + { + Id = source.Id, + Number = source.Number + 1 + }; + } + + public MyEntity Map(MyEntityDto2 source, MyEntity destination) + { + destination.Id = source.Id; + destination.Number = source.Number + 1; + return destination; + } +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs new file mode 100644 index 0000000000..fe2b72300c --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnum.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.Mapperly.SampleClasses; + +public enum MyEnum +{ + Value1 = 1, + Value2, + Value3 +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs new file mode 100644 index 0000000000..40d28d0501 --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyEnumDto.cs @@ -0,0 +1,9 @@ +namespace Volo.Abp.Mapperly.SampleClasses; + +public enum MyEnumDto +{ + Value1 = 2, + Value2, + Value3, + Value +} diff --git a/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs new file mode 100644 index 0000000000..9fd2c556fb --- /dev/null +++ b/framework/test/Volo.Abp.Mapperly.Tests/Volo/Abp/Mapperly/SampleClasses/MyNotMappedDto.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Mapperly.SampleClasses; + +public class MyNotMappedDto +{ + public Guid Id { get; set; } + + public int Number { get; set; } +} diff --git a/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/FallbackTenantResolveContributor_Tests.cs b/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/FallbackTenantResolveContributor_Tests.cs new file mode 100644 index 0000000000..00bf669aaf --- /dev/null +++ b/framework/test/Volo.Abp.MultiTenancy.Tests/Volo/Abp/MultiTenancy/FallbackTenantResolveContributor_Tests.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Shouldly; +using Volo.Abp.MultiTenancy.ConfigurationStore; +using Xunit; + +namespace Volo.Abp.MultiTenancy; + +public class FallbackTenantResolveContributor_Tests : MultiTenancyTestBase +{ + private readonly Guid _testTenantId = Guid.NewGuid(); + private readonly string _testTenantName = "acme"; + private readonly string _testTenantNormalizedName = "ACME"; + + private readonly AbpTenantResolveOptions _options; + private readonly ITenantResolver _tenantResolver; + + public FallbackTenantResolveContributor_Tests() + { + _options = ServiceProvider.GetRequiredService>().Value; + _tenantResolver = ServiceProvider.GetRequiredService(); + } + + protected override void BeforeAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.Tenants = new[] + { + new TenantConfiguration(_testTenantId, _testTenantName, _testTenantNormalizedName) + }; + }); + + services.Configure(options => + { + options.FallbackTenant = _testTenantName; + }); + } + + [Fact] + public async Task Should_Resolve_To_Fallback_Tenant_If_No_Other_Contributor_Succeeds() + { + var result = await _tenantResolver.ResolveTenantIdOrNameAsync(); + + result.TenantIdOrName.ShouldBe(_testTenantName); + result.AppliedResolvers.ShouldContain(TenantResolverNames.FallbackTenant); + } + + [Fact] + public async Task Should_Not_Override_Resolved_Tenant() + { + // Arrange + var customTenantName = "resolved-tenant"; + _options.TenantResolvers.Insert(0, new TestTenantResolveContributor(customTenantName)); + + // Act + var result = await _tenantResolver.ResolveTenantIdOrNameAsync(); + + // Assert + result.TenantIdOrName.ShouldBe(customTenantName); + result.AppliedResolvers.First().ShouldBe("Test"); + result.AppliedResolvers.ShouldNotContain(TenantResolverNames.FallbackTenant); + } + + public class TestTenantResolveContributor : TenantResolveContributorBase + { + private readonly string _tenant; + + public TestTenantResolveContributor(string tenant) + { + _tenant = tenant; + } + + public override string Name => "Test"; + + public override Task ResolveAsync(ITenantResolveContext context) + { + context.TenantIdOrName = _tenant; + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/TestSharedTypeEntity.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/TestSharedTypeEntity.cs new file mode 100644 index 0000000000..5b1d94a316 --- /dev/null +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Domain/TestSharedTypeEntity.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.TestApp.Domain; + +public class TestSharedEntity : AggregateRoot, IMultiTenant, ISoftDelete +{ + private readonly Dictionary _dynamicPropertites = new(); + + public object this[string key] + { + get => _dynamicPropertites.GetValueOrDefault(key); + set => _dynamicPropertites[key] = value; + } + + public Guid? TenantId { get; set; } + + public virtual string Name { get; set; } + + public virtual int Age { get; set; } + + public virtual DateTime? Birthday { get; set; } + + public bool IsDeleted { get; set; } + + public TestSharedEntity() + { + + } + + public TestSharedEntity(Guid id) + : base(id) + { + + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs index 679590ac31..5e321736d4 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityCache_Tests.cs @@ -37,8 +37,8 @@ public abstract class EntityCache_Tests : TestAppTestBase(() => ProductEntityCache.GetAsync(notExistId)); - await Assert.ThrowsAsync(() => ProductCacheItem.GetAsync(notExistId)); + await Assert.ThrowsAsync>(() => ProductEntityCache.GetAsync(notExistId)); + await Assert.ThrowsAsync>(() => ProductCacheItem.GetAsync(notExistId)); } [Fact] diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/RepositoryExtensions_Tests.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/RepositoryExtensions_Tests.cs index 747c53e333..53ef423378 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/RepositoryExtensions_Tests.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/RepositoryExtensions_Tests.cs @@ -24,10 +24,10 @@ public abstract class RepositoryExtensions_Tests : TestAppTestBa await WithUnitOfWorkAsync(async () => { var id = Guid.NewGuid(); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync>(async () => await PersonRepository.EnsureExistsAsync(Guid.NewGuid()) ); - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync>(async () => await PersonRepository.EnsureExistsAsync(x => x.Id == id) ); diff --git a/latest-versions.json b/latest-versions.json index 7e756d726b..c09373e176 100644 --- a/latest-versions.json +++ b/latest-versions.json @@ -1,4 +1,58 @@ [ + { + "version": "9.3.3", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "4.3.3" + } + }, + { + "version": "9.3.2", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "4.3.2" + } + }, + { + "version": "9.3.1", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "4.3.1" + } + }, + { + "version": "9.2.3", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "4.2.3" + } + }, + { + "version": "9.2.2", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "4.2.2" + } + }, + { + "version": "9.2.1", + "releaseDate": "", + "type": "stable", + "message": "", + "leptonx": { + "version": "4.2.1" + } + }, { "version": "9.2.0", "releaseDate": "", diff --git a/modules/README.md b/modules/README.md index 670d60c1cc..4eea191640 100644 --- a/modules/README.md +++ b/modules/README.md @@ -1,3 +1,5 @@ ## ABP Free Modules -These modules are free & open source. \ No newline at end of file +These modules are free & open source. + +For detailed information about module architecture and installer projects, see the [Module Installer Projects documentation](../docs/en/framework/architecture/modularity/installer-projects.md). \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj index d8c149f8b5..b4530a5dd7 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj +++ b/modules/account/src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.csproj @@ -21,6 +21,7 @@ + diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs new file mode 100644 index 0000000000..20cfd1753e --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationMappers.cs @@ -0,0 +1,21 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Identity; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.Account; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityUserToProfileDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(ProfileDto.HasPassword))] + public override partial ProfileDto Map(IdentityUser source); + + [MapperIgnoreTarget(nameof(ProfileDto.HasPassword))] + public override partial void Map(IdentityUser source, ProfileDto destination); + + public override void AfterMap(IdentityUser source, ProfileDto destination) + { + destination.HasPassword = source.PasswordHash != null; + } +} \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs index 283e06a934..9291cae3fd 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModule.cs @@ -1,4 +1,5 @@ -using Volo.Abp.AutoMapper; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Mapperly; using Volo.Abp.Emailing; using Volo.Abp.Identity; using Volo.Abp.Modularity; @@ -12,7 +13,8 @@ namespace Volo.Abp.Account; typeof(AbpAccountApplicationContractsModule), typeof(AbpIdentityApplicationModule), typeof(AbpUiNavigationModule), - typeof(AbpEmailingModule) + typeof(AbpEmailingModule), + typeof(AbpMapperlyModule) )] public class AbpAccountApplicationModule : AbpModule { @@ -23,14 +25,11 @@ public class AbpAccountApplicationModule : AbpModule options.FileSets.AddEmbedded(); }); - Configure(options => - { - options.AddProfile(validate: true); - }); - Configure(options => { options.Applications["MVC"].Urls[AccountUrlNames.PasswordReset] = "Account/ResetPassword"; }); + + context.Services.AddMapperlyObjectMapper(); } } diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs deleted file mode 100644 index 0c117d98d0..0000000000 --- a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AbpAccountApplicationModuleAutoMapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; -using Volo.Abp.Identity; - -namespace Volo.Abp.Account; - -public class AbpAccountApplicationModuleAutoMapperProfile : Profile -{ - public AbpAccountApplicationModuleAutoMapperProfile() - { - CreateMap() - .ForMember(dest => dest.HasPassword, - op => op.MapFrom(src => src.PasswordHash != null)) - .MapExtraProperties(); - } -} diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountAppService.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountAppService.cs index cc7cdd8c83..09f78f2851 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountAppService.cs +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/AccountAppService.cs @@ -33,6 +33,7 @@ public class AccountAppService : ApplicationService, IAccountAppService IdentityOptions = identityOptions; LocalizationResource = typeof(AccountResource); + ObjectMapperContext = typeof(AbpAccountApplicationModule); } public virtual async Task RegisterAsync(RegisterDto input) diff --git a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs deleted file mode 100644 index af59d75321..0000000000 --- a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,20 +0,0 @@ -using AutoMapper; -using Volo.Abp.Account.Blazor.Pages.Account; -using Volo.Abp.AutoMapper; -using Volo.Abp.Identity; - -namespace Volo.Abp.Account.Blazor; - -public class AbpAccountBlazorAutoMapperProfile : Profile -{ - public AbpAccountBlazorAutoMapperProfile() - { - CreateMap() - .MapExtraProperties() - .Ignore(x => x.PhoneNumberConfirmed) - .Ignore(x => x.EmailConfirmed); - - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs new file mode 100644 index 0000000000..f3079ae8d5 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorMappers.cs @@ -0,0 +1,27 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Account.Blazor.Pages.Account; +using Volo.Abp.Mapperly; +using Volo.Abp.Identity; + +namespace Volo.Abp.Account.Blazor; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class ProfileDtoToPersonalInfoModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(PersonalInfoModel.PhoneNumberConfirmed))] + [MapperIgnoreTarget(nameof(PersonalInfoModel.EmailConfirmed))] + public override partial PersonalInfoModel Map(ProfileDto source); + + [MapperIgnoreTarget(nameof(PersonalInfoModel.PhoneNumberConfirmed))] + [MapperIgnoreTarget(nameof(PersonalInfoModel.EmailConfirmed))] + public override partial void Map(ProfileDto source, PersonalInfoModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class PersonalInfoModelToUpdateProfileDtoMapper : MapperBase +{ + public override partial UpdateProfileDto Map(PersonalInfoModel source); + public override partial void Map(PersonalInfoModel source, UpdateProfileDto destination); +} \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs index 61b42e1807..6a44f156ee 100644 --- a/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs +++ b/modules/account/src/Volo.Abp.Account.Blazor/AbpAccountBlazorModule.cs @@ -2,7 +2,7 @@ using Volo.Abp.Account.Blazor.Pages.Account; using Volo.Abp.AspNetCore.Components.Web.Theming; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.ObjectExtending; using Volo.Abp.ObjectExtending.Modularity; @@ -13,21 +13,16 @@ namespace Volo.Abp.Account.Blazor; [DependsOn( typeof(AbpAspNetCoreComponentsWebThemingModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAccountApplicationContractsModule) )] public class AbpAccountBlazorModule : AbpModule { private readonly static OneTimeRunner OneTimeRunner = new OneTimeRunner(); - + public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { @@ -39,7 +34,7 @@ public class AbpAccountBlazorModule : AbpModule options.AdditionalAssemblies.Add(typeof(AbpAccountBlazorModule).Assembly); }); } - + public override void PostConfigureServices(ServiceConfigurationContext context) { OneTimeRunner.Run(() => diff --git a/modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj b/modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj index 0eaa3cd978..7590216d06 100644 --- a/modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj +++ b/modules/account/src/Volo.Abp.Account.Blazor/Volo.Abp.Account.Blazor.csproj @@ -10,7 +10,7 @@ - + diff --git a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs index b8ca910eba..9e3fff950b 100644 --- a/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs +++ b/modules/account/src/Volo.Abp.Account.Web.IdentityServer/Pages/Account/IdentityServerSupportedLoginModel.cs @@ -1,4 +1,4 @@ -using IdentityModel; +using Duende.IdentityModel; using IdentityServer4.Events; using IdentityServer4.Models; using IdentityServer4.Services; diff --git a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs deleted file mode 100644 index cdc92e8b19..0000000000 --- a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebAutomapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Volo.Abp.Account.Web.Pages.Account; -using Volo.Abp.Identity; -using AutoMapper; -using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo; - -namespace Volo.Abp.Account.Web; - -public class AbpAccountWebAutoMapperProfile : Profile -{ - public AbpAccountWebAutoMapperProfile() - { - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs new file mode 100644 index 0000000000..e352b14ac6 --- /dev/null +++ b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebMappers.cs @@ -0,0 +1,13 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Account.Web.Pages.Account.Components.ProfileManagementGroup.PersonalInfo; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.Account.Web; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class ProfileDtoToPersonalInfoModelMapper : MapperBase +{ + public override partial AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel Map(ProfileDto source); + public override partial void Map(ProfileDto source, AccountProfilePersonalInfoManagementGroupViewComponent.PersonalInfoModel destination); +} \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs index 1b41deaff7..f246beed84 100644 --- a/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs +++ b/modules/account/src/Volo.Abp.Account.Web/AbpAccountWebModule.cs @@ -8,7 +8,7 @@ using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.ExceptionHandling; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Identity.AspNetCore; @@ -24,14 +24,14 @@ namespace Volo.Abp.Account.Web; [DependsOn( typeof(AbpAccountApplicationContractsModule), typeof(AbpIdentityAspNetCoreModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpExceptionHandlingModule) )] public class AbpAccountWebModule : AbpModule { private readonly static OneTimeRunner OneTimeRunner = new OneTimeRunner(); - + public override void PreConfigureServices(ServiceConfigurationContext context) { context.Services.PreConfigure(options => @@ -59,11 +59,7 @@ public class AbpAccountWebModule : AbpModule ConfigureProfileManagementPage(); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { @@ -96,7 +92,7 @@ public class AbpAccountWebModule : AbpModule }); } - + public override void PostConfigureServices(ServiceConfigurationContext context) { OneTimeRunner.Run(() => diff --git a/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj b/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj index efedb773fe..3f52eae0a4 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj +++ b/modules/account/src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.csproj @@ -39,7 +39,7 @@ - + diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo.Abp.BackgroundJobs.Domain.csproj b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo.Abp.BackgroundJobs.Domain.csproj index 9a23652a5a..2a35994fe6 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo.Abp.BackgroundJobs.Domain.csproj +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo.Abp.BackgroundJobs.Domain.csproj @@ -10,7 +10,7 @@ - + diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/AbpBackgroundJobsDomainModule.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/AbpBackgroundJobsDomainModule.cs index 1a551ef7c4..3342deb794 100644 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/AbpBackgroundJobsDomainModule.cs +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/AbpBackgroundJobsDomainModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; namespace Volo.Abp.BackgroundJobs; @@ -7,16 +7,13 @@ namespace Volo.Abp.BackgroundJobs; [DependsOn( typeof(AbpBackgroundJobsDomainSharedModule), typeof(AbpBackgroundJobsModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class AbpBackgroundJobsDomainModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); + } } diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainAutoMapperProfile.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainAutoMapperProfile.cs deleted file mode 100644 index 67891f4f91..0000000000 --- a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainAutoMapperProfile.cs +++ /dev/null @@ -1,17 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; - -namespace Volo.Abp.BackgroundJobs; - -public class BackgroundJobsDomainAutoMapperProfile : Profile -{ - public BackgroundJobsDomainAutoMapperProfile() - { - CreateMap() - .ConstructUsing(x => new BackgroundJobRecord(x.Id)) - .Ignore(record => record.ConcurrencyStamp) - .Ignore(record => record.ExtraProperties); - - CreateMap(); - } -} diff --git a/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainMapperlyMappers.cs b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainMapperlyMappers.cs new file mode 100644 index 0000000000..9ac5cef9bf --- /dev/null +++ b/modules/background-jobs/src/Volo.Abp.BackgroundJobs.Domain/Volo/Abp/BackgroundJobs/BackgroundJobsDomainMapperlyMappers.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.BackgroundJobs; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BackgroundJobInfoToBackgroundJobRecordMapper + : MapperBase +{ + [MapperIgnoreTarget(nameof(BackgroundJobRecord.ConcurrencyStamp))] + [MapperIgnoreTarget(nameof(BackgroundJobRecord.ExtraProperties))] + public override partial BackgroundJobRecord Map(BackgroundJobInfo source); + + [MapperIgnoreTarget(nameof(BackgroundJobRecord.ConcurrencyStamp))] + [MapperIgnoreTarget(nameof(BackgroundJobRecord.ExtraProperties))] + public override partial void Map(BackgroundJobInfo source, BackgroundJobRecord destination); + + [ObjectFactory] + protected BackgroundJobRecord CreateBackgroundJobRecord(BackgroundJobInfo source) + { + return new BackgroundJobRecord(source.Id); + } +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BackgroundJobRecordToBackgroundJobInfoMapper + : MapperBase +{ + public override partial BackgroundJobInfo Map(BackgroundJobRecord source); + + public override partial void Map(BackgroundJobRecord source, BackgroundJobInfo destination); +} diff --git a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo.Blogging.Admin.Application.abppkg.analyze.json b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo.Blogging.Admin.Application.abppkg.analyze.json index 473750d55d..58e8a82dd7 100644 --- a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo.Blogging.Admin.Application.abppkg.analyze.json +++ b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo.Blogging.Admin.Application.abppkg.analyze.json @@ -21,9 +21,9 @@ "name": "AbpCachingModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Abp.Ddd.Application", diff --git a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo.Blogging.Admin.Application.csproj b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo.Blogging.Admin.Application.csproj index 45863cd86c..aea034ba35 100644 --- a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo.Blogging.Admin.Application.csproj +++ b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo.Blogging.Admin.Application.csproj @@ -17,7 +17,7 @@ - + diff --git a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationAutoMapperProfile.cs b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationAutoMapperProfile.cs deleted file mode 100644 index 378fc46132..0000000000 --- a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; -using Volo.Blogging.Admin.Blogs; -using Volo.Blogging.Blogs; -using Volo.Blogging.Blogs.Dtos; - -namespace Volo.Blogging.Admin -{ - public class BloggingAdminApplicationAutoMapperProfile : Profile - { - public BloggingAdminApplicationAutoMapperProfile() - { - CreateMap(); - } - } -} diff --git a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationMappers.cs b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationMappers.cs new file mode 100644 index 0000000000..2999a3c480 --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Blogging.Blogs; +using Volo.Blogging.Blogs.Dtos; + +namespace Volo.Blogging.Admin; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogToBlogDtoMapper : MapperBase +{ + public override partial BlogDto Map(Blog source); + + public override partial void Map(Blog source, BlogDto destination); +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationModule.cs b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationModule.cs index c9fcbbaf0a..77ec9aa552 100644 --- a/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationModule.cs +++ b/modules/blogging/src/Volo.Blogging.Admin.Application/Volo/Blogging/Admin/BloggingAdminApplicationModule.cs @@ -1,11 +1,8 @@ -using Microsoft.AspNetCore.Authorization; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Application; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Modularity; -using Volo.Blogging.Comments; -using Volo.Blogging.Posts; namespace Volo.Blogging.Admin { @@ -13,18 +10,14 @@ namespace Volo.Blogging.Admin typeof(BloggingDomainModule), typeof(BloggingAdminApplicationContractsModule), typeof(AbpCachingModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpDddApplicationModule) )] public class BloggingAdminApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } } diff --git a/modules/blogging/src/Volo.Blogging.Admin.Web/AbpBloggingAdminWebAutoMapperProfile.cs b/modules/blogging/src/Volo.Blogging.Admin.Web/AbpBloggingAdminWebAutoMapperProfile.cs deleted file mode 100644 index 35f83cd480..0000000000 --- a/modules/blogging/src/Volo.Blogging.Admin.Web/AbpBloggingAdminWebAutoMapperProfile.cs +++ /dev/null @@ -1,18 +0,0 @@ -using AutoMapper; -using Volo.Blogging.Admin.Blogs; -using Volo.Blogging.Admin.Pages.Blogging.Admin.Blogs; -using Volo.Blogging.Blogs; -using Volo.Blogging.Blogs.Dtos; -using EditModel = Volo.Blogging.Admin.Pages.Blogging.Admin.Blogs.EditModel; - -namespace Volo.Blogging.Admin -{ - public class AbpBloggingAdminWebAutoMapperProfile : Profile - { - public AbpBloggingAdminWebAutoMapperProfile() - { - CreateMap(); - CreateMap(); - } - } -} diff --git a/modules/blogging/src/Volo.Blogging.Admin.Web/AbpBloggingAdminWebMappers.cs b/modules/blogging/src/Volo.Blogging.Admin.Web/AbpBloggingAdminWebMappers.cs new file mode 100644 index 0000000000..30a2518050 --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Admin.Web/AbpBloggingAdminWebMappers.cs @@ -0,0 +1,24 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Blogging.Admin.Blogs; +using Volo.Blogging.Admin.Pages.Blogging.Admin.Blogs; +using Volo.Blogging.Blogs.Dtos; +using EditModel = Volo.Blogging.Admin.Pages.Blogging.Admin.Blogs.EditModel; + +namespace Volo.Blogging.Admin; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogDtoToBlogEditViewModelMapper : MapperBase +{ + public override partial EditModel.BlogEditViewModel Map(BlogDto source); + + public override partial void Map(BlogDto source, EditModel.BlogEditViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogCreateModalViewToCreateBlogDtoMapper : MapperBase +{ + public override partial CreateBlogDto Map(CreateModel.BlogCreateModalView source); + + public override partial void Map(CreateModel.BlogCreateModalView source, CreateBlogDto destination); +} diff --git a/modules/blogging/src/Volo.Blogging.Admin.Web/BloggingAdminWebModule.cs b/modules/blogging/src/Volo.Blogging.Admin.Web/BloggingAdminWebModule.cs index 507745b386..79e933465c 100644 --- a/modules/blogging/src/Volo.Blogging.Admin.Web/BloggingAdminWebModule.cs +++ b/modules/blogging/src/Volo.Blogging.Admin.Web/BloggingAdminWebModule.cs @@ -2,7 +2,7 @@ using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Modularity; using Volo.Abp.UI.Navigation; @@ -15,7 +15,7 @@ namespace Volo.Blogging.Admin typeof(BloggingAdminApplicationContractsModule), typeof(AbpAspNetCoreMvcUiBootstrapModule), typeof(AbpAspNetCoreMvcUiBundlingModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class BloggingAdminWebModule : AbpModule { @@ -34,6 +34,8 @@ namespace Volo.Blogging.Admin public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddMapperlyObjectMapper(); + Configure(options => { options.MenuContributors.Add(new BloggingAdminMenuContributor()); @@ -44,12 +46,6 @@ namespace Volo.Blogging.Admin options.FileSets.AddEmbedded(); }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); - Configure(options => { options.DisableModule(BloggingAdminRemoteServiceConsts.ModuleName); diff --git a/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.abppkg.analyze.json b/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.abppkg.analyze.json index bfadd4006b..42604b9be1 100644 --- a/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.abppkg.analyze.json +++ b/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.abppkg.analyze.json @@ -21,9 +21,9 @@ "name": "AbpAspNetCoreMvcUiBundlingModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" } ], "implementingInterfaces": [ diff --git a/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.csproj b/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.csproj index 04df5c550d..89b19e0be5 100644 --- a/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.csproj +++ b/modules/blogging/src/Volo.Blogging.Admin.Web/Volo.Blogging.Admin.Web.csproj @@ -18,7 +18,7 @@ - + diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.abppkg.analyze.json b/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.abppkg.analyze.json index b332019e6d..417f7e0bd4 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.abppkg.analyze.json +++ b/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.abppkg.analyze.json @@ -21,9 +21,9 @@ "name": "AbpCachingModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Abp.BlobStoring", diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj b/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj index 5119993b9c..08d01780d6 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj +++ b/modules/blogging/src/Volo.Blogging.Application/Volo.Blogging.Application.csproj @@ -17,7 +17,7 @@ - + diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationAutoMapperProfile.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationAutoMapperProfile.cs deleted file mode 100644 index 71e90070cf..0000000000 --- a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,33 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Blogging.Blogs; -using Volo.Blogging.Blogs.Dtos; -using Volo.Blogging.Comments; -using Volo.Blogging.Comments.Dtos; -using Volo.Blogging.Posts; -using Volo.Blogging.Tagging; -using Volo.Blogging.Tagging.Dtos; -using Volo.Blogging.Users; - -namespace Volo.Blogging -{ - public class BloggingApplicationAutoMapperProfile : Profile - { - public BloggingApplicationAutoMapperProfile() - { - CreateMap(); - CreateMap(); - CreateMap().Ignore(x=>x.Writer).Ignore(x=>x.CommentCount).Ignore(x=>x.Tags); - CreateMap().Ignore(x => x.Writer); - CreateMap(); - CreateMap().Ignore(x=>x.CommentCount).Ignore(x=>x.Tags); - CreateMap() - .IgnoreModificationAuditedObjectProperties() - .IgnoreDeletionAuditedObjectProperties() - .Ignore(x => x.ConcurrencyStamp) - .Ignore(x => x.Writer) - .Ignore(x => x.CommentCount) - .Ignore(x => x.Tags); - } - } -} diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationMappers.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationMappers.cs new file mode 100644 index 0000000000..28bf339526 --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationMappers.cs @@ -0,0 +1,98 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Blogging.Blogs; +using Volo.Blogging.Blogs.Dtos; +using Volo.Blogging.Comments; +using Volo.Blogging.Comments.Dtos; +using Volo.Blogging.Posts; +using Volo.Blogging.Tagging; +using Volo.Blogging.Tagging.Dtos; +using Volo.Blogging.Users; + +namespace Volo.Blogging; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PostCacheItemToPostWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(PostWithDetailsDto.LastModificationTime))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.LastModifierId))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.IsDeleted))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.DeletionTime))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.DeleterId))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.ConcurrencyStamp))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.Writer))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.CommentCount))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.Tags))] + public override partial PostWithDetailsDto Map(PostCacheItem source); + + [MapperIgnoreTarget(nameof(PostWithDetailsDto.LastModificationTime))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.LastModifierId))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.IsDeleted))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.DeletionTime))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.DeleterId))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.ConcurrencyStamp))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.Writer))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.CommentCount))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.Tags))] + public override partial void Map(PostCacheItem source, PostWithDetailsDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PostToPostCacheItemMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(PostCacheItem.CommentCount))] + [MapperIgnoreTarget(nameof(PostCacheItem.Tags))] + public override partial PostCacheItem Map(Post source); + + [MapperIgnoreTarget(nameof(PostCacheItem.CommentCount))] + [MapperIgnoreTarget(nameof(PostCacheItem.Tags))] + public override partial void Map(Post source, PostCacheItem destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class CommentToCommentWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(CommentWithDetailsDto.Writer))] + public override partial CommentWithDetailsDto Map(Comment source); + + [MapperIgnoreTarget(nameof(CommentWithDetailsDto.Writer))] + public override partial void Map(Comment source, CommentWithDetailsDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PostToPostWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(PostWithDetailsDto.Tags))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.CommentCount))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.Writer))] + public override partial PostWithDetailsDto Map(Post source); + + [MapperIgnoreTarget(nameof(PostWithDetailsDto.Tags))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.CommentCount))] + [MapperIgnoreTarget(nameof(PostWithDetailsDto.Writer))] + public override partial void Map(Post source, PostWithDetailsDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class TagToTagDtoMapper : MapperBase +{ + public override partial TagDto Map(Tag source); + + public override partial void Map(Tag source, TagDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogUserToBlogUserDtoMapper : MapperBase +{ + public override partial BlogUserDto Map(BlogUser source); + + public override partial void Map(BlogUser source, BlogUserDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogToBlogDtoMapper : MapperBase +{ + public override partial BlogDto Map(Blog source); + + public override partial void Map(Blog source, BlogDto destination); +} diff --git a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationModule.cs b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationModule.cs index aee53fe9b0..2a4ba01979 100644 --- a/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationModule.cs +++ b/modules/blogging/src/Volo.Blogging.Application/Volo/Blogging/BloggingApplicationModule.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Application; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.BlobStoring; using Volo.Abp.Caching; using Volo.Abp.Modularity; @@ -14,7 +14,7 @@ namespace Volo.Blogging typeof(BloggingDomainModule), typeof(BloggingApplicationContractsModule), typeof(AbpCachingModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpBlobStoringModule), typeof(AbpDddApplicationModule) )] @@ -22,12 +22,8 @@ namespace Volo.Blogging { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); - + context.Services.AddMapperlyObjectMapper(); + Configure(options => { //TODO: Rename UpdatePolicy/DeletePolicy since it's candidate to conflicts with other modules! diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.abppkg.analyze.json b/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.abppkg.analyze.json index 4fb8a11fb7..a91fd840e4 100644 --- a/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.abppkg.analyze.json +++ b/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.abppkg.analyze.json @@ -16,9 +16,9 @@ "name": "AbpDddDomainModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Abp.Caching", diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.csproj b/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.csproj index ff0dbba17b..75291c83c1 100644 --- a/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.csproj +++ b/modules/blogging/src/Volo.Blogging.Domain/Volo.Blogging.Domain.csproj @@ -13,7 +13,7 @@ - + diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainMappers.cs b/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainMappers.cs new file mode 100644 index 0000000000..dfc7fcadcf --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainMappers.cs @@ -0,0 +1,40 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Blogging.Blogs; +using Volo.Blogging.Comments; +using Volo.Blogging.Posts; +using Volo.Blogging.Tagging; + +namespace Volo.Blogging; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class TagToTagEtoMapper : MapperBase +{ + public override partial TagEto Map(Tag source); + + public override partial void Map(Tag source, TagEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PostToPostEtoMapper : MapperBase +{ + public override partial PostEto Map(Post source); + + public override partial void Map(Post source, PostEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class CommentToCommentEtoMapper : MapperBase +{ + public override partial CommentEto Map(Comment source); + + public override partial void Map(Comment source, CommentEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogToBlogEtoMapper : MapperBase +{ + public override partial BlogEto Map(Blog source); + + public override partial void Map(Blog source, BlogEto destination); +} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainMappingProfile.cs b/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainMappingProfile.cs deleted file mode 100644 index da36b39049..0000000000 --- a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainMappingProfile.cs +++ /dev/null @@ -1,19 +0,0 @@ -using AutoMapper; -using Volo.Blogging.Blogs; -using Volo.Blogging.Comments; -using Volo.Blogging.Posts; -using Volo.Blogging.Tagging; - -namespace Volo.Blogging -{ - public class BloggingDomainMappingProfile : Profile - { - public BloggingDomainMappingProfile() - { - CreateMap(); - CreateMap(); - CreateMap(); - CreateMap(); - } - } -} \ No newline at end of file diff --git a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainModule.cs b/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainModule.cs index 2e5e9a68e0..b2bd8439f2 100644 --- a/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainModule.cs +++ b/modules/blogging/src/Volo.Blogging.Domain/Volo/Blogging/BloggingDomainModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Domain; using Volo.Abp.Domain.Entities.Events.Distributed; @@ -14,20 +14,15 @@ namespace Volo.Blogging [DependsOn( typeof(BloggingDomainSharedModule), typeof(AbpDddDomainModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpCachingModule) )] public class BloggingDomainModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); - + context.Services.AddMapperlyObjectMapper(); + Configure(options => { options.EtoMappings.Add(typeof(BloggingDomainModule)); diff --git a/modules/blogging/src/Volo.Blogging.Web/AbpBloggingWebAutoMapperProfile.cs b/modules/blogging/src/Volo.Blogging.Web/AbpBloggingWebAutoMapperProfile.cs deleted file mode 100644 index f96b302a6a..0000000000 --- a/modules/blogging/src/Volo.Blogging.Web/AbpBloggingWebAutoMapperProfile.cs +++ /dev/null @@ -1,16 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Blogging.Pages.Blogs.Posts; -using Volo.Blogging.Posts; - -namespace Volo.Blogging -{ - public class AbpBloggingWebAutoMapperProfile : Profile - { - public AbpBloggingWebAutoMapperProfile() - { - CreateMap().Ignore(x=>x.Tags); - CreateMap(); - } - } -} diff --git a/modules/blogging/src/Volo.Blogging.Web/AbpBloggingWebMappers.cs b/modules/blogging/src/Volo.Blogging.Web/AbpBloggingWebMappers.cs new file mode 100644 index 0000000000..538621c56e --- /dev/null +++ b/modules/blogging/src/Volo.Blogging.Web/AbpBloggingWebMappers.cs @@ -0,0 +1,24 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Blogging.Pages.Blogs.Posts; +using Volo.Blogging.Posts; + +namespace Volo.Blogging; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PostWithDetailsDtoToEditPostViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(EditPostViewModel.Tags))] + public override partial EditPostViewModel Map(PostWithDetailsDto source); + + [MapperIgnoreTarget(nameof(EditPostViewModel.Tags))] + public override partial void Map(PostWithDetailsDto source, EditPostViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class CreatePostViewModelToCreatePostDtoMapper : MapperBase +{ + public override partial CreatePostDto Map(NewModel.CreatePostViewModel source); + + public override partial void Map(NewModel.CreatePostViewModel source, CreatePostDto destination); +} diff --git a/modules/blogging/src/Volo.Blogging.Web/BloggingWebModule.cs b/modules/blogging/src/Volo.Blogging.Web/BloggingWebModule.cs index 9de636cdb2..95b66ee136 100644 --- a/modules/blogging/src/Volo.Blogging.Web/BloggingWebModule.cs +++ b/modules/blogging/src/Volo.Blogging.Web/BloggingWebModule.cs @@ -8,7 +8,7 @@ using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Prismjs; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Modularity; using Volo.Abp.UI.Navigation; @@ -23,7 +23,7 @@ namespace Volo.Blogging typeof(BloggingApplicationContractsModule), typeof(AbpAspNetCoreMvcUiBootstrapModule), typeof(AbpAspNetCoreMvcUiBundlingModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class BloggingWebModule : AbpModule { @@ -42,17 +42,13 @@ namespace Volo.Blogging public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddMapperlyObjectMapper(); + Configure(options => { options.FileSets.AddEmbedded(); }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); - Configure(options => { options diff --git a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.abppkg.analyze.json b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.abppkg.analyze.json index 895ffb1675..7eba0d50a7 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.abppkg.analyze.json +++ b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.abppkg.analyze.json @@ -21,9 +21,9 @@ "name": "AbpAspNetCoreMvcUiBundlingModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" } ], "implementingInterfaces": [ diff --git a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj index 06133868ab..a7156b0e8d 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj +++ b/modules/blogging/src/Volo.Blogging.Web/Volo.Blogging.Web.csproj @@ -18,7 +18,7 @@ - + diff --git a/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/CmsKitHttpApiHostModule.cs b/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/CmsKitHttpApiHostModule.cs index 523dec994f..1f451f58d9 100644 --- a/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/CmsKitHttpApiHostModule.cs +++ b/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/CmsKitHttpApiHostModule.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Claims; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; diff --git a/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj b/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj index 92c5715372..9dc85442bb 100644 --- a/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj +++ b/modules/cms-kit/host/Volo.CmsKit.HttpApi.Host/Volo.CmsKit.HttpApi.Host.csproj @@ -11,7 +11,7 @@ - + diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Host/CmsKitWebAutoMapperProfile.cs b/modules/cms-kit/host/Volo.CmsKit.Web.Host/CmsKitWebAutoMapperProfile.cs deleted file mode 100644 index bea413f2bc..0000000000 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Host/CmsKitWebAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace Volo.CmsKit; - -public class CmsKitWebAutoMapperProfile : Profile -{ - public CmsKitWebAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Web project. - } -} diff --git a/modules/cms-kit/host/Volo.CmsKit.Web.Host/CmsKitWebHostModule.cs b/modules/cms-kit/host/Volo.CmsKit.Web.Host/CmsKitWebHostModule.cs index 7f2f539d7e..522771c77b 100644 --- a/modules/cms-kit/host/Volo.CmsKit.Web.Host/CmsKitWebHostModule.cs +++ b/modules/cms-kit/host/Volo.CmsKit.Web.Host/CmsKitWebHostModule.cs @@ -23,7 +23,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.FeatureManagement; @@ -87,7 +87,6 @@ public class CmsKitWebHostModule : AbpModule ConfigureCache(configuration); ConfigureUrls(configuration); ConfigureAuthentication(context, configuration); - ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureSwaggerServices(context.Services); ConfigureMultiTenancy(); @@ -159,14 +158,6 @@ public class CmsKitWebHostModule : AbpModule }); } - private void ConfigureAutoMapper() - { - Configure(options => - { - options.AddMaps(); - }); - } - private void ConfigureVirtualFileSystem(IWebHostEnvironment hostingEnvironment) { if (hostingEnvironment.IsDevelopment()) diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo.CmsKit.Admin.Application.abppkg.analyze.json b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo.CmsKit.Admin.Application.abppkg.analyze.json index e2ddb41de0..ce5b9fcead 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo.CmsKit.Admin.Application.abppkg.analyze.json +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo.CmsKit.Admin.Application.abppkg.analyze.json @@ -11,9 +11,9 @@ "name": "CmsKitAdminApplicationContractsModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.CmsKit.Common.Application", diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationAutoMapperProfile.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationAutoMapperProfile.cs deleted file mode 100644 index 60f8ccb8a8..0000000000 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,54 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.CmsKit.Admin.Blogs; -using Volo.CmsKit.Admin.Comments; -using Volo.CmsKit.Admin.MediaDescriptors; -using Volo.CmsKit.Admin.Pages; -using Volo.CmsKit.Blogs; -using Volo.CmsKit.Admin.Tags; -using Volo.CmsKit.Comments; -using Volo.CmsKit.MediaDescriptors; -using Volo.CmsKit.Pages; -using Volo.CmsKit.Tags; -using Volo.CmsKit.Users; -using Volo.CmsKit.Menus; -using Volo.CmsKit.Admin.Menus; - -namespace Volo.CmsKit.Admin; - -public class CmsKitAdminApplicationAutoMapperProfile : Profile -{ - public CmsKitAdminApplicationAutoMapperProfile() - { - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - CreateMap() - .Ignore(x => x.Author) - .MapExtraProperties(); - - CreateMap().MapExtraProperties(); - CreateMap(); - - CreateMap(MemberList.Destination).MapExtraProperties(); - CreateMap() - .Ignore(d => d.BlogName) - .MapExtraProperties(); - - CreateMap(MemberList.Source).MapExtraProperties(); - CreateMap(MemberList.Source).MapExtraProperties(); - - CreateMap().Ignore(b => b.BlogPostCount).MapExtraProperties(); - - CreateMap(MemberList.Destination); - - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - CreateMap() - .Ignore(x => x.PageTitle) - .MapExtraProperties(); - } -} diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationMappers.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationMappers.cs new file mode 100644 index 0000000000..533fa08656 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationMappers.cs @@ -0,0 +1,140 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.CmsKit.Admin.Blogs; +using Volo.CmsKit.Admin.Comments; +using Volo.CmsKit.Admin.MediaDescriptors; +using Volo.CmsKit.Admin.Pages; +using Volo.CmsKit.Blogs; +using Volo.CmsKit.Admin.Tags; +using Volo.CmsKit.Comments; +using Volo.CmsKit.MediaDescriptors; +using Volo.CmsKit.Pages; +using Volo.CmsKit.Tags; +using Volo.CmsKit.Users; +using Volo.CmsKit.Menus; +using Volo.CmsKit.Admin.Menus; + +namespace Volo.CmsKit.Admin; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class TagEntityTypeDefinitonToTagDefinitionDtoMapper : MapperBase +{ + public override partial TagDefinitionDto Map(TagEntityTypeDefiniton source); + + public override partial void Map(TagEntityTypeDefiniton source, TagDefinitionDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogPostToBlogPostDtoMapper : MapperBase +{ + public override partial BlogPostDto Map(BlogPost source); + + public override partial void Map(BlogPost source, BlogPostDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogPostToBlogPostListDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(BlogPostListDto.BlogName))] + public override partial BlogPostListDto Map(BlogPost source); + + [MapperIgnoreTarget(nameof(BlogPostListDto.BlogName))] + public override partial void Map(BlogPost source, BlogPostListDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class MenuItemToMenuItemWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(MenuItemWithDetailsDto.PageTitle))] + public override partial MenuItemWithDetailsDto Map(MenuItem source); + + [MapperIgnoreTarget(nameof(MenuItemWithDetailsDto.PageTitle))] + public override partial void Map(MenuItem source, MenuItemWithDetailsDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class MenuItemToMenuItemMapper : MapperBase +{ + public override partial MenuItemDto Map(MenuItem source); + + public override partial void Map(MenuItem source, MenuItemDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class MediaDescriptorToMediaDescriptorDtoMapper : MapperBase +{ + public override partial MediaDescriptorDto Map(MediaDescriptor source); + + public override partial void Map(MediaDescriptor source, MediaDescriptorDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class TagToTagDtoMapper : MapperBase +{ + public override partial TagDto Map(Tag source); + + public override partial void Map(Tag source, TagDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogToBlogDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(BlogDto.BlogPostCount))] + public override partial BlogDto Map(Blog source); + + [MapperIgnoreTarget(nameof(BlogDto.BlogPostCount))] + public override partial void Map(Blog source, BlogDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PageToPageLookupDtoMapper : MapperBase +{ + public override partial PageLookupDto Map(Page source); + + public override partial void Map(Page source, PageLookupDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class PageToPageDtoMapper : MapperBase +{ + public override partial PageDto Map(Page source); + + public override partial void Map(Page source, PageDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CommentToCommentWithAuthorDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(CommentWithAuthorDto.Author))] + public override partial CommentWithAuthorDto Map(Comment source); + + [MapperIgnoreTarget(nameof(CommentWithAuthorDto.Author))] + public override partial void Map(Comment source, CommentWithAuthorDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CommentToCommentDtoMapper : MapperBase +{ + public override partial CommentDto Map(Comment source); + + public override partial void Map(Comment source, CommentDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CmsUserToCommentsCmsUserDtoMapper : MapperBase +{ + public override partial Comments.CmsUserDto Map(CmsUser source); + + public override partial void Map(CmsUser source, Comments.CmsUserDto destination); +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationModule.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationModule.cs index 2a9765dd5a..0be1a58d00 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/CmsKitAdminApplicationModule.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.GlobalFeatures; using Volo.Abp.Localization; using Volo.Abp.Modularity; @@ -17,23 +17,18 @@ namespace Volo.CmsKit.Admin; [DependsOn( typeof(CmsKitAdminApplicationContractsModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(CmsKitCommonApplicationModule) )] public class CmsKitAdminApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); + context.Services.AddMapperlyObjectMapper(); ConfigureTagOptions(); ConfigureCommentOptions(); - - Configure(options => - { - options.AddMaps(validate: true); - }); } private void ConfigureTagOptions() diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/GlobalResources/GlobalResourceAdminAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/GlobalResources/GlobalResourceAdminAppService.cs index 0847347ab6..eec3483a90 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/GlobalResources/GlobalResourceAdminAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Application/Volo/CmsKit/Admin/GlobalResources/GlobalResourceAdminAppService.cs @@ -7,13 +7,14 @@ using Volo.CmsKit.Features; using Volo.CmsKit.GlobalFeatures; using Volo.CmsKit.GlobalResources; using Volo.CmsKit.Permissions; +using Volo.CmsKit.Admin; namespace Volo.CmsKit.Admin.GlobalResources; [RequiresFeature(CmsKitFeatures.GlobalResourceEnable)] [RequiresGlobalFeature(typeof(GlobalResourcesFeature))] [Authorize(CmsKitAdminPermissions.GlobalResources.Default)] -public class GlobalResourceAdminAppService : ApplicationService, IGlobalResourceAdminAppService +public class GlobalResourceAdminAppService : CmsKitAdminAppServiceBase, IGlobalResourceAdminAppService { public GlobalResourceManager GlobalResourceManager { get; } diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebAutoMapperProfile.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebAutoMapperProfile.cs deleted file mode 100644 index 655399109d..0000000000 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebAutoMapperProfile.cs +++ /dev/null @@ -1,57 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.CmsKit.Admin.Blogs; -using Volo.CmsKit.Admin.Menus; -using Volo.CmsKit.Admin.Pages; -using Volo.CmsKit.Admin.Tags; -using Volo.CmsKit.Menus; -using Volo.CmsKit.Tags; - -namespace Volo.CmsKit.Admin.Web; - -public class CmsKitAdminWebAutoMapperProfile : Profile -{ - public CmsKitAdminWebAutoMapperProfile() - { - CreateBlogPostMappings(); - CreateBlogMappings(); - CreateMenuMappings(); - CreatePageMappings(); - CreateTagMappings(); - } - - protected virtual void CreateBlogPostMappings() - { - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - } - - protected virtual void CreateBlogMappings() - { - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - } - - protected virtual void CreateMenuMappings() - { - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - } - - protected virtual void CreatePageMappings() - { - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - } - - protected virtual void CreateTagMappings() - { - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - } -} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebMappers.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebMappers.cs new file mode 100644 index 0000000000..99b07a65fc --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebMappers.cs @@ -0,0 +1,214 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.CmsKit.Admin.Blogs; +using Volo.CmsKit.Admin.Menus; +using Volo.CmsKit.Admin.Pages; +using Volo.CmsKit.Admin.Tags; +using Volo.CmsKit.Admin.Web.Pages.CmsKit.BlogPosts; +using Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs; +using Volo.CmsKit.Admin.Web.Pages.CmsKit.Tags; +using Volo.CmsKit.Blogs; +using Volo.CmsKit.Tags; +using CreateModalModel = Volo.CmsKit.Admin.Web.Pages.CmsKit.Tags.CreateModalModel; + +namespace Volo.CmsKit.Admin.Web; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogFeatureInputDtoToBlogFeatureViewModelMapper : TwoWayMapperBase +{ + public override partial FeaturesModalModel.BlogFeatureViewModel Map(BlogFeatureInputDto source); + public override partial void Map(BlogFeatureInputDto source, FeaturesModalModel.BlogFeatureViewModel destination); + + public override partial BlogFeatureInputDto ReverseMap(FeaturesModalModel.BlogFeatureViewModel destination); + public override partial void ReverseMap(FeaturesModalModel.BlogFeatureViewModel destination, BlogFeatureInputDto source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogFeatureDtoToBlogFeatureViewModelMapper : TwoWayMapperBase +{ + public override partial FeaturesModalModel.BlogFeatureViewModel Map(BlogFeatureDto source); + public override partial void Map(BlogFeatureDto source, FeaturesModalModel.BlogFeatureViewModel destination); + + public override partial BlogFeatureDto ReverseMap(FeaturesModalModel.BlogFeatureViewModel destination); + public override partial void ReverseMap(FeaturesModalModel.BlogFeatureViewModel destination, BlogFeatureDto source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class CreateBlogPostDtoToCreateBlogPostViewModelMapper : TwoWayMapperBase +{ + public override partial CreateModel.CreateBlogPostViewModel Map(CreateBlogPostDto source); + public override partial void Map(CreateBlogPostDto source, CreateModel.CreateBlogPostViewModel destination); + + public override partial CreateBlogPostDto ReverseMap(CreateModel.CreateBlogPostViewModel destination); + public override partial void ReverseMap(CreateModel.CreateBlogPostViewModel destination, CreateBlogPostDto source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class TagUpdateDtoToTagEditViewModelMapper : TwoWayMapperBase +{ + public override partial EditModalModel.TagEditViewModel Map(TagUpdateDto source); + public override partial void Map(TagUpdateDto source, EditModalModel.TagEditViewModel destination); + + public override partial TagUpdateDto ReverseMap(EditModalModel.TagEditViewModel destination); + public override partial void ReverseMap(EditModalModel.TagEditViewModel destination, TagUpdateDto source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class TagDtoToCreateBlogPostDtoMapper : MapperBase +{ + public override partial EditModalModel.TagEditViewModel Map(TagDto source); + + public override partial void Map(TagDto source, EditModalModel.TagEditViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CreateBlogPostViewModelToCreateBlogPostDtoMapper : MapperBase +{ + public override partial CreateBlogPostDto Map(CreateModel.CreateBlogPostViewModel source); + + public override partial void Map(CreateModel.CreateBlogPostViewModel source, CreateBlogPostDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class UpdateBlogPostViewModelToUpdateBlogPostDtoMapper : TwoWayMapperBase +{ + public override partial UpdateBlogPostDto Map(UpdateModel.UpdateBlogPostViewModel source); + + public override partial void Map(UpdateModel.UpdateBlogPostViewModel source, UpdateBlogPostDto destination); + + public override partial UpdateModel.UpdateBlogPostViewModel ReverseMap(UpdateBlogPostDto destination); + + public override partial void ReverseMap(UpdateBlogPostDto destination, UpdateModel.UpdateBlogPostViewModel source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogPostDtoToUpdateBlogPostViewModelMapper : MapperBase +{ + public override partial UpdateModel.UpdateBlogPostViewModel Map(BlogPostDto source); + + public override partial void Map(BlogPostDto source, UpdateModel.UpdateBlogPostViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogDtoUpdateToBlogViewModelMapper : MapperBase +{ + public override partial Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs.UpdateModalModel.UpdateBlogViewModel Map(BlogDto source); + + public override partial void Map(BlogDto source, Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs.UpdateModalModel.UpdateBlogViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CreateBlogViewModelToCreateBlogDtoMapper : MapperBase +{ + public override partial CreateBlogDto Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs.CreateModalModel.CreateBlogViewModel source); + + public override partial void Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs.CreateModalModel.CreateBlogViewModel source, CreateBlogDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class UpdateBlogViewModelToUpdateBlogDtoMapper : MapperBase +{ + public override partial UpdateBlogDto Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs.UpdateModalModel.UpdateBlogViewModel source); + + public override partial void Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Blogs.UpdateModalModel.UpdateBlogViewModel source, UpdateBlogDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class MenuItemUpdateViewModelToMenuItemCreateInputMapper : TwoWayMapperBase +{ + public override partial MenuItemCreateInput Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Menus.MenuItems.CreateModalModel.MenuItemCreateViewModel source); + + public override partial void Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Menus.MenuItems.CreateModalModel.MenuItemCreateViewModel source, MenuItemCreateInput destination); + public override partial Pages.CmsKit.Menus.MenuItems.CreateModalModel.MenuItemCreateViewModel ReverseMap(MenuItemCreateInput destination); + + public override partial void ReverseMap(MenuItemCreateInput destination, Pages.CmsKit.Menus.MenuItems.CreateModalModel.MenuItemCreateViewModel source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class MenuItemUpdateViewModelToMenuItemUpdateInputMapper : MapperBase +{ + public override partial MenuItemUpdateInput Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Menus.MenuItems.UpdateModalModel.MenuItemUpdateViewModel source); + + public override partial void Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Menus.MenuItems.UpdateModalModel.MenuItemUpdateViewModel source, MenuItemUpdateInput destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class MenuItemWithDetailsDtoToMenuItemUpdateViewModelMapper : MapperBase +{ + public override partial Volo.CmsKit.Admin.Web.Pages.CmsKit.Menus.MenuItems.UpdateModalModel.MenuItemUpdateViewModel Map(MenuItemWithDetailsDto source); + + public override partial void Map(MenuItemWithDetailsDto source, Volo.CmsKit.Admin.Web.Pages.CmsKit.Menus.MenuItems.UpdateModalModel.MenuItemUpdateViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class PageDtoToCreatePageInputDtoMapper : MapperBase +{ + public override partial Volo.CmsKit.Admin.Web.Pages.CmsKit.Pages.UpdateModel.UpdatePageViewModel Map(PageDto source); + + public override partial void Map(PageDto source, Volo.CmsKit.Admin.Web.Pages.CmsKit.Pages.UpdateModel.UpdatePageViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CreatePageViewModelToCreatePageInputDtoMapper : TwoWayMapperBase +{ + public override partial CreatePageInputDto Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Pages.CreateModel.CreatePageViewModel source); + + public override partial void Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Pages.CreateModel.CreatePageViewModel source, CreatePageInputDto destination); + public override partial Pages.CmsKit.Pages.CreateModel.CreatePageViewModel ReverseMap(CreatePageInputDto destination); + + public override partial void ReverseMap(CreatePageInputDto destination, Pages.CmsKit.Pages.CreateModel.CreatePageViewModel source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class UpdatePageViewModelToUpdatePageInputDtoMapper : TwoWayMapperBase +{ + public override partial UpdatePageInputDto Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Pages.UpdateModel.UpdatePageViewModel source); + + public override partial void Map(Volo.CmsKit.Admin.Web.Pages.CmsKit.Pages.UpdateModel.UpdatePageViewModel source, UpdatePageInputDto destination); + + public override partial Pages.CmsKit.Pages.UpdateModel.UpdatePageViewModel ReverseMap(UpdatePageInputDto destination); + + public override partial void ReverseMap(UpdatePageInputDto destination, Pages.CmsKit.Pages.UpdateModel.UpdatePageViewModel source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class TagCreateViewModelToTagCreateDtoMapper : TwoWayMapperBase +{ + public override partial TagCreateDto Map(CreateModalModel.TagCreateViewModel source); + + public override partial void Map(CreateModalModel.TagCreateViewModel source, TagCreateDto destination); + public override partial CreateModalModel.TagCreateViewModel ReverseMap(TagCreateDto destination); + + public override partial void ReverseMap(TagCreateDto destination, CreateModalModel.TagCreateViewModel source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class TagEditViewModelToTagUpdateDtoMapper : MapperBase +{ + public override partial TagUpdateDto Map(EditModalModel.TagEditViewModel source); + + public override partial void Map(EditModalModel.TagEditViewModel source, TagUpdateDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class TagDtoToTagEditViewModelMapper : MapperBase +{ + public override partial EditModalModel.TagEditViewModel Map(TagDto source); + + public override partial void Map(TagDto source, EditModalModel.TagEditViewModel destination); +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebModule.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebModule.cs index 45cbbd818d..c820362566 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/CmsKitAdminWebModule.cs @@ -7,7 +7,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Packages.MarkdownIt; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Prismjs; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.PageToolbars; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Localization; using Volo.Abp.Modularity; @@ -82,8 +82,7 @@ public class CmsKitAdminWebModule : AbpModule options.FileSets.AddEmbedded("Volo.CmsKit.Admin.Web"); }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => { options.AddMaps(validate: true); }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Create.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Create.cshtml.cs index 83fa3d0285..8e969115e0 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Create.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Create.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using System; using System.Collections.Generic; @@ -50,7 +49,6 @@ public class CreateModel : CmsKitAdminPageModel return new OkObjectResult(createResult); } - [AutoMap(typeof(CreateBlogPostDto), ReverseMap = true)] public class CreateBlogPostViewModel : ExtensibleObject { [Required] diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Update.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Update.cshtml.cs index 810a955158..b0ed97d8cd 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Update.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/Update.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using System; using System.Collections.Generic; @@ -56,8 +55,6 @@ public class UpdateModel : CmsKitAdminPageModel return NoContent(); } - [AutoMap(typeof(BlogPostDto))] - [AutoMap(typeof(UpdateBlogPostDto), ReverseMap = true)] public class UpdateBlogPostViewModel : ExtensibleObject, IHasConcurrencyStamp { [DynamicMaxLength(typeof(BlogPostConsts), nameof(BlogPostConsts.MaxTitleLength))] diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/CreateModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/CreateModal.cshtml.cs index 5f7ea9cee1..422a167776 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/CreateModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/CreateModal.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Volo.Abp.ObjectExtending; diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/FeaturesModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/FeaturesModal.cshtml.cs index a18d2e7d89..a7071a1f12 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/FeaturesModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/FeaturesModal.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; @@ -47,8 +46,6 @@ public class FeaturesModalModel : CmsKitAdminPageModel return NoContent(); } - [AutoMap(typeof(BlogFeatureDto), ReverseMap = true)] - [AutoMap(typeof(BlogFeatureInputDto), ReverseMap = true)] public class BlogFeatureViewModel { private string featureName; diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/UpdateModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/UpdateModal.cshtml.cs index 1cccffe496..b36c0daefc 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/UpdateModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Blogs/UpdateModal.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml.cs index f0730d08a7..6c9a324da8 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/CreateModal.cshtml.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using AutoMapper; using Microsoft.AspNetCore.Mvc; using Volo.Abp.Authorization.Permissions; using Volo.Abp.Features; @@ -48,7 +47,6 @@ public class CreateModalModel : CmsKitAdminPageModel return new OkObjectResult(dto); } - [AutoMap(typeof(MenuItemCreateInput), ReverseMap = true)] public class MenuItemCreateViewModel : ExtensibleObject { [HiddenInput] diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml.cs index 9cb5af1bba..19da825365 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Menus/MenuItems/UpdateModal.cshtml.cs @@ -2,9 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; -using AutoMapper; using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; using Volo.Abp.Domain.Entities; using Volo.Abp.Features; using Volo.Abp.GlobalFeatures; @@ -12,7 +10,6 @@ using Volo.Abp.ObjectExtending; using Volo.CmsKit.Admin.Menus; using Volo.CmsKit.Features; using Volo.CmsKit.GlobalFeatures; -using Volo.CmsKit.Menus; namespace Volo.CmsKit.Admin.Web.Pages.CmsKit.Menus.MenuItems; diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Pages/Create.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Pages/Create.cshtml.cs index 9a8369ad60..96d3f218c6 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Pages/Create.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Pages/Create.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Form; @@ -32,7 +31,6 @@ public class CreateModel : CmsKitAdminPageModel return new OkObjectResult(created); } - [AutoMap(typeof(CreatePageInputDto), ReverseMap = true)] public class CreatePageViewModel : ExtensibleObject { [Required] diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Pages/Update.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Pages/Update.cshtml.cs index 44c48a31c0..fff1f814e5 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Pages/Update.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Pages/Update.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using System; using System.ComponentModel.DataAnnotations; using System.Threading.Tasks; @@ -44,8 +43,6 @@ public class UpdateModel : CmsKitAdminPageModel return NoContent(); } - [AutoMap(typeof(PageDto))] - [AutoMap(typeof(UpdatePageInputDto), ReverseMap = true)] public class UpdatePageViewModel : ExtensibleObject, IHasConcurrencyStamp { [Required] diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Tags/CreateModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Tags/CreateModal.cshtml.cs index 8b2b5f7dcf..807683826c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Tags/CreateModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Tags/CreateModal.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -44,7 +43,6 @@ public class CreateModalModel : CmsKitAdminPageModel return NoContent(); } - [AutoMap(typeof(TagCreateDto), ReverseMap = true)] public class TagCreateViewModel : ExtensibleObject { [DynamicMaxLength(typeof(TagConsts), nameof(TagConsts.MaxEntityTypeLength))] diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Tags/EditModal.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Tags/EditModal.cshtml.cs index 3482e5e05b..34a052591e 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Tags/EditModal.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/Tags/EditModal.cshtml.cs @@ -1,5 +1,4 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; @@ -44,8 +43,6 @@ public class EditModalModel : CmsKitAdminPageModel return NoContent(); } - [AutoMap(typeof(TagDto))] - [AutoMap(typeof(TagUpdateDto), ReverseMap = true)] public class TagEditViewModel : ExtensibleObject, IHasConcurrencyStamp { [Required] diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo.CmsKit.Common.Application.abppkg.analyze.json b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo.CmsKit.Common.Application.abppkg.analyze.json index 970db426d7..b661387b41 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo.CmsKit.Common.Application.abppkg.analyze.json +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo.CmsKit.Common.Application.abppkg.analyze.json @@ -21,9 +21,9 @@ "name": "AbpDddApplicationModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" } ], "implementingInterfaces": [ diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo.CmsKit.Common.Application.csproj b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo.CmsKit.Common.Application.csproj index d7d88be59e..014d592dc7 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo.CmsKit.Common.Application.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo.CmsKit.Common.Application.csproj @@ -9,7 +9,7 @@ - + diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitAppServiceBase.cs b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitAppServiceBase.cs index 99efcb92b8..dc3b79f2d0 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitAppServiceBase.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitAppServiceBase.cs @@ -8,5 +8,6 @@ public abstract class CmsKitAppServiceBase : ApplicationService protected CmsKitAppServiceBase() { LocalizationResource = typeof(CmsKitResource); + ObjectMapperContext = typeof(CmsKitCommonApplicationModule); } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationAutoMapperProfile.cs b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationAutoMapperProfile.cs deleted file mode 100644 index 099b3e1b10..0000000000 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,25 +0,0 @@ -using AutoMapper; -using Volo.CmsKit.Blogs; -using Volo.CmsKit.Tags; -using Volo.CmsKit.Users; - -namespace Volo.CmsKit; - -public class CmsKitCommonApplicationAutoMapperProfile : Profile -{ - public CmsKitCommonApplicationAutoMapperProfile() - { - CreateMap().MapExtraProperties(); - - CreateMap(); - - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - CreateMap().MapExtraProperties(); - CreateMap() - .MapExtraProperties() - .ReverseMap() - .MapExtraProperties(); - } -} diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationMappers.cs b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationMappers.cs new file mode 100644 index 0000000000..53ca4b28f7 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationMappers.cs @@ -0,0 +1,62 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.CmsKit.Blogs; +using Volo.CmsKit.Tags; +using Volo.CmsKit.Users; + +namespace Volo.CmsKit; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class OrganizationUnitRoleToOrganizationUnitRoleDtoMapper : TwoWayMapperBase +{ + public override partial BlogFeatureDto Map(BlogFeatureCacheItem source); + public override partial void Map(BlogFeatureCacheItem source, BlogFeatureDto destination); + + public override partial BlogFeatureCacheItem ReverseMap(BlogFeatureDto destination); + public override partial void ReverseMap(BlogFeatureDto destination, BlogFeatureCacheItem source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogFeatureToBlogFeatureDtoMapper : MapperBase +{ + public override partial BlogFeatureDto Map(BlogFeature source); + + public override partial void Map(BlogFeature source, BlogFeatureDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogFeatureToBlogFeatureCacheItemMapper : MapperBase +{ + public override partial BlogFeatureCacheItem Map(BlogFeature source); + + public override partial void Map(BlogFeature source, BlogFeatureCacheItem destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PopularTagToPopularTagDtoMapper : MapperBase +{ + public override partial PopularTagDto Map(PopularTag source); + + public override partial void Map(PopularTag source, PopularTagDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CmsUserToCmsUserDtoMapper : MapperBase +{ + public override partial CmsUserDto Map(CmsUser source); + + public override partial void Map(CmsUser source, CmsUserDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class TagToTagDtoMapper : MapperBase +{ + public override partial TagDto Map(Tag source); + + public override partial void Map(Tag source, TagDto destination); +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationModule.cs b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationModule.cs index 44125bd4bf..5a9b786bc8 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Application/Volo/CmsKit/CmsKitCommonApplicationModule.cs @@ -1,11 +1,7 @@ -using Volo.Abp.Application; -using Volo.Abp.AutoMapper; -using Volo.Abp.GlobalFeatures; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Application; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; -using Volo.CmsKit.Blogs; -using Volo.CmsKit.GlobalFeatures; -using Volo.CmsKit.MediaDescriptors; -using Volo.CmsKit.Permissions; namespace Volo.CmsKit; @@ -13,15 +9,12 @@ namespace Volo.CmsKit; typeof(CmsKitCommonApplicationContractsModule), typeof(CmsKitDomainModule), typeof(AbpDddApplicationModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class CmsKitCommonApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => - { - options.AddMaps(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Web/CmsKitCommonWebModule.cs b/modules/cms-kit/src/Volo.CmsKit.Common.Web/CmsKitCommonWebModule.cs index e8a30a75bc..754f2263a0 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Web/CmsKitCommonWebModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Web/CmsKitCommonWebModule.cs @@ -1,5 +1,5 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Modularity; using Volo.Abp.VirtualFileSystem; @@ -13,12 +13,14 @@ namespace Volo.CmsKit.Web; [DependsOn( typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(CmsKitCommonApplicationContractsModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class CmsKitCommonWebModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddMapperlyObjectMapper(); + Configure(options => { options.ReactionIcons[StandardReactions.Smile] = new LocalizableIconDictionary("fas fa-smile text-warning"); diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.abppkg.analyze.json b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.abppkg.analyze.json index 6b69cfe938..e507786073 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.abppkg.analyze.json +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.abppkg.analyze.json @@ -16,9 +16,9 @@ "name": "CmsKitCommonApplicationContractsModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" } ], "implementingInterfaces": [ diff --git a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj index 8e8fe81033..f74f7012d0 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj +++ b/modules/cms-kit/src/Volo.CmsKit.Common.Web/Volo.CmsKit.Common.Web.csproj @@ -14,7 +14,7 @@ - + diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/CmsKitPublicApplicationMappers.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/CmsKitPublicApplicationMappers.cs new file mode 100644 index 0000000000..2d71d87eb1 --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/CmsKitPublicApplicationMappers.cs @@ -0,0 +1,111 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.CmsKit.Blogs; +using Volo.CmsKit.Comments; +using Volo.CmsKit.Contents; +using Volo.CmsKit.GlobalResources; +using Volo.CmsKit.Menus; +using Volo.CmsKit.Pages; +using Volo.CmsKit.Public.Comments; +using Volo.CmsKit.Public.GlobalResources; +using Volo.CmsKit.Public.Ratings; +using Volo.CmsKit.Ratings; +using Volo.CmsKit.Users; + +namespace Volo.CmsKit.Public; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CmsUserToCmsUserDtoMapper : MapperBase +{ + public override partial Comments.CmsUserDto Map(CmsUser source); + + public override partial void Map(CmsUser source, Comments.CmsUserDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CommentToCommentDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(CommentDto.Author))] + public override partial CommentDto Map(Comment source); + + [MapperIgnoreTarget(nameof(CommentDto.Author))] + public override partial void Map(Comment source, CommentDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CommentToCommentWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(CommentWithDetailsDto.Replies))] + [MapperIgnoreTarget(nameof(CommentWithDetailsDto.Author))] + public override partial CommentWithDetailsDto Map(Comment source); + + [MapperIgnoreTarget(nameof(CommentWithDetailsDto.Replies))] + [MapperIgnoreTarget(nameof(CommentWithDetailsDto.Author))] + public override partial void Map(Comment source, CommentWithDetailsDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class RatingToRatingDtoMapper : MapperBase +{ + public override partial RatingDto Map(Rating source); + + public override partial void Map(Rating source, RatingDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class PageToPageCacheItemMapper : MapperBase +{ + public override partial PageCacheItem Map(Page source); + + public override partial void Map(Page source, PageCacheItem destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class PageCacheItemToPageDtoMapper : MapperBase +{ + public override partial PageDto Map(PageCacheItem source); + + public override partial void Map(PageCacheItem source, PageDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogPostToPageDtoMapper : MapperBase +{ + public override partial PageDto Map(Page source); + + public override partial void Map(Page source, PageDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class BlogPostToBlogPostCommonDtoMapper : MapperBase +{ + public override partial BlogPostCommonDto Map(BlogPost source); + + public override partial void Map(BlogPost source, BlogPostCommonDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class MenuItemToMenuItemDtoMapper : MapperBase +{ + public override partial MenuItemDto Map(MenuItem source); + + public override partial void Map(MenuItem source, MenuItemDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class GlobalResourceToGlobalResourceDtoMapper : MapperBase +{ + public override partial GlobalResourceDto Map(GlobalResource source); + + public override partial void Map(GlobalResource source, GlobalResourceDto destination); +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/CmsKitPublicApplicationModule.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/CmsKitPublicApplicationModule.cs index 4d7f816081..93a0ae83f1 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/CmsKitPublicApplicationModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/CmsKitPublicApplicationModule.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; using Volo.Abp.Caching; using Volo.Abp.Modularity; @@ -14,11 +13,6 @@ public class CmsKitPublicApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddMaps(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/GlobalResources/GlobalResourcePublicAppService.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/GlobalResources/GlobalResourcePublicAppService.cs index 3b432967db..a91e6bd0d4 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/GlobalResources/GlobalResourcePublicAppService.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/GlobalResources/GlobalResourcePublicAppService.cs @@ -5,12 +5,13 @@ using Volo.Abp.GlobalFeatures; using Volo.CmsKit.Features; using Volo.CmsKit.GlobalFeatures; using Volo.CmsKit.GlobalResources; +using Volo.CmsKit.Public; namespace Volo.CmsKit.Public.GlobalResources; [RequiresFeature(CmsKitFeatures.GlobalResourceEnable)] [RequiresGlobalFeature(typeof(GlobalResourcesFeature))] -public class GlobalResourcePublicAppService : ApplicationService, IGlobalResourcePublicAppService +public class GlobalResourcePublicAppService : CmsKitPublicAppServiceBase, IGlobalResourcePublicAppService { public GlobalResourceManager GlobalResourceManager { get; } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/GlobalResources/Handlers/GlobalResourceEventHandler.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/GlobalResources/Handlers/GlobalResourceEventHandler.cs index d16e271e4e..1b5089def9 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/GlobalResources/Handlers/GlobalResourceEventHandler.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/GlobalResources/Handlers/GlobalResourceEventHandler.cs @@ -12,12 +12,12 @@ public class GlobalResourceEventHandler : ILocalEventHandler>, ITransientDependency { - public IObjectMapper ObjectMapper { get; } + public IObjectMapper ObjectMapper { get; } private readonly IDistributedCache _resourceCache; public GlobalResourceEventHandler( IDistributedCache resourceCache, - IObjectMapper objectMapper) + IObjectMapper objectMapper) { ObjectMapper = objectMapper; _resourceCache = resourceCache; @@ -29,4 +29,4 @@ public class GlobalResourceEventHandler : eventData.Entity.Name, ObjectMapper.Map(eventData.Entity)); } -} \ No newline at end of file +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/PublicApplicationAutoMapperProfile.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/PublicApplicationAutoMapperProfile.cs deleted file mode 100644 index a625eca4f8..0000000000 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/PublicApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,46 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.CmsKit.Blogs; -using Volo.CmsKit.Comments; -using Volo.CmsKit.Contents; -using Volo.CmsKit.GlobalResources; -using Volo.CmsKit.Menus; -using Volo.CmsKit.Pages; -using Volo.CmsKit.Public.Blogs; -using Volo.CmsKit.Public.Comments; -using Volo.CmsKit.Public.GlobalResources; -using Volo.CmsKit.Public.Ratings; -using Volo.CmsKit.Ratings; -using Volo.CmsKit.Users; - -namespace Volo.CmsKit.Public; - -public class PublicApplicationAutoMapperProfile : Profile -{ - public PublicApplicationAutoMapperProfile() - { - CreateMap().MapExtraProperties(); - - CreateMap() - .Ignore(x => x.Author).MapExtraProperties(); - - CreateMap() - .Ignore(x => x.Replies) - .Ignore(x => x.Author) - .MapExtraProperties(); - - CreateMap(); - - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - - CreateMap().MapExtraProperties(); - } -} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebAutoMapperProfile.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebAutoMapperProfile.cs index 5acb8d6fe9..7fcbeed7d8 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebAutoMapperProfile.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebAutoMapperProfile.cs @@ -1,15 +1,38 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.CmsKit.Menus; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.CmsKit.Contents; using Volo.CmsKit.Public.Comments; +using Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Blogs; +using Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Pages; namespace Volo.CmsKit.Public.Web; -public class CmsKitPublicWebAutoMapperProfile : Profile +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PageDtoToPageViewModelMapper : TwoWayMapperBase { - public CmsKitPublicWebAutoMapperProfile() - { - CreateMap() - .Ignore(x=> x.ExtraProperties); - } + public override partial PageViewModel Map(PageDto source); + public override partial void Map(PageDto source, PageViewModel destination); + + public override partial PageDto ReverseMap(PageViewModel destination); + public override partial void ReverseMap(PageViewModel destination, PageDto source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class BlogPostCommonDtoToBlogPostViewModelMapper : TwoWayMapperBase +{ + public override partial BlogPostViewModel Map(BlogPostCommonDto source); + public override partial void Map(BlogPostCommonDto source, BlogPostViewModel destination); + + public override partial BlogPostCommonDto ReverseMap(BlogPostViewModel destination); + public override partial void ReverseMap(BlogPostViewModel destination, BlogPostCommonDto source); } + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class CreateCommentWithParametersInputToCommentDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(CreateCommentInput.ExtraProperties))] + public override partial CreateCommentInput Map(CreateCommentWithParametersInput source); + + [MapperIgnoreTarget(nameof(CreateCommentInput.ExtraProperties))] + public override partial void Map(CreateCommentWithParametersInput source, CreateCommentInput destination); +} \ No newline at end of file diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebModule.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebModule.cs index 8132202717..31344206ec 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebModule.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebModule.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Localization; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.GlobalFeatures; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; @@ -67,12 +67,7 @@ public class CmsKitPublicWebModule : AbpModule options.FileSets.AddEmbedded("Volo.CmsKit.Public.Web"); }); - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddMaps(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicControllerBase.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicControllerBase.cs index f22444036d..8251397bdd 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicControllerBase.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicControllerBase.cs @@ -8,5 +8,6 @@ public abstract class CmsKitPublicControllerBase : AbpController public CmsKitPublicControllerBase() { LocalizationResource = typeof(CmsKitResource); + ObjectMapperContext = typeof(CmsKitPublicWebModule); } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPostViewModel.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPostViewModel.cs index 789d2687fc..02caf748ef 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPostViewModel.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Blogs/BlogPostViewModel.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using AutoMapper; using Volo.Abp.Application.Dtos; using Volo.CmsKit.Contents; using Volo.CmsKit.Users; namespace Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Blogs; -[AutoMap(typeof(BlogPostCommonDto), ReverseMap = true)] public class BlogPostViewModel : AuditedEntityDto { public Guid BlogId { get; set; } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Pages/Index.cshtml.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Pages/Index.cshtml.cs index 18eb184742..6eb5982030 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Pages/Index.cshtml.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Pages/Index.cshtml.cs @@ -20,6 +20,8 @@ public class IndexModel : CommonPageModel public IndexModel(IPagePublicAppService pagePublicAppService, ContentParser contentParser) { + ObjectMapperContext = typeof(CmsKitPublicWebModule); + PagePublicAppService = pagePublicAppService; ContentParser = contentParser; } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Pages/PageViewModel.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Pages/PageViewModel.cs index 27432305fe..ebc2a9f69c 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Pages/PageViewModel.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/Public/CmsKit/Pages/PageViewModel.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; -using AutoMapper; using Volo.CmsKit.Contents; -using Volo.CmsKit.Public.Pages; namespace Volo.CmsKit.Public.Web.Pages.Public.CmsKit.Pages; -[AutoMap(typeof(PageDto), ReverseMap = true)] public class PageViewModel { public Guid Id { get; set; } diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.abppkg.analyze.json b/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.abppkg.analyze.json index ad864b4fe1..ea4dad5da4 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.abppkg.analyze.json +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.abppkg.analyze.json @@ -26,9 +26,9 @@ "name": "AbpCachingModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Abp.Ddd.Application", diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.csproj b/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.csproj index 725c70bc2b..f1eb4e09f3 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.csproj +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo.Docs.Admin.Application.csproj @@ -14,7 +14,7 @@ - + diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationAutoMapperProfile.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationAutoMapperProfile.cs deleted file mode 100644 index 366c0ab27b..0000000000 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,22 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Docs.Admin.Documents; -using Volo.Docs.Admin.Projects; -using Volo.Docs.Documents; -using Volo.Docs.Projects; - -namespace Volo.Docs.Admin -{ - public class DocsAdminApplicationAutoMapperProfile : Profile - { - public DocsAdminApplicationAutoMapperProfile() - { - CreateMap(); - CreateMap().Ignore(x => x.ProjectName); - CreateMap(); - CreateMap(); - CreateMap(); - CreateMap(); - } - } -} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationMappers.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationMappers.cs new file mode 100644 index 0000000000..813e4165b8 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationMappers.cs @@ -0,0 +1,58 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Docs.Admin.Documents; +using Volo.Docs.Admin.Projects; +using Volo.Docs.Documents; +using Volo.Docs.Projects; + +namespace Volo.Docs.Admin; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ProjectPdfFileToProjectPdfFileDtoMapper : MapperBase +{ + public override partial ProjectPdfFileDto Map(ProjectPdfFile source); + + public override partial void Map(ProjectPdfFile source, ProjectPdfFileDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class DocumentInfoToDocumentInfoDtoMapper : MapperBase +{ + public override partial DocumentInfoDto Map(DocumentInfo source); + + public override partial void Map(DocumentInfo source, DocumentInfoDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ProjectWithoutDetailsToProjectWithoutDetailsDtoMapper : MapperBase +{ + public override partial ProjectWithoutDetailsDto Map(ProjectWithoutDetails source); + + public override partial void Map(ProjectWithoutDetails source, ProjectWithoutDetailsDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class DocumentWithoutContentToDocumentDtoMapper : MapperBase +{ + public override partial DocumentDto Map(DocumentWithoutContent source); + + public override partial void Map(DocumentWithoutContent source, DocumentDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class DocumentToDocumentDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(DocumentDto.ProjectName))] + public override partial DocumentDto Map(Document source); + + [MapperIgnoreTarget(nameof(DocumentDto.ProjectName))] + public override partial void Map(Document source, DocumentDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ProjectToProjectDtoMapper : MapperBase +{ + public override partial ProjectDto Map(Project source); + + public override partial void Map(Project source, ProjectDto destination); +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationModule.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationModule.cs index 2491bf5fb6..0a54afbcce 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationModule.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/DocsAdminApplicationModule.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Application; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Modularity; using Volo.Docs.Common; @@ -13,7 +13,7 @@ namespace Volo.Docs.Admin typeof(DocsAdminApplicationContractsModule), typeof(DocsCommonApplicationModule), typeof(AbpCachingModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpDddApplicationModule), typeof(AbpBackgroundJobsAbstractionsModule) )] @@ -21,11 +21,7 @@ namespace Volo.Docs.Admin { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } } diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Documents/DocumentAdminAppService.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Documents/DocumentAdminAppService.cs index a6359c2c9b..36edcb12d4 100644 --- a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Documents/DocumentAdminAppService.cs +++ b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Documents/DocumentAdminAppService.cs @@ -46,6 +46,7 @@ namespace Volo.Docs.Admin.Documents _elasticSearchService = elasticSearchService; LocalizationResource = typeof(DocsResource); + ObjectMapperContext = typeof(DocsAdminApplicationModule); } public virtual async Task ClearCacheAsync(ClearCacheInput input) diff --git a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs deleted file mode 100644 index d6a1d2230a..0000000000 --- a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMapperProfile.cs +++ /dev/null @@ -1,30 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Docs.Admin.Documents; -using Volo.Docs.Admin.Pages.Docs.Admin.Projects; -using Volo.Docs.Admin.Projects; - -namespace Volo.Docs.Admin -{ - public class DocsAdminWebAutoMapperProfile : Profile - { - public DocsAdminWebAutoMapperProfile() - { - CreateMap() - .Ignore(x => x.ExtraProperties); - - CreateMap() - .Ignore(x => x.ExtraProperties); - - CreateMap () - .Ignore(x => x.GitHubAccessToken) - .Ignore(x => x.GitHubRootUrl) - .Ignore(x => x.GitHubUserAgent) - .Ignore(x => x.GithubVersionProviderSource) - .Ignore(x => x.VersionBranchPrefix); - - CreateMap(); - CreateMap(); - } - } -} diff --git a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMappers.cs b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMappers.cs new file mode 100644 index 0000000000..87b87c021b --- /dev/null +++ b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebAutoMappers.cs @@ -0,0 +1,60 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Docs.Admin.Documents; +using Volo.Docs.Admin.Pages.Docs.Admin.Projects; +using Volo.Docs.Admin.Projects; + +namespace Volo.Docs.Admin; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PullDocumentViewModelToPullAllDocumentInputMapper : MapperBase +{ + public override partial PullAllDocumentInput Map(PullModel.PullDocumentViewModel source); + + public override partial void Map(PullModel.PullDocumentViewModel source, PullAllDocumentInput destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PullDocumentViewModelToPullDocumentInputMapper : MapperBase +{ + public override partial PullDocumentInput Map(PullModel.PullDocumentViewModel source); + + public override partial void Map(PullModel.PullDocumentViewModel source, PullDocumentInput destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ProjectDtoToEditGithubProjectViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.GitHubAccessToken))] + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.GitHubRootUrl))] + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.GitHubUserAgent))] + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.GithubVersionProviderSource))] + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.VersionBranchPrefix))] + public override partial EditModel.EditGithubProjectViewModel Map(ProjectDto source); + + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.GitHubAccessToken))] + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.GitHubRootUrl))] + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.GitHubUserAgent))] + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.GithubVersionProviderSource))] + [MapperIgnoreTarget(nameof(EditModel.EditGithubProjectViewModel.VersionBranchPrefix))] + public override partial void Map(ProjectDto source, EditModel.EditGithubProjectViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CreateGithubProjectViewModelToCreateProjectDtoMapper : MapperBase +{ + public override partial CreateProjectDto Map(CreateModel.CreateGithubProjectViewModel source); + + public override partial void Map(CreateModel.CreateGithubProjectViewModel source, CreateProjectDto destination); +} + + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class EditGithubProjectViewModelToUpdateProjectDtoMyClassMapper : MapperBase +{ + public override partial UpdateProjectDto Map(EditModel.EditGithubProjectViewModel source); + + public override partial void Map(EditModel.EditGithubProjectViewModel source, UpdateProjectDto destination); +} diff --git a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebModule.cs b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebModule.cs index 3e0d548487..ead6d38375 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebModule.cs +++ b/modules/docs/src/Volo.Docs.Admin.Web/DocsAdminWebModule.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Modularity; using Volo.Abp.UI.Navigation; @@ -32,6 +32,8 @@ namespace Volo.Docs.Admin public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddMapperlyObjectMapper(); + Configure(options => { options.MenuContributors.Add(new DocsMenuContributor()); @@ -42,12 +44,6 @@ namespace Volo.Docs.Admin options.FileSets.AddEmbedded(); }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); - Configure(options => { options.DisableModule(DocsAdminRemoteServiceConsts.ModuleName); diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.csproj b/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.csproj index b706561d9d..3142de4e32 100644 --- a/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.csproj +++ b/modules/docs/src/Volo.Docs.Admin.Web/Volo.Docs.Admin.Web.csproj @@ -19,7 +19,7 @@ - + diff --git a/modules/docs/src/Volo.Docs.Application/Volo.Docs.Application.abppkg.analyze.json b/modules/docs/src/Volo.Docs.Application/Volo.Docs.Application.abppkg.analyze.json index b6e792225a..3eb0baf95f 100644 --- a/modules/docs/src/Volo.Docs.Application/Volo.Docs.Application.abppkg.analyze.json +++ b/modules/docs/src/Volo.Docs.Application/Volo.Docs.Application.abppkg.analyze.json @@ -21,9 +21,9 @@ "name": "AbpCachingModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Docs.Common.Application", diff --git a/modules/docs/src/Volo.Docs.Application/Volo.Docs.Application.csproj b/modules/docs/src/Volo.Docs.Application/Volo.Docs.Application.csproj index b7530d7fe8..ade9347ac4 100644 --- a/modules/docs/src/Volo.Docs.Application/Volo.Docs.Application.csproj +++ b/modules/docs/src/Volo.Docs.Application/Volo.Docs.Application.csproj @@ -14,7 +14,7 @@ - + diff --git a/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationAutoMapperProfile.cs b/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationAutoMapperProfile.cs deleted file mode 100644 index f2cb5476f7..0000000000 --- a/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,20 +0,0 @@ -using AutoMapper; -using Volo.Docs.Documents; -using Volo.Abp.AutoMapper; -using Volo.Docs.Common.Projects; -using Volo.Docs.Projects; - -namespace Volo.Docs -{ - public class DocsApplicationAutoMapperProfile : Profile - { - public DocsApplicationAutoMapperProfile() - { - CreateMap(); - CreateMap(); - CreateMap().Ignore(x => x.Project).Ignore(x => x.Contributors); - CreateMap(); - CreateMap(); - } - } -} diff --git a/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationMappers.cs b/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationMappers.cs new file mode 100644 index 0000000000..bd8474b7b9 --- /dev/null +++ b/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationMappers.cs @@ -0,0 +1,51 @@ +using Riok.Mapperly.Abstractions; +using Volo.Docs.Documents; +using Volo.Abp.Mapperly; +using Volo.Docs.Common.Projects; +using Volo.Docs.Projects; + +namespace Volo.Docs; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class DocumentResourceToDocumentResourceDtoMapper : MapperBase +{ + public override partial DocumentResourceDto Map(DocumentResource source); + + public override partial void Map(DocumentResource source, DocumentResourceDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class DocumentContributorToDocumentContributorDtoMapper : MapperBase +{ + public override partial DocumentContributorDto Map(DocumentContributor source); + + public override partial void Map(DocumentContributor source, DocumentContributorDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class VersionInfoToVersionInfoDtoMapper : MapperBase +{ + public override partial VersionInfoDto Map(VersionInfo source); + + public override partial void Map(VersionInfo source, VersionInfoDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ProjectToProjectDtoMapper : MapperBase +{ + public override partial ProjectDto Map(Project source); + + public override partial void Map(Project source, ProjectDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class DocumentToDocumentWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(DocumentWithDetailsDto.Project))] + [MapperIgnoreTarget(nameof(DocumentWithDetailsDto.Contributors))] + public override partial DocumentWithDetailsDto Map(Document source); + + [MapperIgnoreTarget(nameof(DocumentWithDetailsDto.Project))] + [MapperIgnoreTarget(nameof(DocumentWithDetailsDto.Contributors))] + public override partial void Map(Document source, DocumentWithDetailsDto destination); +} \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationModule.cs b/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationModule.cs index f6d60099ad..e30577f8f6 100644 --- a/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationModule.cs +++ b/modules/docs/src/Volo.Docs.Application/Volo/Docs/DocsApplicationModule.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Volo.Abp.Application; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Modularity; using Volo.Docs.Common; @@ -13,7 +13,7 @@ namespace Volo.Docs typeof(DocsDomainModule), typeof(DocsApplicationContractsModule), typeof(AbpCachingModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(DocsCommonApplicationModule), typeof(AbpDddApplicationModule) )] @@ -21,12 +21,7 @@ namespace Volo.Docs { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); context.Services.TryAddSingleton(NullNavigationTreePostProcessor.Instance); } diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.abppkg.analyze.json b/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.abppkg.analyze.json index 0b3f9b4bf2..090a9292d0 100644 --- a/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.abppkg.analyze.json +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.abppkg.analyze.json @@ -16,9 +16,9 @@ "name": "DocsCommonApplicationContractsModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Abp.Ddd.Application", diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.csproj b/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.csproj index 545995b902..0e1570ddea 100644 --- a/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.csproj +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo.Docs.Common.Application.csproj @@ -13,7 +13,7 @@ - + diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationAutoMapperProfile.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationAutoMapperProfile.cs deleted file mode 100644 index 8175654743..0000000000 --- a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; -using Volo.Docs.Common.Projects; -using Volo.Docs.Projects; - -namespace Volo.Docs.Common -{ - public class DocsCommonApplicationAutoMapperProfile : Profile - { - public DocsCommonApplicationAutoMapperProfile() - { - CreateMap(); - CreateMap(); - } - } -} diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationMappers.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationMappers.cs new file mode 100644 index 0000000000..9c0874ecdd --- /dev/null +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationMappers.cs @@ -0,0 +1,22 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Docs.Common.Projects; +using Volo.Docs.Projects; + +namespace Volo.Docs.Common; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ProjectToProjectDtoMapper : MapperBase +{ + public override partial ProjectDto Map(Project source); + + public override partial void Map(Project source, ProjectDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class VersionInfoToVersionInfoDtoMapper : MapperBase +{ + public override partial VersionInfoDto Map(VersionInfo source); + + public override partial void Map(VersionInfo source, VersionInfoDto destination); +} diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationModule.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationModule.cs index b7eb350930..3339c528dd 100644 --- a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationModule.cs +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/DocsCommonApplicationModule.cs @@ -1,6 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Application; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; namespace Volo.Docs.Common; @@ -8,18 +8,13 @@ namespace Volo.Docs.Common; [DependsOn( typeof(DocsDomainModule), typeof(DocsCommonApplicationContractsModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpDddApplicationModule) )] public class DocsCommonApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Projects/ProjectAppService.cs b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Projects/ProjectAppService.cs index 96634a300e..eb5c9bc7db 100644 --- a/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Projects/ProjectAppService.cs +++ b/modules/docs/src/Volo.Docs.Common.Application/Volo/Docs/Common/Projects/ProjectAppService.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Volo.Abp.Application.Dtos; using Volo.Abp.Caching; using Volo.Abp.Data; +using Volo.Abp.Threading; using Volo.Docs.Caching; using Volo.Docs.Documents; using Volo.Docs.Projects; @@ -19,6 +21,8 @@ namespace Volo.Docs.Common.Projects private readonly IDocumentSourceFactory _documentSource; protected IDistributedCache LanguageCache { get; } + private readonly SemaphoreSlim _syncSemaphore = new SemaphoreSlim(1, 1); + public ProjectAppService( IProjectRepository projectRepository, IDistributedCache> versionCache, @@ -56,21 +60,31 @@ namespace Volo.Docs.Common.Projects public virtual async Task> GetVersionsAsync(string shortName) { var project = await _projectRepository.GetByShortNameAsync(shortName); - - var versions = await _versionCache.GetOrAddAsync( - CacheKeyGenerator.GenerateProjectVersionsCacheKey(project), - () => GetVersionsAsync(project), - () => new DistributedCacheEntryOptions + using (await _syncSemaphore.LockAsync()) + { + var versions = await _versionCache.GetAsync(CacheKeyGenerator.GenerateProjectVersionsCacheKey(project)); + if (versions.IsNullOrEmpty()) { - //TODO: Configurable? - AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12), - SlidingExpiration = TimeSpan.FromMinutes(60) + versions = await GetVersionsAsync(project); + if (!versions.IsNullOrEmpty()) + { + await _versionCache.SetAsync( + CacheKeyGenerator.GenerateProjectVersionsCacheKey(project), + versions, + new DistributedCacheEntryOptions + { + //TODO: Configurable? + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(12), + SlidingExpiration = TimeSpan.FromMinutes(60) + } + ); + } } - ); - return new ListResultDto( - ObjectMapper.Map, List>(versions) - ); + return new ListResultDto( + ObjectMapper.Map, List>(versions) + ); + } } protected virtual async Task> GetVersionsAsync(Project project) diff --git a/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.abppkg.analyze.json b/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.abppkg.analyze.json index c5c46bc5d7..8dba6531a8 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.abppkg.analyze.json +++ b/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.abppkg.analyze.json @@ -16,9 +16,9 @@ "name": "AbpDddDomainModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Abp.BlobStoring", diff --git a/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.csproj b/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.csproj index e57312e16b..25c6b9bb83 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.csproj +++ b/modules/docs/src/Volo.Docs.Domain/Volo.Docs.Domain.csproj @@ -31,7 +31,7 @@ - + diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/DocsDomainMappingProfile.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/DocsDomainMappingProfile.cs index 74f7224ea6..757d8d7195 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/DocsDomainMappingProfile.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/DocsDomainMappingProfile.cs @@ -1,15 +1,22 @@ -using AutoMapper; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; using Volo.Docs.Documents; using Volo.Docs.Projects; -namespace Volo.Docs +namespace Volo.Docs; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ProjectToProjectEtoMapper : MapperBase { - public class DocsDomainMappingProfile : Profile - { - public DocsDomainMappingProfile() - { - CreateMap(); - CreateMap(); - } - } + public override partial ProjectEto Map(Project source); + + public override partial void Map(Project source, ProjectEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class DocumentToDocumentEtoMapper : MapperBase +{ + public override partial DocumentEto Map(Document source); + + public override partial void Map(Document source, DocumentEto destination); } \ No newline at end of file diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/DocsDomainModule.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/DocsDomainModule.cs index 502b2f9065..414ed4b36f 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/DocsDomainModule.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/DocsDomainModule.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Volo.Abp; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.BlobStoring; using Volo.Abp.Caching; using Volo.Abp.Domain; @@ -27,7 +27,7 @@ namespace Volo.Docs [DependsOn( typeof(DocsDomainSharedModule), typeof(AbpDddDomainModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpBlobStoringModule), typeof(AbpCachingModule) )] @@ -35,13 +35,8 @@ namespace Volo.Docs { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); - + context.Services.AddMapperlyObjectMapper(); + Configure(options => { options.EtoMappings.Add(typeof(DocsDomainModule)); diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs index cd83333495..308f26f086 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Projects/Pdf/IText/ITextHtmlToPdfRenderer.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using iText.Html2pdf; using iText.Kernel.Pdf; using iText.Kernel.Pdf.Action; +using iText.Layout.Font; using Microsoft.Extensions.Options; using Volo.Abp.DependencyInjection; using Volo.Docs.Utils; @@ -14,13 +15,13 @@ namespace Volo.Docs.Projects.Pdf.IText; public class ITextHtmlToPdfRenderer : IHtmlToPdfRenderer, ITransientDependency { protected IOptions Options { get; } - + public ITextHtmlToPdfRenderer(IOptions options) { Options = options; } - - public virtual Task RenderAsync(string title, string html, List documents) + + public virtual async Task RenderAsync(string title, string html, List documents) { var pdfStream = new MemoryStream(); using (var pdfWriter = new PdfWriter(pdfStream)) @@ -29,33 +30,43 @@ public class ITextHtmlToPdfRenderer : IHtmlToPdfRenderer, ITransientDependency using (var pdfDocument = new iText.Kernel.Pdf.PdfDocument(pdfWriter)) { pdfDocument.GetDocumentInfo().SetTitle(title); - CreatePdfFromHtml(html, pdfDocument); - AddOutlinesToPdf(pdfDocument, documents); + await CreatePdfFromHtmlAsync(html, pdfDocument); + await AddOutlinesToPdfAsync(pdfDocument, documents); } } - + pdfStream.Position = 0; - return Task.FromResult(pdfStream); + return pdfStream; } - private void CreatePdfFromHtml(string html, iText.Kernel.Pdf.PdfDocument pdfDocument) + protected virtual async Task CreatePdfFromHtmlAsync(string html, iText.Kernel.Pdf.PdfDocument pdfDocument) { var converter = new ConverterProperties(); + var fontProvider = await GetFontProviderAsync(); + if (fontProvider != null) + { + converter.SetFontProvider(fontProvider); + } var tagWorkerFactory = new HtmlIdTagWorkerFactory(pdfDocument); converter.SetTagWorkerFactory(tagWorkerFactory); - HtmlConverter.ConvertToDocument(html, pdfDocument, converter); - tagWorkerFactory.AddNamedDestinations(); } - private void AddOutlinesToPdf(iText.Kernel.Pdf.PdfDocument pdfDocument, List documents) + protected virtual Task GetFontProviderAsync() + { + return Task.FromResult(null); + } + + protected virtual Task AddOutlinesToPdfAsync(iText.Kernel.Pdf.PdfDocument pdfDocument, List documents) { var pdfOutlines = pdfDocument.GetOutlines(false); BuildPdfOutlines(pdfOutlines, documents); + + return Task.CompletedTask; } - private void BuildPdfOutlines(PdfOutline parentOutline, List pdfDocumentNodes) + protected virtual Task BuildPdfOutlines(PdfOutline parentOutline, List pdfDocumentNodes) { foreach (var pdfDocumentNode in pdfDocumentNodes) { @@ -63,7 +74,7 @@ public class ITextHtmlToPdfRenderer : IHtmlToPdfRenderer, ITransientDependency { continue; } - + var outline = parentOutline.AddOutline(pdfDocumentNode.Title); if (!pdfDocumentNode.Id.IsNullOrWhiteSpace()) { @@ -75,5 +86,7 @@ public class ITextHtmlToPdfRenderer : IHtmlToPdfRenderer, ITransientDependency BuildPdfOutlines(outline, pdfDocumentNode.Children); } } + + return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Web/DocsWebAutoMapperProfile.cs b/modules/docs/src/Volo.Docs.Web/DocsWebAutoMapperProfile.cs deleted file mode 100644 index e0a383e691..0000000000 --- a/modules/docs/src/Volo.Docs.Web/DocsWebAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace Volo.Docs -{ - public class DocsWebAutoMapperProfile : Profile - { - public DocsWebAutoMapperProfile() - { - } - } -} diff --git a/modules/docs/src/Volo.Docs.Web/DocsWebModule.cs b/modules/docs/src/Volo.Docs.Web/DocsWebModule.cs index dc9993dea6..6839196d0b 100644 --- a/modules/docs/src/Volo.Docs.Web/DocsWebModule.cs +++ b/modules/docs/src/Volo.Docs.Web/DocsWebModule.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -9,7 +8,7 @@ using Volo.Abp.Ui.LayoutHooks; using Volo.Abp.AspNetCore.Mvc.UI.Packages; using Volo.Abp.AspNetCore.Mvc.UI.Packages.Prismjs; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Modularity; using Volo.Abp.VirtualFileSystem; @@ -23,7 +22,7 @@ namespace Volo.Docs { [DependsOn( typeof(DocsApplicationContractsModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAspNetCoreMvcUiBootstrapModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpAspNetCoreMvcUiPackagesModule), @@ -46,6 +45,8 @@ namespace Volo.Docs public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddMapperlyObjectMapper(); + Configure(options => { options.FileSets.AddEmbedded(); @@ -88,12 +89,6 @@ namespace Volo.Docs } }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); - Configure(options => { options.Converters[MarkdownDocumentToHtmlConverter.Type] = typeof(MarkdownDocumentToHtmlConverter); diff --git a/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanWebDocumentSectionRenderer.cs b/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanWebDocumentSectionRenderer.cs index e4a26b2df6..48379765ec 100644 --- a/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanWebDocumentSectionRenderer.cs +++ b/modules/docs/src/Volo.Docs.Web/HtmlConverting/ScribanWebDocumentSectionRenderer.cs @@ -1,22 +1,11 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System.Collections.Generic; using System.Threading.Tasks; -using Scriban; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Volo.Abp; -using Volo.Abp.ObjectMapping; using Volo.Docs.Documents.Rendering; -using Volo.Extensions; namespace Volo.Docs.HtmlConverting { public class ScribanWebDocumentSectionRenderer : ScribanDocumentSectionRenderer, IWebDocumentSectionRenderer { - private IObjectMapper ObjectMapper { get; set; } - public Task GetDocumentNavigationsAsync(string documentContent) { return GetSectionAsync(documentContent, DocsNav); @@ -35,4 +24,4 @@ namespace Volo.Docs.HtmlConverting return templates; } } -} \ No newline at end of file +} diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css index 36ca24e100..71840b79d2 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.css @@ -10,7 +10,7 @@ body a { text-decoration: none; } body .btn-primary { - background-color: #b84297 !important; + background-color: #E83090 !important; font-size: 12px; } body .for-mobile { @@ -298,7 +298,7 @@ body.scrolledMore .alert-criteria p.alert-p { } .docs-page .docs-sidebar .docs-tree-list ul li.selected-tree > span .fa { transform: rotate(90deg); - color: #b84297; + color: #E83090; } .docs-page .docs-sidebar .docs-tree-list ul li.selected-tree.last-link > span .fa { transform: rotate(0deg); @@ -482,7 +482,7 @@ body.scrolledMore .alert-criteria p.alert-p { margin-bottom: 1rem; margin-left: 0; padding: 1em 1.5em; - background-color: #e3edf2; + background-color: rgb(227, 237, 242); font-size: 1em; border-radius: 12px; color: #385766; @@ -549,7 +549,7 @@ body.scrolledMore .alert-criteria p.alert-p { background-color: #f4f6fa; border-color: #f4f6fa; border-radius: 12px; - background: rgba(190, 223, 238, 0.82); + background: hsla(199, 59%, 84%, 0.82); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); z-index: 3; @@ -600,16 +600,16 @@ body.scrolledMore .alert-criteria p.alert-p { font-weight: normal; } .docs-page .docs-page-index .docs-inner-anchors .navbar .nav-pills .nav-link.active { - border-left: 1px solid #b84297; + border-left: 1px solid #E83090; background: none; - color: #b84297; + color: #E83090; font-weight: normal; } .docs-page .docs-page-index .docs-inner-anchors .navbar .nav-pills .nav-pills .nav-link.active { - color: #b84297; + color: #E83090; } .docs-page .docs-page-index .docs-inner-anchors .navbar .nav-pills .nav-pills .nav-pills .nav-link.active { - color: #b84297; + color: #E83090; } .docs-page .docs-page-index .docs-inner-anchors .index-scroll { margin-left: -30px; @@ -667,7 +667,7 @@ body.scrolledMore .alert-criteria p.alert-p { display: none; } body .close-mmenu, -body .close-dmenu { + body .close-dmenu { position: absolute; top: -78px; left: 25px; @@ -720,7 +720,7 @@ body .close-dmenu { display: none; } .docs-page .docs-sidebar .docs-top .navbar.navbar-logo .navbar-collapse { - background: #38003d; + background: rgb(56, 0, 61); position: fixed; top: 86px; left: 0; diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.scss b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.scss index deba3d39c3..9d5de59393 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.scss +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Shared/Styles/vs.scss @@ -16,7 +16,7 @@ body { } .btn-primary { - background-color: #b84297 !important; + background-color: #E83090 !important; font-size: 12px; } @@ -368,7 +368,7 @@ body { > span { .fa { transform: rotate(90deg); - color: #b84297; + color: #E83090; } } @@ -734,9 +734,9 @@ body { font-weight: normal; &.active { - border-left: 1px solid #b84297; + border-left: 1px solid #E83090; background: none; - color: #b84297; + color: #E83090; font-weight: normal; } } @@ -744,14 +744,14 @@ body { .nav-pills { .nav-link { &.active { - color: #b84297; + color: #E83090; } } .nav-pills { .nav-link { &.active { - color: #b84297; + color: #E83090; } } } diff --git a/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.abppkg.analyze.json b/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.abppkg.analyze.json index 754357e581..9abe50dda3 100644 --- a/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.abppkg.analyze.json +++ b/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.abppkg.analyze.json @@ -11,9 +11,9 @@ "name": "DocsApplicationContractsModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Abp.AspNetCore.Mvc.UI.Bootstrap", diff --git a/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj b/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj index 2c36600107..353f15811c 100644 --- a/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj +++ b/modules/docs/src/Volo.Docs.Web/Volo.Docs.Web.csproj @@ -15,7 +15,7 @@ - + diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj b/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj index 0b3693b5d1..7125af8060 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo.Abp.Identity.Application.csproj @@ -18,7 +18,7 @@ - + diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs new file mode 100644 index 0000000000..18645cf013 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationMappers.cs @@ -0,0 +1,19 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Identity; +using Volo.Abp.Mapperly; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityUserToIdentityUserDtoMapper : MapperBase +{ + public override partial IdentityUserDto Map(IdentityUser source); + public override partial void Map(IdentityUser source, IdentityUserDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityRoleToIdentityRoleDtoMapper : MapperBase +{ + public override partial IdentityRoleDto Map(IdentityRole source); + public override partial void Map(IdentityRole source, IdentityRoleDto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs index 673a0605c7..07865ab8f5 100644 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement; @@ -8,18 +8,13 @@ namespace Volo.Abp.Identity; [DependsOn( typeof(AbpIdentityDomainModule), typeof(AbpIdentityApplicationContractsModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpPermissionManagementApplicationModule) )] public class AbpIdentityApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } diff --git a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs b/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs deleted file mode 100644 index a546a9bca1..0000000000 --- a/modules/identity/src/Volo.Abp.Identity.Application/Volo/Abp/Identity/AbpIdentityApplicationModuleAutoMapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; - -namespace Volo.Abp.Identity; - -public class AbpIdentityApplicationModuleAutoMapperProfile : Profile -{ - public AbpIdentityApplicationModuleAutoMapperProfile() - { - CreateMap() - .MapExtraProperties(); - - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs deleted file mode 100644 index 52a450785e..0000000000 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,18 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; - -namespace Volo.Abp.Identity.Blazor; - -public class AbpIdentityBlazorAutoMapperProfile : Profile -{ - public AbpIdentityBlazorAutoMapperProfile() - { - CreateMap() - .MapExtraProperties() - .Ignore(x => x.Password) - .Ignore(x => x.RoleNames); - - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs new file mode 100644 index 0000000000..8272d99098 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorMappers.cs @@ -0,0 +1,25 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.Identity.Blazor; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityUserDtoToIdentityUserUpdateDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.Password))] + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial IdentityUserUpdateDto Map(IdentityUserDto source); + + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.Password))] + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial void Map(IdentityUserDto source, IdentityUserUpdateDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityRoleDtoToIdentityRoleUpdateDtoMapper : MapperBase +{ + public override partial IdentityRoleUpdateDto Map(IdentityRoleDto source); + public override partial void Map(IdentityRoleDto source, IdentityRoleUpdateDto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs index 3367b42a1b..89aba05f89 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/AbpIdentityBlazorModule.cs @@ -1,7 +1,7 @@ using Localization.Resources.AbpUi; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.BlazoriseUI; using Volo.Abp.Identity.Localization; using Volo.Abp.Localization; @@ -16,7 +16,7 @@ namespace Volo.Abp.Identity.Blazor; [DependsOn( typeof(AbpIdentityApplicationContractsModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpPermissionManagementBlazorModule), typeof(AbpBlazoriseUIModule) )] @@ -26,12 +26,7 @@ public class AbpIdentityBlazorModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { @@ -42,7 +37,7 @@ public class AbpIdentityBlazorModule : AbpModule { options.AdditionalAssemblies.Add(typeof(AbpIdentityBlazorModule).Assembly); }); - + Configure(options => { options.Resources diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor index d65325abae..456292fa5c 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor @@ -9,6 +9,9 @@ @using Volo.Abp.BlazoriseUI.Components.ObjectExtending @using Volo.Abp.AspNetCore.Components.Web.Theming.Layout @inject AbpBlazorMessageLocalizerHelper LH +@using Microsoft.Extensions.Localization +@using Volo.Abp.UI.Navigation.Localization.Resource +@inject IStringLocalizer LUiNavigation @inherits AbpCrudPageBase diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor.cs index d683823461..4e9a6576d1 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor.cs +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/RoleManagement.razor.cs @@ -41,6 +41,7 @@ public partial class RoleManagement protected override ValueTask SetBreadcrumbItemsAsync() { + BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(LUiNavigation["Menu:Administration"].Value)); BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(L["Menu:IdentityManagement"].Value)); BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(L["Roles"].Value)); return base.SetBreadcrumbItemsAsync(); diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor index 9ecb31e9e7..c2c54814ff 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor @@ -6,6 +6,9 @@ @using Volo.Abp.Identity.Localization @using Volo.Abp.AspNetCore.Components.Web.Theming.Layout @inject AbpBlazorMessageLocalizerHelper LH +@using Microsoft.Extensions.Localization +@using Volo.Abp.UI.Navigation.Localization.Resource +@inject IStringLocalizer LUiNavigation @inherits AbpCrudPageBase diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs index 1da55397ee..a52474b8b1 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Pages/Identity/UserManagement.razor.cs @@ -75,6 +75,7 @@ public partial class UserManagement protected override ValueTask SetBreadcrumbItemsAsync() { + BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(LUiNavigation["Menu:Administration"].Value)); BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(L["Menu:IdentityManagement"].Value)); BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(L["Users"].Value)); return base.SetBreadcrumbItemsAsync(); diff --git a/modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj b/modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj index 355cb4294a..777fb1c31e 100644 --- a/modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj +++ b/modules/identity/src/Volo.Abp.Identity.Blazor/Volo.Abp.Identity.Blazor.csproj @@ -8,7 +8,7 @@ - + diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj b/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj index fadd6e4109..a1bde79665 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo.Abp.Identity.Domain.csproj @@ -23,7 +23,7 @@ - + diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs index baf0871c58..590beb8e09 100644 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/AbpIdentityDomainModule.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Domain; using Volo.Abp.Domain.Entities.Events.Distributed; using Volo.Abp.Modularity; @@ -19,7 +19,7 @@ namespace Volo.Abp.Identity; typeof(AbpDddDomainModule), typeof(AbpIdentityDomainSharedModule), typeof(AbpUsersDomainModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class AbpIdentityDomainModule : AbpModule { @@ -35,12 +35,7 @@ public class AbpIdentityDomainModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs new file mode 100644 index 0000000000..8986ddcd3a --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappers.cs @@ -0,0 +1,33 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.Users; + +namespace Volo.Abp.Identity; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityUserToUserEtoMapper : MapperBase +{ + public override partial UserEto Map(IdentityUser source); + public override partial void Map(IdentityUser source, UserEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityClaimTypeToIdentityClaimTypeEtoMapper : MapperBase +{ + public override partial IdentityClaimTypeEto Map(IdentityClaimType source); + public override partial void Map(IdentityClaimType source, IdentityClaimTypeEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityRoleToIdentityRoleEtoMapper : MapperBase +{ + public override partial IdentityRoleEto Map(IdentityRole source); + public override partial void Map(IdentityRole source, IdentityRoleEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class OrganizationUnitToOrganizationUnitEtoMapper : MapperBase +{ + public override partial OrganizationUnitEto Map(OrganizationUnit source); + public override partial void Map(OrganizationUnit source, OrganizationUnitEto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs b/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs deleted file mode 100644 index ae9e5b5065..0000000000 --- a/modules/identity/src/Volo.Abp.Identity.Domain/Volo/Abp/Identity/IdentityDomainMappingProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; -using Volo.Abp.Users; - -namespace Volo.Abp.Identity; - -public class IdentityDomainMappingProfile : Profile -{ - public IdentityDomainMappingProfile() - { - CreateMap(); - CreateMap(); - CreateMap(); - CreateMap(); - } -} diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs index 9f6085339a..ca6dc42d6f 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreIdentityRoleRepository.cs @@ -57,7 +57,7 @@ public class EfCoreIdentityRoleRepository : EfCoreRepository> GetActiveDelegationsAsync(Guid targetUserId, CancellationToken cancellationToken = default) { + var now = Clock.Now; return await (await GetDbSetAsync()) .AsNoTracking() .Where(x => x.TargetUserId == targetUserId && - x.StartTime <= Clock.Now && - x.EndTime >= Clock.Now) + x.StartTime <= now && + x.EndTime >= now) .ToListAsync(cancellationToken: cancellationToken); } public virtual async Task FindActiveDelegationByIdAsync(Guid id, CancellationToken cancellationToken = default) { + var now = Clock.Now; return await (await GetDbSetAsync()) .AsNoTracking() .FirstOrDefaultAsync(x => x.Id == id && - x.StartTime <= Clock.Now && - x.EndTime >= Clock.Now + x.StartTime <= now && + x.EndTime >= now , cancellationToken: GetCancellationToken(cancellationToken)); } } diff --git a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs index c0d664fbd4..fb783cd343 100644 --- a/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.EntityFrameworkCore/Volo/Abp/Identity/EntityFrameworkCore/EfCoreOrganizationUnitRepository.cs @@ -48,7 +48,7 @@ public class EfCoreOrganizationUnitRepository string sorting = null, int maxResultCount = int.MaxValue, int skipCount = 0, - bool includeDetails = true, + bool includeDetails = false, CancellationToken cancellationToken = default) { return await (await GetDbSetAsync()) diff --git a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs index f977572a04..7deee94160 100644 --- a/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs +++ b/modules/identity/src/Volo.Abp.Identity.MongoDB/Volo/Abp/Identity/MongoDB/MongoIdentityRoleRepository.cs @@ -117,7 +117,7 @@ public class MongoIdentityRoleRepository : MongoDbRepository> GetActiveDelegationsAsync(Guid targetUserId, CancellationToken cancellationToken = default) { + var now = Clock.Now; return await (await GetQueryableAsync(cancellationToken)) .Where(x => x.TargetUserId == targetUserId) - .Where(x => x.StartTime <= Clock.Now && x.EndTime >= Clock.Now) + .Where(x => x.StartTime <= now && x.EndTime >= now) .ToListAsync(cancellationToken: cancellationToken); } public virtual async Task FindActiveDelegationByIdAsync(Guid id, CancellationToken cancellationToken = default) { + var now = Clock.Now; return await (await GetQueryableAsync(cancellationToken)) .FirstOrDefaultAsync(x => x.Id == id && - x.StartTime <= Clock.Now && - x.EndTime >= Clock.Now + x.StartTime <= now && + x.EndTime >= now , cancellationToken: GetCancellationToken(cancellationToken)); } } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs deleted file mode 100644 index e032b3352f..0000000000 --- a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebAutoMapperProfile.cs +++ /dev/null @@ -1,57 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Abp.Identity.Web.Pages.Identity.Roles; -using CreateUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.CreateModalModel; -using EditUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.EditModalModel; - -namespace Volo.Abp.Identity.Web; - -public class AbpIdentityWebAutoMapperProfile : Profile -{ - public AbpIdentityWebAutoMapperProfile() - { - CreateUserMappings(); - CreateRoleMappings(); - } - - protected virtual void CreateUserMappings() - { - //List - CreateMap() - .Ignore(x => x.Password); - - //CreateModal - CreateMap() - .MapExtraProperties() - .ForMember(dest => dest.RoleNames, opt => opt.Ignore()); - - CreateMap() - .ForMember(dest => dest.IsAssigned, opt => opt.Ignore()); - - //EditModal - CreateMap() - .MapExtraProperties() - .ForMember(dest => dest.RoleNames, opt => opt.Ignore()); - - CreateMap() - .ForMember(dest => dest.IsAssigned, opt => opt.Ignore()); - - CreateMap() - .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) - .ForMember(dest => dest.ModifiedBy, opt => opt.Ignore()); - } - - protected virtual void CreateRoleMappings() - { - //List - CreateMap(); - - //CreateModal - CreateMap() - .MapExtraProperties(); - - //EditModal - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs new file mode 100644 index 0000000000..d992c6e9b3 --- /dev/null +++ b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebMappers.cs @@ -0,0 +1,94 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.Identity.Web.Pages.Identity.Roles; +using CreateUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.CreateModalModel; +using EditUserModalModel = Volo.Abp.Identity.Web.Pages.Identity.Users.EditModalModel; + +namespace Volo.Abp.Identity.Web; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityUserDtoToEditUserModalModelUserInfoViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(EditUserModalModel.UserInfoViewModel.Password))] + public override partial EditUserModalModel.UserInfoViewModel Map(IdentityUserDto source); + + [MapperIgnoreTarget(nameof(EditUserModalModel.UserInfoViewModel.Password))] + public override partial void Map(IdentityUserDto source, EditUserModalModel.UserInfoViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CreateUserModalModelUserInfoViewModelToIdentityUserCreateDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserCreateDto.RoleNames))] + public override partial IdentityUserCreateDto Map(CreateUserModalModel.UserInfoViewModel source); + + [MapperIgnoreTarget(nameof(IdentityUserCreateDto.RoleNames))] + public override partial void Map(CreateUserModalModel.UserInfoViewModel source, IdentityUserCreateDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityRoleDtoToCreateUserModalModelAssignedRoleViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(CreateUserModalModel.AssignedRoleViewModel.IsAssigned))] + public override partial CreateUserModalModel.AssignedRoleViewModel Map(IdentityRoleDto source); + + [MapperIgnoreTarget(nameof(CreateUserModalModel.AssignedRoleViewModel.IsAssigned))] + public override partial void Map(IdentityRoleDto source, CreateUserModalModel.AssignedRoleViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class EditUserModalModelUserInfoViewModelToIdentityUserUpdateDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial IdentityUserUpdateDto Map(EditUserModalModel.UserInfoViewModel source); + + [MapperIgnoreTarget(nameof(IdentityUserUpdateDto.RoleNames))] + public override partial void Map(EditUserModalModel.UserInfoViewModel source, IdentityUserUpdateDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityRoleDtoToEditUserModalModelAssignedRoleViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(EditUserModalModel.AssignedRoleViewModel.IsAssigned))] + public override partial EditUserModalModel.AssignedRoleViewModel Map(IdentityRoleDto source); + + [MapperIgnoreTarget(nameof(EditUserModalModel.AssignedRoleViewModel.IsAssigned))] + public override partial void Map(IdentityRoleDto source, EditUserModalModel.AssignedRoleViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityUserDtoToEditUserModalModelDetailViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.CreatedBy))] + [MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.ModifiedBy))] + public override partial EditUserModalModel.DetailViewModel Map(IdentityUserDto source); + + [MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.CreatedBy))] + [MapperIgnoreTarget(nameof(EditUserModalModel.DetailViewModel.ModifiedBy))] + public override partial void Map(IdentityUserDto source, EditUserModalModel.DetailViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityRoleDtoToEditModalModelRoleInfoModelMapper : MapperBase +{ + public override partial EditModalModel.RoleInfoModel Map(IdentityRoleDto source); + public override partial void Map(IdentityRoleDto source, EditModalModel.RoleInfoModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class CreateModalModelRoleInfoModelToIdentityRoleCreateDtoMapper : MapperBase +{ + public override partial IdentityRoleCreateDto Map(CreateModalModel.RoleInfoModel source); + public override partial void Map(CreateModalModel.RoleInfoModel source, IdentityRoleCreateDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class EditModalModelRoleInfoModelToIdentityRoleUpdateDtoMapper : MapperBase +{ + public override partial IdentityRoleUpdateDto Map(EditModalModel.RoleInfoModel source); + public override partial void Map(EditModalModel.RoleInfoModel source, IdentityRoleUpdateDto destination); +} diff --git a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs index ae3e8ebc72..6d2faf6467 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs +++ b/modules/identity/src/Volo.Abp.Identity.Web/AbpIdentityWebModule.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.PageToolbars; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Identity.Localization; using Volo.Abp.Identity.Web.Navigation; @@ -19,7 +19,7 @@ using Volo.Abp.Threading; namespace Volo.Abp.Identity.Web; [DependsOn(typeof(AbpIdentityApplicationContractsModule))] -[DependsOn(typeof(AbpAutoMapperModule))] +[DependsOn(typeof(AbpMapperlyModule))] [DependsOn(typeof(AbpPermissionManagementWebModule))] [DependsOn(typeof(AbpAspNetCoreMvcUiThemeSharedModule))] public class AbpIdentityWebModule : AbpModule @@ -51,12 +51,7 @@ public class AbpIdentityWebModule : AbpModule options.FileSets.AddEmbedded(); }); - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/Index.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/Index.cshtml index ca58ce4b84..5b2c767228 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/Index.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Roles/Index.cshtml @@ -7,12 +7,15 @@ @using Volo.Abp.Identity.Localization @using Volo.Abp.Identity.Web.Navigation @using Volo.Abp.Identity.Web.Pages.Identity.Roles +@using Volo.Abp.UI.Navigation.Localization.Resource @model IndexModel @inject IHtmlLocalizer L +@inject IHtmlLocalizer LUiNavigation @inject IAuthorizationService Authorization @inject IPageLayout PageLayout @{ PageLayout.Content.Title = L["Roles"].Value; + PageLayout.Content.BreadCrumb.Add(LUiNavigation["Menu:Administration"].Value); PageLayout.Content.BreadCrumb.Add(L["Menu:IdentityManagement"].Value); PageLayout.Content.MenuItemName = IdentityMenuNames.Roles; } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/Index.cshtml b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/Index.cshtml index 1b9cce4919..31b3751cbd 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/Index.cshtml +++ b/modules/identity/src/Volo.Abp.Identity.Web/Pages/Identity/Users/Index.cshtml @@ -7,12 +7,15 @@ @using Volo.Abp.Identity.Localization @using Volo.Abp.Identity.Web.Navigation @using Volo.Abp.Identity.Web.Pages.Identity.Users +@using Volo.Abp.UI.Navigation.Localization.Resource @model IndexModel @inject IHtmlLocalizer L +@inject IHtmlLocalizer LUiNavigation @inject IAuthorizationService Authorization @inject IPageLayout PageLayout @{ PageLayout.Content.Title = L["Users"].Value; + PageLayout.Content.BreadCrumb.Add(LUiNavigation["Menu:Administration"].Value); PageLayout.Content.BreadCrumb.Add(L["Menu:IdentityManagement"].Value); PageLayout.Content.MenuItemName = IdentityMenuNames.Users; } diff --git a/modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj b/modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj index 8a77024b52..54bd5fd28b 100644 --- a/modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj +++ b/modules/identity/src/Volo.Abp.Identity.Web/Volo.Abp.Identity.Web.csproj @@ -40,7 +40,7 @@ - + diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.abppkg.analyze.json b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.abppkg.analyze.json index 096202cd42..c1f39e1aee 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.abppkg.analyze.json +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.abppkg.analyze.json @@ -11,9 +11,9 @@ "name": "AbpIdentityServerDomainSharedModule" }, { - "declaringAssemblyName": "Volo.Abp.AutoMapper", - "namespace": "Volo.Abp.AutoMapper", - "name": "AbpAutoMapperModule" + "declaringAssemblyName": "Volo.Abp.Mapperly", + "namespace": "Volo.Abp.Mapperly", + "name": "AbpMapperlyModule" }, { "declaringAssemblyName": "Volo.Abp.Identity.Domain", diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.csproj b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.csproj index fd856b4ca3..fb9248b9cb 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.csproj +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo.Abp.IdentityServer.Domain.csproj @@ -17,7 +17,7 @@ - + diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs index 9b9dde5c96..1fedb1cc74 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AbpIdentityServerDomainModule.cs @@ -5,7 +5,7 @@ using IdentityServer4.Stores; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.BackgroundWorkers; using Volo.Abp.Caching; using Volo.Abp.Domain.Entities.Events.Distributed; @@ -28,7 +28,7 @@ namespace Volo.Abp.IdentityServer; [DependsOn( typeof(AbpIdentityServerDomainSharedModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpIdentityDomainModule), typeof(AbpSecurityModule), typeof(AbpCachingModule), @@ -41,12 +41,7 @@ public class AbpIdentityServerDomainModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedSigningAlgorithmsConverter.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedSigningAlgorithmsConverter.cs index 25bd38ad10..49297a3d21 100644 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedSigningAlgorithmsConverter.cs +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/AllowedSigningAlgorithmsConverter.cs @@ -1,36 +1,24 @@ using System; -using System.Collections.Generic; using System.Linq; -using AutoMapper; namespace Volo.Abp.IdentityServer; -public class AllowedSigningAlgorithmsConverter : - IValueConverter, string>, - IValueConverter> +public static class AllowedSigningAlgorithmsConverter { - public static AllowedSigningAlgorithmsConverter Converter = new AllowedSigningAlgorithmsConverter(); - - public string Convert(ICollection sourceMember, ResolutionContext context) - { - if (sourceMember == null || !sourceMember.Any()) - { - return null; - } - return sourceMember.Aggregate((x, y) => $"{x},{y}"); - } - - public ICollection Convert(string sourceMember, ResolutionContext context) + private const char Separator = ','; + + public static string[] SplitToArray(string algorithms) { - var list = new HashSet(); - if (!String.IsNullOrWhiteSpace(sourceMember)) + if (string.IsNullOrWhiteSpace(algorithms)) { - sourceMember = sourceMember.Trim(); - foreach (var item in sourceMember.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Distinct()) - { - list.Add(item); - } + return []; } - return list; + + return algorithms + .Split([Separator], StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .Where(x => !string.IsNullOrEmpty(x)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); } -} +} \ No newline at end of file diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerAutoMapperProfile.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerAutoMapperProfile.cs deleted file mode 100644 index 53a7de989c..0000000000 --- a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerAutoMapperProfile.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System.Collections.Generic; -using System.Security.Claims; -using AutoMapper; -using Volo.Abp.IdentityServer.ApiResources; -using Volo.Abp.IdentityServer.ApiScopes; -using Volo.Abp.IdentityServer.Clients; -using Volo.Abp.IdentityServer.Devices; -using Volo.Abp.IdentityServer.Grants; -using Volo.Abp.IdentityServer.IdentityResources; - -namespace Volo.Abp.IdentityServer; - -public class IdentityServerAutoMapperProfile : Profile -{ - /// - /// TODO: Reverse maps will not used probably. Remove those will not used - /// - public IdentityServerAutoMapperProfile() - { - CreateMap() - .ConstructUsing(src => src.Type) - .ReverseMap() - .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src)); - - CreateClientMap(); - CreateApiResourceMap(); - CreateApiScopeMap(); - CreateIdentityResourceMap(); - CreatePersistedGrantMap(); - CreateDeviceFlowCodesMap(); - } - - private void CreateClientMap() - { - CreateMap() - .ConstructUsing(src => src.Origin) - .ReverseMap() - .ForMember(dest => dest.Origin, opt => opt.MapFrom(src => src)); - - CreateMap>() - .ReverseMap(); - - CreateMap() - .ForMember(dest => dest.ProtocolType, opt => opt.Condition(srs => srs != null)) - .ForMember(x => x.AllowedIdentityTokenSigningAlgorithms, opts => opts.ConvertUsing(AllowedSigningAlgorithmsConverter.Converter, x => x.AllowedIdentityTokenSigningAlgorithms)) - .ReverseMap() - .ForMember(x => x.AllowedIdentityTokenSigningAlgorithms, opts => opts.ConvertUsing(AllowedSigningAlgorithmsConverter.Converter, x => x.AllowedIdentityTokenSigningAlgorithms)); - - CreateMap() - .ConstructUsing(src => src.Origin) - .ReverseMap() - .ForMember(dest => dest.Origin, opt => opt.MapFrom(src => src)); - - CreateMap() - .ConstructUsing(src => src.Provider) - .ReverseMap() - .ForMember(dest => dest.Provider, opt => opt.MapFrom(src => src)); - - CreateMap(MemberList.None) - .ConstructUsing(src => new Claim(src.Type, src.Value)) - .ReverseMap(); - - CreateMap(MemberList.None) - .ConstructUsing(src => new IdentityServer4.Models.ClientClaim(src.Type, src.Value, ClaimValueTypes.String)) - .ReverseMap(); - - CreateMap() - .ConstructUsing(src => src.Scope) - .ReverseMap() - .ForMember(dest => dest.Scope, opt => opt.MapFrom(src => src)); - - CreateMap() - .ConstructUsing(src => src.PostLogoutRedirectUri) - .ReverseMap() - .ForMember(dest => dest.PostLogoutRedirectUri, opt => opt.MapFrom(src => src)); - - CreateMap() - .ConstructUsing(src => src.RedirectUri) - .ReverseMap() - .ForMember(dest => dest.RedirectUri, opt => opt.MapFrom(src => src)); - - CreateMap() - .ConstructUsing(src => src.GrantType) - .ReverseMap() - .ForMember(dest => dest.GrantType, opt => opt.MapFrom(src => src)); - - CreateMap(MemberList.Destination) - .ForMember(dest => dest.Type, opt => opt.Condition(srs => srs != null)) - .ReverseMap(); - - CreateMap(); - } - - private void CreateApiResourceMap() - { - CreateMap() - .ForMember(dest => dest.ApiSecrets, opt => opt.MapFrom(src => src.Secrets)) - .ForMember(x => x.AllowedAccessTokenSigningAlgorithms, opts => opts.ConvertUsing(AllowedSigningAlgorithmsConverter.Converter, x => x.AllowedAccessTokenSigningAlgorithms)); - - CreateMap(); - - CreateMap() - .ConstructUsing(x => x.Scope) - .ReverseMap() - .ForMember(dest => dest.Scope, opt => opt.MapFrom(src => src)); - - CreateMap>() - .ReverseMap(); - - CreateMap(); - } - - private void CreateApiScopeMap() - { - CreateMap>() - .ReverseMap(); - - CreateMap() - .ConstructUsing(x => x.Type) - .ReverseMap() - .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src)); - - CreateMap(MemberList.Destination) - .ConstructUsing(src => new IdentityServer4.Models.ApiScope()) - .ReverseMap(); - } - - private void CreateIdentityResourceMap() - { - CreateMap() - .ConstructUsing(src => new IdentityServer4.Models.IdentityResource()); - - CreateMap() - .ConstructUsing(x => x.Type) - .ReverseMap() - .ForMember(dest => dest.Type, opt => opt.MapFrom(src => src)); - - CreateMap>() - .ReverseMap(); - - CreateMap(); - } - - private void CreatePersistedGrantMap() - { - //TODO: Why PersistedGrant mapping is in this profile? - CreateMap().ReverseMap(); - CreateMap(); - } - - private void CreateDeviceFlowCodesMap() - { - CreateMap(); - } -} diff --git a/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerMapperlyMappers.cs b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerMapperlyMappers.cs new file mode 100644 index 0000000000..004583a0bb --- /dev/null +++ b/modules/identityserver/src/Volo.Abp.IdentityServer.Domain/Volo/Abp/IdentityServer/IdentityServerMapperlyMappers.cs @@ -0,0 +1,238 @@ +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Volo.Abp.IdentityServer.ApiResources; +using Volo.Abp.IdentityServer.ApiScopes; +using Volo.Abp.IdentityServer.Clients; +using Volo.Abp.IdentityServer.Devices; +using Volo.Abp.IdentityServer.Grants; +using Volo.Abp.IdentityServer.IdentityResources; + +namespace Volo.Abp.IdentityServer; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ClientToISClientMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.AllowedIdentityTokenSigningAlgorithms))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.Claims))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.ClientSecrets))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.AllowedGrantTypes))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.AllowedScopes))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.AllowedCorsOrigins))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.RedirectUris))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.PostLogoutRedirectUris))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.IdentityProviderRestrictions))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.Properties))] + public override partial IdentityServer4.Models.Client Map(Client source); + + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.AllowedIdentityTokenSigningAlgorithms))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.Claims))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.ClientSecrets))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.AllowedGrantTypes))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.AllowedScopes))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.AllowedCorsOrigins))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.RedirectUris))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.PostLogoutRedirectUris))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.IdentityProviderRestrictions))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.Client.Properties))] + public override partial void Map(Client source, IdentityServer4.Models.Client destination); + + public override void AfterMap(Client source, IdentityServer4.Models.Client destination) + { + destination.AllowedIdentityTokenSigningAlgorithms = AllowedSigningAlgorithmsConverter.SplitToArray(source.AllowedIdentityTokenSigningAlgorithms); + if (source.Properties != null) + { + destination.Properties = source.Properties.ToDictionary(x => x.Key, x => x.Value); + } + if (source.Claims != null) + { + destination.Claims = source.Claims.Select(x => new IdentityServer4.Models.ClientClaim(x.Type, x.Value, ClaimValueTypes.String)).ToList(); + } + if (source.ClientSecrets != null) + { + destination.ClientSecrets = source.ClientSecrets.Select(x => new IdentityServer4.Models.Secret(x.Value, x.Expiration) { Type = x.Type, Description = x.Description }).ToList(); + } + if (source.AllowedGrantTypes != null) + { + destination.AllowedGrantTypes = source.AllowedGrantTypes.Select(x => x.GrantType).ToList(); + } + if (source.AllowedScopes != null) + { + destination.AllowedScopes = source.AllowedScopes.Select(x => x.Scope).ToList(); + } + if (source.AllowedCorsOrigins != null) + { + destination.AllowedCorsOrigins = source.AllowedCorsOrigins.Select(x => x.Origin).ToList(); + } + if (source.RedirectUris != null) + { + destination.RedirectUris = source.RedirectUris.Select(x => x.RedirectUri).ToList(); + } + if (source.PostLogoutRedirectUris != null) + { + destination.PostLogoutRedirectUris = source.PostLogoutRedirectUris.Select(x => x.PostLogoutRedirectUri).ToList(); + } + if (source.IdentityProviderRestrictions != null) + { + destination.IdentityProviderRestrictions = source.IdentityProviderRestrictions.Select(x => x.Provider).ToList(); + } + } +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ApiResourceToISApiResourceMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.AllowedAccessTokenSigningAlgorithms))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.ApiSecrets))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.Properties))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.Scopes))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.UserClaims))] + public override partial IdentityServer4.Models.ApiResource Map(ApiResource source); + + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.AllowedAccessTokenSigningAlgorithms))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.ApiSecrets))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.Properties))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.Scopes))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiResource.UserClaims))] + public override partial void Map(ApiResource source, IdentityServer4.Models.ApiResource destination); + + public override void AfterMap(ApiResource source, IdentityServer4.Models.ApiResource destination) + { + destination.AllowedAccessTokenSigningAlgorithms = AllowedSigningAlgorithmsConverter.SplitToArray(source.AllowedAccessTokenSigningAlgorithms); + if (source.Properties != null) + { + destination.Properties = source.Properties.ToDictionary(x => x.Key, x => x.Value); + } + if (source.Secrets != null) + { + destination.ApiSecrets = source.Secrets.Select(x => new IdentityServer4.Models.Secret(x.Value, x.Expiration) { Type = x.Type, Description = x.Description }).ToList(); + } + if (source.UserClaims != null) + { + destination.UserClaims = source.UserClaims.Select(x => x.Type).ToList(); + } + if (source.Scopes != null) + { + destination.Scopes = source.Scopes.Select(x => x.Scope).ToList(); + } + } +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ApiScopeToISApiScopeMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiScope.UserClaims))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiScope.Properties))] + public override partial IdentityServer4.Models.ApiScope Map(ApiScope source); + + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiScope.UserClaims))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.ApiScope.Properties))] + public override partial void Map(ApiScope source, IdentityServer4.Models.ApiScope destination); + + public override void AfterMap(ApiScope source, IdentityServer4.Models.ApiScope destination) + { + if (source.Properties != null) + { + destination.Properties = source.Properties.ToDictionary(x => x.Key, x => x.Value); + } + if (source.UserClaims != null) + { + destination.UserClaims = source.UserClaims.Select(x => x.Type).ToList(); + } + } +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityResourceToISIdentityResourceMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityServer4.Models.IdentityResource.UserClaims))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.IdentityResource.Properties))] + public override partial IdentityServer4.Models.IdentityResource Map(IdentityResource source); + + [MapperIgnoreTarget(nameof(IdentityServer4.Models.IdentityResource.UserClaims))] + [MapperIgnoreTarget(nameof(IdentityServer4.Models.IdentityResource.Properties))] + public override partial void Map(IdentityResource source, IdentityServer4.Models.IdentityResource destination); + + public override void AfterMap(IdentityResource source, IdentityServer4.Models.IdentityResource destination) + { + if (source.Properties != null) + { + destination.Properties = source.Properties.ToDictionary(x => x.Key, x => x.Value); + } + if (source.UserClaims != null) + { + destination.UserClaims = source.UserClaims.Select(x => x.Type).ToList(); + } + } +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ClientToClientEtoMapper : MapperBase +{ + public override partial ClientEto Map(Client source); + public override partial void Map(Client source, ClientEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityResourceToIdentityResourceEtoMapper : MapperBase +{ + public override partial IdentityResourceEto Map(IdentityResource source); + public override partial void Map(IdentityResource source, IdentityResourceEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PersistedGrantToISPersistedGrantMapper : TwoWayMapperBase +{ + public override partial IdentityServer4.Models.PersistedGrant Map(PersistedGrant source); + public override partial void Map(PersistedGrant source, IdentityServer4.Models.PersistedGrant destination); + + public override PersistedGrant ReverseMap(IdentityServer4.Models.PersistedGrant source) + { + var entity = new PersistedGrant(System.Guid.Empty) + { + Key = source.Key, + Type = source.Type, + SubjectId = source.SubjectId, + SessionId = source.SessionId, + ClientId = source.ClientId, + Description = source.Description, + CreationTime = source.CreationTime, + Expiration = source.Expiration, + ConsumedTime = source.ConsumedTime, + Data = source.Data + }; + return entity; + } + + public override void ReverseMap(IdentityServer4.Models.PersistedGrant source, PersistedGrant destination) + { + destination.Key = source.Key; + destination.Type = source.Type; + destination.SubjectId = source.SubjectId; + destination.SessionId = source.SessionId; + destination.ClientId = source.ClientId; + destination.Description = source.Description; + destination.CreationTime = source.CreationTime; + destination.Expiration = source.Expiration; + destination.ConsumedTime = source.ConsumedTime; + destination.Data = source.Data; + } +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PersistedGrantToPersistedGrantEtoMapper : MapperBase +{ + public override partial PersistedGrantEto Map(PersistedGrant source); + public override partial void Map(PersistedGrant source, PersistedGrantEto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class DeviceFlowCodesToDeviceFlowCodesEtoMapper : MapperBase +{ + public override partial DeviceFlowCodesEto Map(DeviceFlowCodes source); + public override partial void Map(DeviceFlowCodes source, DeviceFlowCodesEto destination); +} + + diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.BlazorWASM/OpenIddict.Demo.Client.BlazorWASM.csproj b/modules/openiddict/app/OpenIddict.Demo.Client.BlazorWASM/OpenIddict.Demo.Client.BlazorWASM.csproj index 05f4b7ddab..5b22c22ac9 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.BlazorWASM/OpenIddict.Demo.Client.BlazorWASM.csproj +++ b/modules/openiddict/app/OpenIddict.Demo.Client.BlazorWASM/OpenIddict.Demo.Client.BlazorWASM.csproj @@ -7,7 +7,7 @@ - + diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.BlazorWASM/Program.cs b/modules/openiddict/app/OpenIddict.Demo.Client.BlazorWASM/Program.cs index a2bd2c85e3..11da671980 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.BlazorWASM/Program.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Client.BlazorWASM/Program.cs @@ -1,4 +1,4 @@ -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using OpenIddict.Demo.Client.BlazorWASM; diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.Console/OpenIddict.Demo.Client.Console.csproj b/modules/openiddict/app/OpenIddict.Demo.Client.Console/OpenIddict.Demo.Client.Console.csproj index 153955d3ad..c4ba8e6011 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.Console/OpenIddict.Demo.Client.Console.csproj +++ b/modules/openiddict/app/OpenIddict.Demo.Client.Console/OpenIddict.Demo.Client.Console.csproj @@ -8,7 +8,7 @@ - + diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.Console/Program.cs b/modules/openiddict/app/OpenIddict.Demo.Client.Console/Program.cs index 2e55cc24b1..a17e4ce5df 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.Console/Program.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Client.Console/Program.cs @@ -1,6 +1,6 @@ using System.Net.Http.Headers; using System.Text.Json; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations; const string email = "admin@abp.io"; @@ -75,6 +75,41 @@ Console.WriteLine("UserInfo: {0}", JsonSerializer.Serialize(JsonDocument.Parse(u })); Console.WriteLine(); +var tokenExchangeResponse = await client.RequestTokenExchangeTokenAsync(new TokenExchangeTokenRequest() +{ + Address = configuration.TokenEndpoint, + ClientId = clientId, + ClientSecret = clientSecret, + SubjectToken = refreshTokenResponse.AccessToken!, + SubjectTokenType = "urn:ietf:params:oauth:token-type:access_token", + Scope = "AbpAPI profile roles email phone offline_access", +}); + +if (tokenExchangeResponse.IsError) +{ + throw new Exception(tokenExchangeResponse.Error); +} + +Console.WriteLine("Token Exchange token: {0}", tokenExchangeResponse.AccessToken); +Console.WriteLine(); +Console.WriteLine("Token Exchange token: {0}", tokenExchangeResponse.RefreshToken); +Console.WriteLine(); + +userinfo = await client.GetUserInfoAsync(new UserInfoRequest() +{ + Address = configuration.UserInfoEndpoint, + Token = tokenExchangeResponse.AccessToken +}); +if (userinfo.IsError) +{ + throw new Exception(userinfo.Error); +} + +Console.WriteLine("Token Exchange UserInfo: {0}", JsonSerializer.Serialize(JsonDocument.Parse(userinfo.Raw), new JsonSerializerOptions +{ + WriteIndented = true +})); +Console.WriteLine(); var introspectionResponse = await client.IntrospectTokenAsync(new TokenIntrospectionRequest() { diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/OpenIddict.Demo.Client.Mvc.csproj b/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/OpenIddict.Demo.Client.Mvc.csproj index 981a815659..f0ab305f3e 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/OpenIddict.Demo.Client.Mvc.csproj +++ b/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/OpenIddict.Demo.Client.Mvc.csproj @@ -7,7 +7,7 @@ - + diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/Pages/Index.cshtml b/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/Pages/Index.cshtml index dc32cca59c..ea4a9f3347 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/Pages/Index.cshtml +++ b/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/Pages/Index.cshtml @@ -15,17 +15,38 @@ @if (HttpContext.User.Identity != null && HttpContext.User.Identity.IsAuthenticated) {
    +

    Current User

    @foreach (var claim in HttpContext.User.Claims) {
  • @claim.Type : @claim.Value
  • }
+
    +

    oidc

    + @{ + var oidc = await HttpContext.AuthenticateAsync("oidc"); + if (oidc.Principal != null) + { + foreach (var claim in oidc.Principal.Claims) + { +
  • @claim.Type : @claim.Value
  • + } + } + } +
+ +

HttpContext.GetTokenAsync("access_token")
@await HttpContext.GetTokenAsync("access_token")

+

HttpContext.GetTokenAsync("id_token") +
+ @await HttpContext.GetTokenAsync("id_token") +

+ var client = new HttpClient(); var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44303/api/claims"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await HttpContext.GetTokenAsync("access_token")); diff --git a/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/Program.cs b/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/Program.cs index 3f9afcfcbc..c0801d8635 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/Program.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Client.Mvc/Program.cs @@ -1,4 +1,4 @@ -using IdentityModel; +using Duende.IdentityModel; using OpenIddict.Demo.Client.Mvc; var builder = WebApplication.CreateBuilder(args); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/EntityFrameworkCore/ServerDataSeedContributor.cs b/modules/openiddict/app/OpenIddict.Demo.Server/EntityFrameworkCore/ServerDataSeedContributor.cs index c67b2977ef..e1fd97136b 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/EntityFrameworkCore/ServerDataSeedContributor.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Server/EntityFrameworkCore/ServerDataSeedContributor.cs @@ -79,6 +79,7 @@ public class ServerDataSeedContributor : IDataSeedContributor, ITransientDepende OpenIddictConstants.Permissions.GrantTypes.RefreshToken, OpenIddictConstants.Permissions.GrantTypes.DeviceCode, OpenIddictConstants.Permissions.GrantTypes.ClientCredentials, + OpenIddictConstants.Permissions.GrantTypes.TokenExchange, OpenIddictConstants.Permissions.Prefixes.GrantType + MyTokenExtensionGrant.ExtensionGrantName, OpenIddictConstants.Permissions.ResponseTypes.Code, diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250215074649_Initial.Designer.cs b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.Designer.cs similarity index 99% rename from modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250215074649_Initial.Designer.cs rename to modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.Designer.cs index fec1a9f143..1ba6189674 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250215074649_Initial.Designer.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace OpenIddict.Demo.Server.Migrations { [DbContext(typeof(ServerDbContext))] - [Migration("20250215074649_Initial")] + [Migration("20250710090114_Initial")] partial class Initial { /// @@ -22,7 +22,7 @@ namespace OpenIddict.Demo.Server.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "9.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -938,6 +938,9 @@ namespace OpenIddict.Demo.Server.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1161,8 +1164,8 @@ namespace OpenIddict.Demo.Server.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250215074649_Initial.cs b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.cs similarity index 99% rename from modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250215074649_Initial.cs rename to modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.cs index fdf8dac987..a992bf78c2 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250215074649_Initial.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/20250710090114_Initial.cs @@ -378,6 +378,7 @@ namespace OpenIddict.Demo.Server.Migrations RedirectUris = table.Column(type: "nvarchar(max)", nullable: true), Requirements = table.Column(type: "nvarchar(max)", nullable: true), Settings = table.Column(type: "nvarchar(max)", nullable: true), + FrontChannelLogoutUri = table.Column(type: "nvarchar(max)", nullable: true), ClientUri = table.Column(type: "nvarchar(max)", nullable: true), LogoUri = table.Column(type: "nvarchar(max)", nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), @@ -644,7 +645,7 @@ namespace OpenIddict.Demo.Server.Migrations ReferenceId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), - Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Type = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/ServerDbContextModelSnapshot.cs b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/ServerDbContextModelSnapshot.cs index 4099fccc43..b1caafb242 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/ServerDbContextModelSnapshot.cs +++ b/modules/openiddict/app/OpenIddict.Demo.Server/Migrations/ServerDbContextModelSnapshot.cs @@ -19,7 +19,7 @@ namespace OpenIddict.Demo.Server.Migrations #pragma warning disable 612, 618 modelBuilder .HasAnnotation("_Abp_DatabaseProvider", EfCoreDatabaseProvider.SqlServer) - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "9.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 128); SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); @@ -935,6 +935,9 @@ namespace OpenIddict.Demo.Server.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1158,8 +1161,8 @@ namespace OpenIddict.Demo.Server.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/modules/openiddict/app/OpenIddict.Demo.Server/Pages/Index.cshtml b/modules/openiddict/app/OpenIddict.Demo.Server/Pages/Index.cshtml index fd28464cc7..daa3ee9b95 100644 --- a/modules/openiddict/app/OpenIddict.Demo.Server/Pages/Index.cshtml +++ b/modules/openiddict/app/OpenIddict.Demo.Server/Pages/Index.cshtml @@ -4,7 +4,13 @@ ViewData["Title"] = "Home page"; } -
-

Welcome

-

Learn about building Web apps with ASP.NET Core.

-
\ No newline at end of file +@if (HttpContext.User.Identity != null && HttpContext.User.Identity.IsAuthenticated) +{ +
    +

    Current User

    + @foreach (var claim in HttpContext.User.Claims) + { +
  • @claim.Type : @claim.Value
  • + } +
+} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs index dddc89d515..3a9c8109fc 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictAspNetCoreModule.cs @@ -79,7 +79,8 @@ public class AbpOpenIddictAspNetCoreModule : AbpModule .AllowClientCredentialsFlow() .AllowRefreshTokenFlow() .AllowDeviceAuthorizationFlow() - .AllowNoneFlow(); + .AllowNoneFlow() + .AllowTokenExchangeFlow(); builder.RegisterScopes(new[] { diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictErrors.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictErrors.cs new file mode 100644 index 0000000000..45422ad210 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/AbpOpenIddictErrors.cs @@ -0,0 +1,8 @@ +namespace Volo.Abp.OpenIddict; + +public static class AbpOpenIddictErrors +{ + public const string AccountLocked = "account_locked"; + + public const string AccountInactive = "account_inactive"; +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs index 5c2f6ef996..6216571168 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/AuthorizeController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Text.Encodings.Web; @@ -43,7 +44,7 @@ public class AuthorizeController : AbpOpenIdDictControllerBase var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); if (result is not { Succeeded: true } || ((request.HasPromptValue(OpenIddictConstants.PromptValues.Login) || request.MaxAge is 0 || - (request.MaxAge != null && result.Properties?.IssuedUtc != null && + (request.MaxAge is not null && result.Properties?.IssuedUtc is not null && TimeProvider.System.GetUtcNow() - result.Properties.IssuedUtc > TimeSpan.FromSeconds(request.MaxAge.Value))) && TempData["IgnoreAuthenticationChallenge"] is null or false)) { @@ -148,6 +149,13 @@ public class AuthorizeController : AbpOpenIdDictControllerBase case OpenIddictConstants.ConsentTypes.Explicit when authorizations.Any() && !request.HasPromptValue(OpenIddictConstants.PromptValues.Consent): var principal = await SignInManager.CreateUserPrincipalAsync(user); + var sid = dynamicPrincipal.FindFirst(JwtRegisteredClaimNames.Sid); + if (sid != null) + { + principal.RemoveClaims(JwtRegisteredClaimNames.Sid); + principal.AddClaim(JwtRegisteredClaimNames.Sid, sid.Value); + } + if (result.Properties != null && result.Properties.IsPersistent) { var claim = new Claim(AbpClaimTypes.RememberMe, true.ToString()).SetDestinations(OpenIddictConstants.Destinations.AccessToken); @@ -247,6 +255,13 @@ public class AuthorizeController : AbpOpenIdDictControllerBase var principal = await SignInManager.CreateUserPrincipalAsync(user); + var sid = User.FindFirst(JwtRegisteredClaimNames.Sid); + if (sid != null) + { + principal.RemoveClaims(JwtRegisteredClaimNames.Sid); + principal.AddClaim(JwtRegisteredClaimNames.Sid, sid.Value); + } + var result = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); if (result.Succeeded && result.Properties != null && result.Properties.IsPersistent) { diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs index 33a1a37845..5059c7c5ca 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.Password.cs @@ -107,10 +107,13 @@ public partial class TokenController ClientId = request.ClientId }); + var errorCode = OpenIddictConstants.Errors.InvalidGrant; string errorDescription; + if (result.IsLockedOut) { Logger.LogInformation("Authentication failed for username: {username}, reason: locked out", request.Username); + errorCode = AbpOpenIddictErrors.AccountLocked; errorDescription = "The user account has been locked out due to invalid login attempts. Please wait a while and try again."; } else if (result.IsNotAllowed) @@ -139,7 +142,8 @@ public partial class TokenController return await HandleConfirmUserAsync(request, user); } - errorDescription = "You are not allowed to login! Your account is inactive."; + errorCode = AbpOpenIddictErrors.AccountInactive; + errorDescription = "You are not allowed to login! Your account is inactive or needs to confirm your email/phone number."; } } else @@ -150,7 +154,7 @@ public partial class TokenController var properties = new AuthenticationProperties(new Dictionary { - [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.Error] = errorCode, [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = errorDescription }); diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.TokenExchange.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.TokenExchange.cs new file mode 100644 index 0000000000..8f17a34be0 --- /dev/null +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.TokenExchange.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using OpenIddict.Abstractions; +using OpenIddict.Server.AspNetCore; + +namespace Volo.Abp.OpenIddict.Controllers; + +public partial class TokenController +{ + protected virtual async Task HandleTokenExchangeGrantTypeAsync(OpenIddictRequest request) + { + // Retrieve the claims principal stored in the subject token. + // + // Note: the principal may not represent a user (e.g if the token was issued during a client credentials token + // request and represents a client application): developers are strongly encouraged to ensure that the user + // and client identifiers are randomly generated so that a malicious client cannot impersonate a legit user. + // + // See https://datatracker.ietf.org/doc/html/rfc9068#SecurityConsiderations for more information. + var result = await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + + // If available, retrieve the claims principal stored in the actor token. + var actor = result.Properties?.GetParameter(OpenIddictServerAspNetCoreConstants.Properties.ActorTokenPrincipal); + + // Retrieve the user profile corresponding to the subject token. + var user = await UserManager.FindByIdAsync(result.Principal!.GetClaim(OpenIddictConstants.Claims.Subject)!); + if (user is null) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid." + })); + } + + // Ensure the user is still allowed to sign in. + if (!await PreSignInCheckAsync(user)) + { + return Forbid( + authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, + properties: new AuthenticationProperties(new Dictionary + { + [OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant, + [OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in." + })); + } + + // Note: whether the identity represents a delegated or impersonated access (or any other + // model) is entirely up to the implementer: to support all scenarios, OpenIddict doesn't + // enforce any specific constraint on the identity used for the sign-in operation and only + // requires that the standard "act" and "may_act" claims be valid JSON objects if present. + + // Clear the dynamic claims cache. + await IdentityDynamicClaimsPrincipalContributorCache.ClearAsync(user.Id, user.TenantId); + + // Create a new ClaimsPrincipal containing the claims that + // will be used to create an id_token, a token or a code. + var principal = await SignInManager.CreateUserPrincipalAsync(user); + + // Note: IdentityModel doesn't support serializing ClaimsIdentity.Actor to the + // standard "act" claim yet, which requires adding the "act" claim manually. + // + // For more information, see + // https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/pull/3219. + if (!string.IsNullOrEmpty(actor?.GetClaim(OpenIddictConstants.Claims.Subject)) && + !string.Equals(principal.GetClaim(OpenIddictConstants.Claims.Subject), actor.GetClaim(OpenIddictConstants.Claims.Subject), StringComparison.Ordinal)) + { + principal.SetClaim(OpenIddictConstants.Claims.Actor, new JsonObject + { + [OpenIddictConstants.Claims.Subject] = actor.GetClaim(OpenIddictConstants.Claims.Subject) + }); + } + + // Note: in this sample, the granted scopes match the requested scope + // but you may want to allow the user to uncheck specific scopes. + // For that, simply restrict the list of scopes before calling SetScopes. + principal.SetScopes(request.GetScopes()); + principal.SetResources(await GetResourcesAsync(request.GetScopes())); + + await OpenIddictClaimsPrincipalManager.HandleAsync(request, principal); + + // Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens. + return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme); + } +} diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.cs index e3b3d10c39..e348ffe007 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.AspNetCore/Volo/Abp/OpenIddict/Controllers/TokenController.cs @@ -42,6 +42,11 @@ public partial class TokenController : AbpOpenIdDictControllerBase return await HandleClientCredentialsAsync(request); } + if (request.IsTokenExchangeGrantType()) + { + return await HandleTokenExchangeGrantTypeAsync(request); + } + var extensionGrantsOptions = HttpContext.RequestServices.GetRequiredService>(); var extensionTokenGrant = extensionGrantsOptions.Value.Find(request.GrantType); if (extensionTokenGrant != null) diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Tokens/OpenIddictTokenConsts.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Tokens/OpenIddictTokenConsts.cs index c0e2674911..847cf0ef7a 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Tokens/OpenIddictTokenConsts.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain.Shared/Volo/Abp/OpenIddict/Tokens/OpenIddictTokenConsts.cs @@ -8,5 +8,5 @@ public class OpenIddictTokenConsts public static int SubjectMaxLength { get; set; } = 400; - public static int TypeMaxLength { get; set; } = 50; + public static int TypeMaxLength { get; set; } = 150; } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs index 1312ad5d8c..fd20f71012 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/AbpOpenIddictDomainModule.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using OpenIddict.Abstractions; +using OpenIddict.Core; using Volo.Abp.BackgroundWorkers; using Volo.Abp.Caching; using Volo.Abp.DistributedLocking; @@ -65,15 +66,15 @@ public class AbpOpenIddictDomainModule : AbpModule .SetDefaultTokenEntity(); builder - .AddApplicationStore() - .AddAuthorizationStore() - .AddScopeStore() - .AddTokenStore(); - - builder.ReplaceApplicationManager(typeof(AbpApplicationManager)); - builder.ReplaceAuthorizationManager(typeof(AbpAuthorizationManager)); - builder.ReplaceScopeManager(typeof(AbpScopeManager)); - builder.ReplaceTokenManager(typeof(AbpTokenManager)); + .ReplaceApplicationStore() + .ReplaceAuthorizationStore() + .ReplaceScopeStore() + .ReplaceTokenStore(); + + builder.ReplaceApplicationManager(); + builder.ReplaceAuthorizationManager(); + builder.ReplaceScopeManager(); + builder.ReplaceTokenManager(); builder.Services.TryAddScoped(provider => (IAbpApplicationManager)provider.GetRequiredService()); diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationDescriptor.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationDescriptor.cs index 3b5a4fc247..5fb2de1205 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationDescriptor.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationDescriptor.cs @@ -1,16 +1,22 @@ -using OpenIddict.Abstractions; +using System; +using OpenIddict.Abstractions; namespace Volo.Abp.OpenIddict.Applications; public class AbpApplicationDescriptor : OpenIddictApplicationDescriptor { + /// + /// Gets or sets the front-channel logout URI associated with the application. + /// + public virtual Uri FrontChannelLogoutUri { get; set; } + /// /// URI to further information about client. /// - public string ClientUri { get; set; } + public virtual string ClientUri { get; set; } /// /// URI to client logo. /// - public string LogoUri { get; set; } + public virtual string LogoUri { get; set; } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs index e02d671652..ea2283ea25 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpApplicationManager.cs @@ -17,7 +17,7 @@ public class AbpApplicationManager : OpenIddictApplicationManager cache, [NotNull] ILogger logger, [NotNull] IOptionsMonitor options, - [NotNull] IOpenIddictApplicationStoreResolver resolver, + [NotNull] IOpenIddictApplicationStore resolver, AbpOpenIddictIdentifierConverter identifierConverter) : base(cache, logger, options, resolver) { @@ -44,6 +44,17 @@ public class AbpApplicationManager : OpenIddictApplicationManager GetFrontChannelLogoutUriAsync(object application, CancellationToken cancellationToken = default) + { + Check.NotNull(application, nameof(application)); + Check.AssignableTo(application.GetType(), nameof(application)); + + return await Store.As().GetFrontChannelLogoutUriAsync(application.As(), cancellationToken); + } + public virtual async ValueTask GetClientUriAsync(object application, CancellationToken cancellationToken = default) { Check.NotNull(application, nameof(application)); - Check.AssignableTo(application.GetType(), nameof(application)); + Check.AssignableTo(application.GetType(), nameof(application)); return await Store.As().GetClientUriAsync(application.As(), cancellationToken); } @@ -71,8 +91,15 @@ public class AbpApplicationManager : OpenIddictApplicationManager GetLogoUriAsync(object application, CancellationToken cancellationToken = default) { Check.NotNull(application, nameof(application)); - Check.AssignableTo(application.GetType(), nameof(application)); + Check.AssignableTo(application.GetType(), nameof(application)); return await Store.As().GetLogoUriAsync(application.As(), cancellationToken); } + + protected virtual bool IsImplicitFileUri(Uri uri) + { + Check.NotNull(uri, nameof(uri)); + + return uri.IsAbsoluteUri && uri.IsFile && !uri.OriginalString.StartsWith(uri.Scheme, StringComparison.OrdinalIgnoreCase); + } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpOpenIddictApplicationStore.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpOpenIddictApplicationStore.cs index 416d90b3b7..f604a0960c 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpOpenIddictApplicationStore.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/AbpOpenIddictApplicationStore.cs @@ -635,6 +635,13 @@ public class AbpOpenIddictApplicationStore : AbpOpenIddictStoreBase GetFrontChannelLogoutUriAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default) + { + Check.NotNull(application, nameof(application)); + + return await new ValueTask(application.FrontChannelLogoutUri); + } + public virtual ValueTask GetClientUriAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default) { Check.NotNull(application, nameof(application)); diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/IAbpApplicationManager.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/IAbpApplicationManager.cs index 1f12a9d088..b0dd375908 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/IAbpApplicationManager.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/IAbpApplicationManager.cs @@ -6,6 +6,8 @@ namespace Volo.Abp.OpenIddict.Applications; public interface IAbpApplicationManager : IOpenIddictApplicationManager { + ValueTask GetFrontChannelLogoutUriAsync(object application, CancellationToken cancellationToken = default); + ValueTask GetClientUriAsync(object application, CancellationToken cancellationToken = default); ValueTask GetLogoUriAsync(object application, CancellationToken cancellationToken = default); diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/IAbpOpenIdApplicationStore.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/IAbpOpenIdApplicationStore.cs index ca2cd50102..9dd0b70515 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/IAbpOpenIdApplicationStore.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/IAbpOpenIdApplicationStore.cs @@ -6,6 +6,8 @@ namespace Volo.Abp.OpenIddict.Applications; public interface IAbpOpenIdApplicationStore : IOpenIddictApplicationStore { + ValueTask GetFrontChannelLogoutUriAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default); + ValueTask GetClientUriAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default); ValueTask GetLogoUriAsync(OpenIddictApplicationModel application, CancellationToken cancellationToken = default); diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplication.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplication.cs index e88370e874..d2f7b5f752 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplication.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplication.cs @@ -94,13 +94,18 @@ public class OpenIddictApplication : FullAuditedAggregateRoot /// public virtual string Settings { get; set; } + /// + /// Gets or sets the front-channel logout URI associated with the application. + /// + public virtual string FrontChannelLogoutUri { get; set; } + /// /// URI to further information about client. /// - public string ClientUri { get; set; } + public virtual string ClientUri { get; set; } /// /// URI to client logo. /// - public string LogoUri { get; set; } + public virtual string LogoUri { get; set; } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationExtensions.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationExtensions.cs index 791136316a..818a40f973 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationExtensions.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationExtensions.cs @@ -27,6 +27,7 @@ public static class OpenIddictApplicationExtensions RedirectUris = model.RedirectUris, Requirements = model.Requirements, Settings = model.Settings, + FrontChannelLogoutUri = model.FrontChannelLogoutUri, ClientUri = model.ClientUri, LogoUri = model.LogoUri }; @@ -59,6 +60,7 @@ public static class OpenIddictApplicationExtensions entity.RedirectUris = model.RedirectUris; entity.Requirements = model.Requirements; entity.Settings = model.Settings; + entity.FrontChannelLogoutUri = model.FrontChannelLogoutUri; entity.ClientUri = model.ClientUri; entity.LogoUri = model.LogoUri; @@ -100,6 +102,7 @@ public static class OpenIddictApplicationExtensions RedirectUris = entity.RedirectUris, Requirements = entity.Requirements, Settings = entity.Settings, + FrontChannelLogoutUri = entity.FrontChannelLogoutUri, ClientUri = entity.ClientUri, LogoUri = entity.LogoUri }; diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationModel.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationModel.cs index 48a376769f..6841b10a04 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationModel.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Applications/OpenIddictApplicationModel.cs @@ -89,13 +89,18 @@ public class OpenIddictApplicationModel : ExtensibleObject /// public virtual string Settings { get; set; } + /// + /// Gets or sets the front-channel logout URI associated with the application. + /// + public virtual string FrontChannelLogoutUri { get; set; } + /// /// URI to further information about client. /// - public string ClientUri { get; set; } + public virtual string ClientUri { get; set; } /// /// URI to client logo. /// - public string LogoUri { get; set; } + public virtual string LogoUri { get; set; } } diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Authorizations/AbpAuthorizationManager.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Authorizations/AbpAuthorizationManager.cs index d190192058..50ec2866ec 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Authorizations/AbpAuthorizationManager.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Authorizations/AbpAuthorizationManager.cs @@ -16,7 +16,7 @@ public class AbpAuthorizationManager : OpenIddictAuthorizationManager cache, [NotNull] [ItemNotNull] ILogger> logger, [NotNull] [ItemNotNull] IOptionsMonitor options, - [NotNull] IOpenIddictAuthorizationStoreResolver resolver, + [NotNull] IOpenIddictAuthorizationStore resolver, AbpOpenIddictIdentifierConverter identifierConverter) : base(cache, logger, options, resolver) { diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Scopes/AbpScopeManager.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Scopes/AbpScopeManager.cs index 76596c159b..0f89463733 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Scopes/AbpScopeManager.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Scopes/AbpScopeManager.cs @@ -16,7 +16,7 @@ public class AbpScopeManager : OpenIddictScopeManager [NotNull] [ItemNotNull] IOpenIddictScopeCache cache, [NotNull] [ItemNotNull] ILogger> logger, [NotNull] [ItemNotNull] IOptionsMonitor options, - [NotNull] IOpenIddictScopeStoreResolver resolver, + [NotNull] IOpenIddictScopeStore resolver, AbpOpenIddictIdentifierConverter identifierConverter) : base(cache, logger, options, resolver) { diff --git a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Tokens/AbpTokenManager.cs b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Tokens/AbpTokenManager.cs index 7f4f967124..879d5b3f4f 100644 --- a/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Tokens/AbpTokenManager.cs +++ b/modules/openiddict/src/Volo.Abp.OpenIddict.Domain/Volo/Abp/OpenIddict/Tokens/AbpTokenManager.cs @@ -16,7 +16,7 @@ public class AbpTokenManager : OpenIddictTokenManager [NotNull] [ItemNotNull] IOpenIddictTokenCache cache, [NotNull] [ItemNotNull] ILogger> logger, [NotNull] [ItemNotNull] IOptionsMonitor options, - [NotNull] IOpenIddictTokenStoreResolver resolver, + [NotNull] IOpenIddictTokenStore resolver, AbpOpenIddictIdentifierConverter identifierConverter) : base(cache, logger, options, resolver) { diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs index 28abc4d75b..e628c39644 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Application/Volo/Abp/PermissionManagement/PermissionAppService.cs @@ -9,6 +9,7 @@ using Volo.Abp.Authorization.Permissions; using Volo.Abp.Localization; using Volo.Abp.MultiTenancy; using Volo.Abp.SimpleStateChecking; +using Volo.Abp.PermissionManagement.Localization; namespace Volo.Abp.PermissionManagement; @@ -26,6 +27,9 @@ public class PermissionAppService : ApplicationService, IPermissionAppService IOptions options, ISimpleStateCheckerManager simpleStateCheckerManager) { + LocalizationResource = typeof(AbpPermissionManagementResource); + ObjectMapperContext = typeof(AbpPermissionManagementApplicationModule); + Options = options.Value; PermissionManager = permissionManager; PermissionDefinitionManager = permissionDefinitionManager; diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/AbpPermissionManagementBlazorModule.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/AbpPermissionManagementBlazorModule.cs index 1e7e69d4c3..56d01956e4 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/AbpPermissionManagementBlazorModule.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/AbpPermissionManagementBlazorModule.cs @@ -1,6 +1,5 @@ using Localization.Resources.AbpUi; using Volo.Abp.AspNetCore.Components.Web.Theming; -using Volo.Abp.AutoMapper; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.Localization; @@ -9,7 +8,6 @@ namespace Volo.Abp.PermissionManagement.Blazor; [DependsOn( typeof(AbpAspNetCoreComponentsWebThemingModule), - typeof(AbpAutoMapperModule), typeof(AbpPermissionManagementApplicationContractsModule) )] public class AbpPermissionManagementBlazorModule : AbpModule diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Volo.Abp.PermissionManagement.Blazor.csproj b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Volo.Abp.PermissionManagement.Blazor.csproj index 4983eaa9f8..1506afa4eb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Volo.Abp.PermissionManagement.Blazor.csproj +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Blazor/Volo.Abp.PermissionManagement.Blazor.csproj @@ -9,7 +9,6 @@ - diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionStore.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionStore.cs index 85ffd77924..49e956ad1b 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionStore.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/PermissionStore.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -175,13 +176,15 @@ public class PermissionStore : IPermissionStore, ITransientDependency { using (PermissionGrantRepository.DisableTracking()) { + var permissionNames = new HashSet(notCacheKeys.Select(GetPermissionNameFormCacheKeyOrNull)); var permissions = (await PermissionDefinitionManager.GetPermissionsAsync()) - .Where(x => notCacheKeys.Any(k => GetPermissionNameFormCacheKeyOrNull(k) == x.Name)).ToList(); + .Where(x => permissionNames.Contains(x.Name)) + .ToList(); Logger.LogDebug($"Getting not cache granted permissions from the repository for this provider name,key: {providerName},{providerKey}"); var grantedPermissionsHashSet = new HashSet( - (await PermissionGrantRepository.GetListAsync(notCacheKeys.Select(GetPermissionNameFormCacheKeyOrNull).ToArray(), providerName, providerKey)).Select(p => p.Name) + (await PermissionGrantRepository.GetListAsync(permissionNames.ToArray(), providerName, providerKey)).Select(p => p.Name) ); Logger.LogDebug($"Setting the cache items. Count: {permissions.Count}"); diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebAutoMapperProfile.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebAutoMapperProfile.cs deleted file mode 100644 index f9baa8befd..0000000000 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebAutoMapperProfile.cs +++ /dev/null @@ -1,18 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Abp.PermissionManagement.Web.Pages.AbpPermissionManagement; - -namespace Volo.Abp.PermissionManagement.Web; - -public class AbpPermissionManagementWebAutoMapperProfile : Profile -{ - public AbpPermissionManagementWebAutoMapperProfile() - { - CreateMap().Ignore(p => p.IsAllPermissionsGranted); - - CreateMap() - .ForMember(p => p.Depth, opts => opts.Ignore()); - - CreateMap(); - } -} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebMappers.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebMappers.cs new file mode 100644 index 0000000000..70e738d243 --- /dev/null +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebMappers.cs @@ -0,0 +1,33 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using static Volo.Abp.PermissionManagement.Web.Pages.AbpPermissionManagement.PermissionManagementModal; + +namespace Volo.Abp.PermissionManagement.Web; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PermissionGroupDtoToPermissionGroupViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(PermissionGroupViewModel.IsAllPermissionsGranted))] + public override partial PermissionGroupViewModel Map(PermissionGroupDto source); + + [MapperIgnoreTarget(nameof(PermissionGroupViewModel.IsAllPermissionsGranted))] + public override partial void Map(PermissionGroupDto source, PermissionGroupViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class PermissionGrantInfoDtoToPermissionGrantInfoViewModelMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(PermissionGrantInfoViewModel.Depth))] + public override partial PermissionGrantInfoViewModel Map(PermissionGrantInfoDto source); + + [MapperIgnoreTarget(nameof(PermissionGrantInfoViewModel.Depth))] + public override partial void Map(PermissionGrantInfoDto source, PermissionGrantInfoViewModel destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class ProviderInfoDtoToProviderInfoViewModelMapper : MapperBase +{ + public override partial ProviderInfoViewModel Map(ProviderInfoDto source); + + public override partial void Map(ProviderInfoDto source, ProviderInfoViewModel destination); +} \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebModule.cs b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebModule.cs index 5219fafbbf..2159a6107c 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebModule.cs +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/AbpPermissionManagementWebModule.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; -using Volo.Abp.AutoMapper; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement.Localization; using Volo.Abp.VirtualFileSystem; @@ -11,7 +11,7 @@ namespace Volo.Abp.PermissionManagement.Web; [DependsOn(typeof(AbpPermissionManagementApplicationContractsModule))] [DependsOn(typeof(AbpAspNetCoreMvcUiBootstrapModule))] -[DependsOn(typeof(AbpAutoMapperModule))] +[DependsOn(typeof(AbpMapperlyModule))] public class AbpPermissionManagementWebModule : AbpModule { public override void PreConfigureServices(ServiceConfigurationContext context) @@ -34,11 +34,7 @@ public class AbpPermissionManagementWebModule : AbpModule options.FileSets.AddEmbedded(); }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Volo.Abp.PermissionManagement.Web.csproj b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Volo.Abp.PermissionManagement.Web.csproj index 1c80dda96c..9f58d70303 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Volo.Abp.PermissionManagement.Web.csproj +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Volo.Abp.PermissionManagement.Web.csproj @@ -31,7 +31,7 @@ - + diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/AbpSettingManagementBlazorModule.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/AbpSettingManagementBlazorModule.cs index d8e54353e1..f76b5fdb26 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/AbpSettingManagementBlazorModule.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/AbpSettingManagementBlazorModule.cs @@ -2,8 +2,8 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Components.Web.Theming; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; -using Volo.Abp.AutoMapper; using Volo.Abp.Localization; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.SettingManagement.Blazor.Menus; using Volo.Abp.SettingManagement.Blazor.Settings; @@ -13,7 +13,7 @@ using Volo.Abp.UI.Navigation; namespace Volo.Abp.SettingManagement.Blazor; [DependsOn( - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAspNetCoreComponentsWebThemingModule), typeof(AbpSettingManagementApplicationContractsModule) )] @@ -21,12 +21,7 @@ public class AbpSettingManagementBlazorModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor index 799ba049a4..ad571fb448 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor @@ -4,7 +4,9 @@ @using Volo.Abp.Features @attribute [Authorize] @attribute [RequiresFeature(SettingManagementFeatures.Enable)] - +@using Microsoft.Extensions.Localization +@using Volo.Abp.UI.Navigation.Localization.Resource +@inject IStringLocalizer LUiNavigation @* ************************* PAGE HEADER ************************* *@ diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor.cs index 1e43688ce0..1b443fc827 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor.cs @@ -31,7 +31,8 @@ public partial class SettingManagement protected async override Task OnInitializedAsync() { - BreadcrumbItems.Add(new BreadcrumbItem(@L["Settings"])); + BreadcrumbItems.Add(new BreadcrumbItem(LUiNavigation["Menu:Administration"].Value)); + BreadcrumbItems.Add(new BreadcrumbItem(@L["Menu:Settings"].Value)); SettingComponentCreationContext = new SettingComponentCreationContext(ServiceProvider); diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/SettingManagementBlazorAutoMapperProfile.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/SettingManagementBlazorAutoMapperProfile.cs deleted file mode 100644 index 604996e478..0000000000 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/SettingManagementBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,15 +0,0 @@ -using AutoMapper; -using Volo.Abp.SettingManagement.Blazor.Pages.SettingManagement.EmailSettingGroup; - -namespace Volo.Abp.SettingManagement.Blazor; - -public class SettingManagementBlazorAutoMapperProfile : Profile -{ - public SettingManagementBlazorAutoMapperProfile() - { - CreateMap(); - CreateMap(); - - CreateMap(); - } -} diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/SettingManagementBlazorMappers.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/SettingManagementBlazorMappers.cs new file mode 100644 index 0000000000..d5364d8e4d --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/SettingManagementBlazorMappers.cs @@ -0,0 +1,31 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using static Volo.Abp.SettingManagement.Blazor.Pages.SettingManagement.EmailSettingGroup.EmailSettingGroupViewComponent; + +namespace Volo.Abp.SettingManagement.Blazor; + +[Mapper] +public partial class UpdateEmailSettingsViewModelToUpdateEmailSettingsDtoMapper : MapperBase +{ + public override partial UpdateEmailSettingsDto Map(UpdateEmailSettingsViewModel source); + + public override partial void Map(UpdateEmailSettingsViewModel source, UpdateEmailSettingsDto destination); +} + + +[Mapper] +public partial class EmailSettingsDtoToUpdateEmailSettingsViewModelMapper : MapperBase +{ + public override partial UpdateEmailSettingsViewModel Map(EmailSettingsDto source); + + public override partial void Map(EmailSettingsDto source, UpdateEmailSettingsViewModel destination); +} + +[Mapper] +public partial class SendTestEmailViewModelToSendTestEmailInputMapper : MapperBase +{ + public override partial SendTestEmailInput Map(SendTestEmailViewModel source); + + public override partial void Map(SendTestEmailViewModel source, SendTestEmailInput destination); +} + diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Volo.Abp.SettingManagement.Blazor.csproj b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Volo.Abp.SettingManagement.Blazor.csproj index 61045cab96..e045dbf4bb 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Volo.Abp.SettingManagement.Blazor.csproj +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Volo.Abp.SettingManagement.Blazor.csproj @@ -9,7 +9,7 @@ - + diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json index ec35f5286f..44077ac44f 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ar.json @@ -17,6 +17,7 @@ "SentSuccessfully": "أرسلت بنجاح", "MailSendingFailed": "فشل إرسال البريد ، يرجى التحقق من تكوين البريد الإلكتروني الخاص بك والمحاولة مرة أخرى.", "Send": "يرسل", + "Menu:Settings": "الإعدادات", "Menu:Emailing": "إرسال بالبريد الإلكتروني", "Menu:TimeZone": "وحدة زمنية", "DisplayName:Timezone": "وحدة زمنية", @@ -36,6 +37,6 @@ "Feature:SettingManagementEnableDescription": "تفعيل إعداد نظام الإدارة في التطبيق.", "Feature:AllowChangingEmailSettings": "السماح لتغيير إعدادات البريد الإلكتروني.", "Feature:AllowChangingEmailSettingsDescription": "السماح لتغيير إعدادات البريد الإلكتروني.", - "SmtpPasswordPlaceholder": "أدخل قيمة لتحديث كلمة المرور", + "SmtpPasswordPlaceholder": "أدخل قيمة لتحديث كلمة المرور" } } \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json index cd9d4d78d1..94a6e1f6ea 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/cs.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Úspěšně odesláno", "MailSendingFailed": "Odesílání e-mailu se nezdařilo. Zkontrolujte konfiguraci e-mailu a zkuste to znovu.", "Send": "Poslat", + "Menu:Settings": "Nastavení", "Menu:Emailing": "Zasílání e-mailem", "Menu:TimeZone": "Časové Pásmo", "DisplayName:Timezone": "Časové pásmo", @@ -36,6 +37,6 @@ "Feature:SettingManagementEnableDescription": "Povolit systém správy nastavení v aplikaci.", "Feature:AllowChangingEmailSettings": "Povolit změnu nastavení e-mailu.", "Feature:AllowChangingEmailSettingsDescription": "Povolit změnu nastavení e-mailu.", - "SmtpPasswordPlaceholder": "Zadejte hodnotu pro aktualizaci hesla", + "SmtpPasswordPlaceholder": "Zadejte hodnotu pro aktualizaci hesla" } } \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json index 6e17d1c0dc..a63b7201f2 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de-DE.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Erfolgreich gesendet", "MailSendingFailed": "E-Mail-Versand fehlgeschlagen. Bitte überprüfen Sie Ihre E-Mail-Konfiguration und versuchen Sie es erneut.", "Send": "Senden", + "Menu:Settings": "Einstellungen", "Menu:Emailing": "E-Mail", "Menu:TimeZone": "Zeitzone", "DisplayName:Timezone": "Zeitzone", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json index 7f8debc508..e78aa19fca 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/de.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Erfolgreich gesendet", "MailSendingFailed": "E-Mail-Versand fehlgeschlagen. Bitte überprüfen Sie Ihre E-Mail-Konfiguration und versuchen Sie es erneut.", "Send": "Schicken", + "Menu:Settings": "Einstellungen", "Menu:Emailing": "E-Mail senden", "Menu:TimeZone": "Zeitzone", "DisplayName:Timezone": "Zeitzone", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json index 611cfb407d..424e38e8de 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/el.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Στάλθηκε με επιτυχία", "MailSendingFailed": "Αποτυχία αποστολής email. Ελέγξτε τη διαμόρφωση του email σας και δοκιμάστε ξανά.", "Send": "Αποστολή", + "Menu:Settings": "Ρυθμίσεις", "Menu:Emailing": "Αποστολή email", "SmtpHost": "Διακομιστής", "SmtpPort": "Πόρτα", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json index 84933fe44b..832163574b 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/en.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Sent successfully", "MailSendingFailed": "Mail sending failed, please check your email configuration and try again.", "Send": "Send", + "Menu:Settings": "Settings", "Menu:Emailing": "Emailing", "Menu:TimeZone": "Time Zone", "DisplayName:Timezone": "Time zone", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json index bfc0ab6a3b..91cc71d532 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/es.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Enviado exitosamente", "MailSendingFailed": "Error al enviar el correo, por favor revise su configuración de correo y vuelva a intentarlo.", "Send": "Enviar", + "Menu:Settings": "Configuraciones", "Menu:Emailing": "Configuración", "Menu:TimeZone": "Zona Horaria", "DisplayName:Timezone": "Zona horaria", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fa.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fa.json index 1a07a34adf..800ac04a66 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fa.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fa.json @@ -5,6 +5,7 @@ "SavedSuccessfully": "با موفقیت ذخیره شد", "Permission:SettingManagement": "مدیریت تنظیمات", "Permission:Emailing": "تنظیمات ایمیل", + "Menu:Settings": "تنظیمات", "Menu:Emailing": "تنظیمات ایمیل", "SmtpHost": "هاست", "SmtpPort": "پورت", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json index f9b52baf2b..8e7efcb8c6 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fi.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Lähetetty onnistuneesti", "MailSendingFailed": "Sähköpostin lähetys epäonnistui. Tarkista sähköpostiasetuksesi ja yritä uudelleen.", "Send": "Lähetä", + "Menu:Settings": "Asetukset", "Menu:Emailing": "Sähköpostiviestit", "Menu:TimeZone": "Aikavyöhyke", "DisplayName:Timezone": "Aikavyöhyke", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json index 758e9a090c..1844cc3fbb 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/fr.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Envoyé avec succès", "MailSendingFailed": "L'envoi de courrier a échoué, veuillez vérifier votre configuration de courrier électronique et réessayer.", "Send": "Envoyer", + "Menu:Settings": "Paramètres", "Menu:Emailing": "Envoi par e-mail", "Menu:TimeZone": "Fuseau Horaire", "DisplayName:Timezone": "Fuseau horaire", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hi.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hi.json index 8dacb200e4..8c00ffca0d 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hi.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hi.json @@ -17,6 +17,7 @@ "SentSuccessfully": "सफलतापूर्वक भेज दिया गया", "MailSendingFailed": "मेल भेजने में विफल, कृपया अपनी ईमेल विन्यास की जाँच करें और पुनः प्रयास करें।", "Send": "भेजना", + "Menu:Settings": "समायोजन", "Menu:Emailing": "ईमेल से भेजना", "Menu:TimeZone": "समय क्षेत्र", "DisplayName:Timezone": "समय क्षेत्र", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json index 133233a10f..c0979707a4 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hr.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Poslan uspješno", "MailSendingFailed": "Slanje e-pošte nije uspjelo, provjerite konfiguraciju e-pošte i pokušajte ponovo.", "Send": "Poslati", + "Menu:Settings": "Postavke", "Menu:Emailing": "Slanje e-poštom", "Menu:TimeZone": "Vremenska Zona", "DisplayName:Timezone": "Vremenska zona", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json index 4aed2b5688..efe7876d8d 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/hu.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Sikeresen elküldve", "MailSendingFailed": "Az e-mail küldése sikertelen, ellenőrizze az e-mail konfigurációját, és próbálja újra.", "Send": "Küld", + "Menu:Settings": "Beállítások", "Menu:Emailing": "E-mailezés", "Menu:TimeZone": "Időzóna", "DisplayName:Timezone": "Időzóna", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json index 2106b156bc..73c3c736a9 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/is.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Tókst að senda", "MailSendingFailed": "Tölvupóstur sendist ekki. Athugaðu tölvupóst stillingar þínar og reyndu aftur.", "Send": "Senda", + "Menu:Settings": "Stillingar", "Menu:Emailing": "Senda tölvupóst", "Menu:TimeZone": "Tímabelti", "DisplayName:Timezone": "Tímabelti", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json index 487078c449..cc6669e7c8 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/it.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Inviato con successo", "MailSendingFailed": "Invio della posta fallito, controlla la tua configurazione email e riprova.", "Send": "Inviare", + "Menu:Settings": "Impostazioni", "Menu:Emailing": "Invio di e-mail", "Menu:TimeZone": "Fuso Orario", "DisplayName:Timezone": "Fuso orario", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json index 06f07346e5..f6d86ab9ba 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/nl.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Succesvol verzonden", "MailSendingFailed": "E-mail verzenden mislukt. Controleer uw e-mailconfiguratie en probeer het opnieuw.", "Send": "Versturen", + "Menu:Settings": "Instellingen", "Menu:Emailing": "E-mail", "Menu:TimeZone": "Tijdzone", "DisplayName:Timezone": "Tijdzone", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json index 35038ee043..45c144626d 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pl-PL.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Wysłano pomyślnie", "MailSendingFailed": "Wysyłanie e-maila nie powiodło się. Sprawdź konfigurację e-maila i spróbuj ponownie.", "Send": "Wysłać", + "Menu:Settings": "Ustawienia", "Menu:Emailing": "Wysyłanie e-maili", "Menu:TimeZone": "Strefa Czasowa", "DisplayName:Timezone": "Strefa czasowa", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json index 9a8cf0317b..831d54707e 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/pt-BR.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Enviado com sucesso", "MailSendingFailed": "Falha no envio de e-mail, verifique sua configuração de e-mail e tente novamente.", "Send": "Enviar", + "Menu:Settings": "Configurações", "Menu:Emailing": "Enviando por e-mail", "Menu:TimeZone": "Fuso Horário", "DisplayName:Timezone": "Fuso horário", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json index 8029a43734..a0f94747ed 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ro-RO.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Trimis cu succes", "MailSendingFailed": "Trimiterea e-mailului a eșuat. Verificați configurația e-mailului și încercați din nou.", "Send": "Trimite", + "Menu:Settings": "Setări", "Menu:Emailing": "Emailing", "Menu:TimeZone": "Fus Orar", "DisplayName:Timezone": "Fus orar", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json index 0ab1535a3b..1145595e8e 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/ru.json @@ -17,6 +17,7 @@ "SentSuccessfully": "отправлено успешно", "MailSendingFailed": "Не удалось отправить письмо. Пожалуйста, проверьте настройки электронной почты и повторите попытку.", "Send": "Отправлять", + "Menu:Settings": "Настройки", "Menu:Emailing": "Отправка по электронной почте", "Menu:TimeZone": "Часовой пояс", "DisplayName:Timezone": "Часовой пояс", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json index 1c68a43ae5..e8661a3471 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sk.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Úspešne odoslané", "MailSendingFailed": "Odoslanie emailu zlyhalo. Skontrolujte konfiguráciu emailu a skúste to znova.", "Send": "Odoslať", + "Menu:Settings": "Nastavenia", "Menu:Emailing": "Posielanie emailov", "Menu:TimeZone": "Časové Pásmo", "DisplayName:Timezone": "Časové pásmo", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json index 4805b28994..9bd877053b 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sl.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Poslano uspešno", "MailSendingFailed": "Pošiljanje e-pošte ni uspelo, preverite konfiguracijo e-pošte in poskusite znova.", "Send": "Pošlji", + "Menu:Settings": "Nastavitve", "Menu:Emailing": "Pošiljanje po e-pošti", "Menu:TimeZone": "Časovni Pas", "DisplayName:Timezone": "Časovni pas", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json index ebbdad2b32..01719059cc 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/sv.json @@ -16,6 +16,7 @@ "TestEmailBody": "Testa e-postmeddelandets brödtext här", "SentSuccessfully": "Skickat framgångsrikt", "Send": "Skicka", + "Menu:Settings": "Inställningar", "Menu:Emailing": "E-post", "Menu:TimeZone": "Tidszon", "DisplayName:Timezone": "Tidszon", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json index 3b7fc2ac5f..ff4107e21b 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/tr.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Gönderildi", "MailSendingFailed": "E-posta gönderme başarısız, lütfen e-posta yapılandırmanızı kontrol edin ve tekrar deneyin.", "Send": "Gönder", + "Menu:Settings": "Ayarlar", "Menu:Emailing": "Email", "Menu:TimeZone": "Zaman Dilimi", "DisplayName:Timezone": "Zaman dilimi", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json index a9cc466fdb..38b284a6ca 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/vi.json @@ -17,6 +17,7 @@ "SentSuccessfully": "Gửi thành công", "MailSendingFailed": "Gửi email thất bại. Vui lòng kiểm tra cấu hình email của bạn và thử lại.", "Send": "Gửi", + "Menu:Settings": "Cài đặt", "Menu:Emailing": "Gửi email", "Menu:TimeZone": "Múi Giờ", "DisplayName:Timezone": "Múi giờ", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json index 3aad0b9e8a..b9d0940273 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hans.json @@ -17,6 +17,7 @@ "SentSuccessfully": "发送成功", "MailSendingFailed": "邮件发送失败,请检查您的电子邮件配置并重试。", "Send": "发送", + "Menu:Settings": "设置", "Menu:Emailing": "邮件", "Menu:TimeZone": "时区", "DisplayName:Timezone": "时区", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json index 77c586efe4..b57a542842 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain.Shared/Volo/Abp/SettingManagement/Localization/Resources/AbpSettingManagement/zh-Hant.json @@ -17,6 +17,7 @@ "SentSuccessfully": "發送成功", "MailSendingFailed": "郵件發送失敗,請檢查你的郵件配置並重試.", "Send": "發送", + "Menu:Settings": "設置", "Menu:Emailing": "信箱", "Menu:TimeZone": "時區", "DisplayName:Timezone": "時區", diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs index 4e892abded..6f11da24e5 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Domain/Volo/Abp/SettingManagement/SettingManagementStore.cs @@ -176,9 +176,10 @@ public class SettingManagementStore : ISettingManagementStore, ITransientDepende string providerKey, List notCacheKeys) { - var settingDefinitions = (await SettingDefinitionManager.GetAllAsync()).Where(x => notCacheKeys.Any(k => GetSettingNameFormCacheKeyOrNull(k) == x.Name)); + var settingNames = new HashSet(notCacheKeys.Select(GetSettingNameFormCacheKeyOrNull)); + var settingDefinitions = (await SettingDefinitionManager.GetAllAsync()).Where(x => settingNames.Contains(x.Name)); - var settingsDictionary = (await SettingRepository.GetListAsync(notCacheKeys.Select(GetSettingNameFormCacheKeyOrNull).ToArray(), providerName, providerKey)) + var settingsDictionary = (await SettingRepository.GetListAsync(settingNames.ToArray(), providerName, providerKey)) .ToDictionary(s => s.Name, s => s.Value); var cacheItems = new List>(); diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/AbpSettingManagementWebModule.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/AbpSettingManagementWebModule.cs index b5d3df17ad..1bc204f316 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/AbpSettingManagementWebModule.cs +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/AbpSettingManagementWebModule.cs @@ -1,8 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; -using Volo.Abp.AutoMapper; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.SettingManagement.Localization; using Volo.Abp.SettingManagement.Web.Navigation; @@ -15,7 +15,7 @@ namespace Volo.Abp.SettingManagement.Web; [DependsOn( typeof(AbpSettingManagementApplicationContractsModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule), typeof(AbpSettingManagementDomainSharedModule) )] @@ -58,10 +58,6 @@ public class AbpSettingManagementWebModule : AbpModule options.DisableModule(SettingManagementRemoteServiceConsts.ModuleName); }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.cshtml b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.cshtml index d31670a036..b259131ec7 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.cshtml +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Pages/SettingManagement/Index.cshtml @@ -4,11 +4,15 @@ @using Volo.Abp.SettingManagement.Localization @using Volo.Abp.SettingManagement.Web.Navigation @using Volo.Abp.SettingManagement.Web.Pages.SettingManagement +@using Volo.Abp.UI.Navigation.Localization.Resource @model IndexModel @inject IHtmlLocalizer L +@inject IHtmlLocalizer LUiNavigation @inject IPageLayout PageLayout @{ PageLayout.Content.Title = L["Settings"].Value; + + PageLayout.Content.BreadCrumb.Add(LUiNavigation["Menu:Administration"].Value); PageLayout.Content.MenuItemName = SettingManagementMenuNames.GroupName; } @section scripts { diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/SettingManagementWebAutoMapperProfile.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/SettingManagementWebAutoMapperProfile.cs deleted file mode 100644 index 77afa99224..0000000000 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/SettingManagementWebAutoMapperProfile.cs +++ /dev/null @@ -1,14 +0,0 @@ -using AutoMapper; -using Volo.Abp.SettingManagement.Web.Pages.SettingManagement.Components.EmailSettingGroup; - -namespace Volo.Abp.SettingManagement.Web; - -public class SettingManagementWebAutoMapperProfile : Profile -{ - public SettingManagementWebAutoMapperProfile() - { - CreateMap(); - - CreateMap(); - } -} \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/SettingManagementWebMappers.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/SettingManagementWebMappers.cs new file mode 100644 index 0000000000..8a205919c9 --- /dev/null +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/SettingManagementWebMappers.cs @@ -0,0 +1,22 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using static Volo.Abp.SettingManagement.Web.Pages.SettingManagement.Components.EmailSettingGroup.EmailSettingGroupViewComponent; +using static Volo.Abp.SettingManagement.Web.Pages.SettingManagement.Components.EmailSettingGroup.SendTestEmailModal; + +namespace Volo.Abp.SettingManagement.Web; + +[Mapper] +public partial class EmailSettingsDtoToUpdateEmailSettingsViewModelMapper : MapperBase +{ + public override partial UpdateEmailSettingsViewModel Map(EmailSettingsDto source); + + public override partial void Map(EmailSettingsDto source, UpdateEmailSettingsViewModel destination); +} + +[Mapper] +public partial class SendTestEmailViewModelToSendTestEmailInputMapper : MapperBase +{ + public override partial SendTestEmailInput Map(SendTestEmailViewModel source); + + public override partial void Map(SendTestEmailViewModel source, SendTestEmailInput destination); +} \ No newline at end of file diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Volo.Abp.SettingManagement.Web.csproj b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Volo.Abp.SettingManagement.Web.csproj index 182fbb46cd..49fe6fe0b2 100644 --- a/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Volo.Abp.SettingManagement.Web.csproj +++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Web/Volo.Abp.SettingManagement.Web.csproj @@ -15,7 +15,7 @@ - + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationAutoMapperProfile.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationAutoMapperProfile.cs deleted file mode 100644 index e7db0c19b9..0000000000 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,12 +0,0 @@ -using AutoMapper; - -namespace Volo.Abp.TenantManagement; - -public class AbpTenantManagementApplicationAutoMapperProfile : Profile -{ - public AbpTenantManagementApplicationAutoMapperProfile() - { - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationMapperlyMappers.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationMapperlyMappers.cs new file mode 100644 index 0000000000..c3f44f2129 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationMapperlyMappers.cs @@ -0,0 +1,13 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.TenantManagement.Application.Volo.Abp.TenantManagement; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class TenantToTenantDtoMapper + : MapperBase +{ + public override partial TenantDto Map(Tenant source); + public override partial void Map(Tenant source, TenantDto destination); +} \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationModule.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationModule.cs index 2583c06458..4f51d28f82 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationModule.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Application/Volo/Abp/TenantManagement/AbpTenantManagementApplicationModule.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Application; -using Volo.Abp.AutoMapper; using Volo.Abp.Modularity; namespace Volo.Abp.TenantManagement; @@ -12,10 +11,6 @@ public class AbpTenantManagementApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorAutoMapperProfile.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorAutoMapperProfile.cs deleted file mode 100644 index 28ad6e9b8a..0000000000 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,12 +0,0 @@ -using AutoMapper; - -namespace Volo.Abp.TenantManagement.Blazor; - -public class AbpTenantManagementBlazorAutoMapperProfile : Profile -{ - public AbpTenantManagementBlazorAutoMapperProfile() - { - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorMapperlyMappers.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorMapperlyMappers.cs new file mode 100644 index 0000000000..7b8ea2a5bf --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorMapperlyMappers.cs @@ -0,0 +1,16 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.TenantManagement.Blazor; + +[Mapper] +[MapExtraProperties] +public partial class TenantDtoToTenantUpdateDtoMapper + : MapperBase +{ + [MapperIgnoreSource(nameof(TenantDto.Id))] + public override partial TenantUpdateDto Map(TenantDto source); + + [MapperIgnoreSource(nameof(TenantDto.Id))] + public override partial void Map(TenantDto source, TenantUpdateDto destination); +} \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorModule.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorModule.cs index 0433f2e33b..0263106b43 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorModule.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/AbpTenantManagementBlazorModule.cs @@ -1,7 +1,7 @@ using Localization.Resources.AbpUi; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.FeatureManagement.Blazor; using Volo.Abp.FeatureManagement.Localization; using Volo.Abp.Localization; @@ -16,7 +16,7 @@ using Volo.Abp.UI.Navigation; namespace Volo.Abp.TenantManagement.Blazor; [DependsOn( - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpTenantManagementApplicationContractsModule), typeof(AbpFeatureManagementBlazorModule) )] @@ -26,12 +26,7 @@ public class AbpTenantManagementBlazorModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor index 478adfb0eb..f87467f8c2 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor @@ -8,6 +8,9 @@ @using Volo.Abp.BlazoriseUI.Components.ObjectExtending @using Volo.Abp.AspNetCore.Components.Web @inject AbpBlazorMessageLocalizerHelper LH +@using Microsoft.Extensions.Localization +@using Volo.Abp.UI.Navigation.Localization.Resource +@inject IStringLocalizer LUiNavigation @inherits AbpCrudPageBase diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor.cs index cea9267f00..272bd9cf7a 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Pages/TenantManagement/TenantManagement.razor.cs @@ -41,6 +41,7 @@ public partial class TenantManagement protected override ValueTask SetBreadcrumbItemsAsync() { + BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(LUiNavigation["Menu:Administration"])); BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(L["Menu:TenantManagement"])); BreadcrumbItems.Add(new BlazoriseUI.BreadcrumbItem(L["Tenants"])); return base.SetBreadcrumbItemsAsync(); diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Volo.Abp.TenantManagement.Blazor.csproj b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Volo.Abp.TenantManagement.Blazor.csproj index 1afbbf56c7..8e54fdcba0 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Volo.Abp.TenantManagement.Blazor.csproj +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Blazor/Volo.Abp.TenantManagement.Blazor.csproj @@ -8,7 +8,7 @@ - + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ar.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ar.json index 8146974400..bc1e004895 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ar.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ar.json @@ -16,8 +16,8 @@ "Permission:Edit": "تحرير", "Permission:Delete": "حذف", "Permission:ManageConnectionStrings": "Manage connection strings", - "Permission:ManageFeatures": "إدارة الميزات", + "Permission:ManageFeatures": "الميزات", "DisplayName:AdminEmailAddress": "عنوان البريد الإلكتروني للمسؤول", "DisplayName:AdminPassword": "كلمة مرور المسؤول" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/cs.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/cs.json index 3902ad703b..a563181b61 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/cs.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/cs.json @@ -16,7 +16,7 @@ "Permission:Edit": "Upravit", "Permission:Delete": "Smazat", "Permission:ManageConnectionStrings": "Spravovat connection stringy", - "Permission:ManageFeatures": "Spravovat funkce", + "Permission:ManageFeatures": "Funkce", "DisplayName:AdminEmailAddress": "Email adresa správce", "DisplayName:AdminPassword": "Heslo správce" } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/de.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/de.json index 0a62c49a4d..59fd237333 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/de.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/de.json @@ -16,8 +16,8 @@ "Permission:Edit": "Bearbeiten", "Permission:Delete": "Löschen", "Permission:ManageConnectionStrings": "Connection Strings verwalten", - "Permission:ManageFeatures": "Features verwalten", + "Permission:ManageFeatures": "Funktionen", "DisplayName:AdminEmailAddress": "Admin-E-Mail-Adresse", "DisplayName:AdminPassword": "Admin-Passwort" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/el.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/el.json index bc75c34e03..21fbed24bb 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/el.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/el.json @@ -15,7 +15,7 @@ "Permission:Edit": "Επεξεργασία", "Permission:Delete": "Διαγραφή", "Permission:ManageConnectionStrings": "Διαχείριση συμβολοσειρών σύνδεσης", - "Permission:ManageFeatures": "Διαχείριση λειτουργιών", + "Permission:ManageFeatures": "Λειτουργίες", "DisplayName:AdminEmailAddress": "Διεύθυνση email διαχειριστή", "DisplayName:AdminPassword": "Κωδικός διαχειριστή" } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en-GB.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en-GB.json index 8b4e4c185b..c53f2be460 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en-GB.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en-GB.json @@ -15,7 +15,7 @@ "Permission:Edit": "Edit", "Permission:Delete": "Delete", "Permission:ManageConnectionStrings": "Manage connection strings", - "Permission:ManageFeatures": "Manage features", + "Permission:ManageFeatures": "Features", "DisplayName:AdminEmailAddress": "Admin Email Address", "DisplayName:AdminPassword": "Admin Password" } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json index 0ba2b5c42f..1fc5b1985c 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/en.json @@ -16,8 +16,9 @@ "Permission:Edit": "Edit", "Permission:Delete": "Delete", "Permission:ManageConnectionStrings": "Manage connection strings", - "Permission:ManageFeatures": "Manage features", + "Permission:ManageFeatures": "Features", "DisplayName:AdminEmailAddress": "Admin Email Address", "DisplayName:AdminPassword": "Admin Password" } } + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/es.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/es.json index bcbfc98852..12ef0a4d31 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/es.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/es.json @@ -16,8 +16,8 @@ "Permission:Edit": "Editar", "Permission:Delete": "Borrar", "Permission:ManageConnectionStrings": "Gestión de cadenas de conexión", - "Permission:ManageFeatures": "Gestión de características", + "Permission:ManageFeatures": "Características", "DisplayName:AdminEmailAddress": "Dirección e-mail de administrador", "DisplayName:AdminPassword": "Contraseña de administrador" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fa.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fa.json index c67a9c070b..7fb481af5e 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fa.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fa.json @@ -15,7 +15,7 @@ "Permission:Edit": "ویرایش", "Permission:Delete": "حذف", "Permission:ManageConnectionStrings": "مدیریت کانکشن استرینگها", - "Permission:ManageFeatures": "مدیریت ویژگی ها", + "Permission:ManageFeatures": "ویژگی‌ها", "DisplayName:AdminEmailAddress": "آدرس ایمیل مدیر", "DisplayName:AdminPassword": "گذرواژه مدیریت" } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fi.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fi.json index 7721629201..4acd99fd0d 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fi.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fi.json @@ -16,8 +16,8 @@ "Permission:Edit": "Muokkaus", "Permission:Delete": "Poisto", "Permission:ManageConnectionStrings": "Hallitse tietokantayhteyksiä", - "Permission:ManageFeatures": "Hallitse ominaisuuksia", + "Permission:ManageFeatures": "Ominaisuudet", "DisplayName:AdminEmailAddress": "Järjestelmänvalvojan sähköpostiosoite", "DisplayName:AdminPassword": "Järjestelmänvalvojan salasana" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fr.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fr.json index 3537ca8ca6..705669c78d 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fr.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/fr.json @@ -16,8 +16,8 @@ "Permission:Edit": "Modifier", "Permission:Delete": "Supprimer", "Permission:ManageConnectionStrings": "Gérer les chaînes de connexion", - "Permission:ManageFeatures": "Gérer les fonctionnalités", + "Permission:ManageFeatures": "Fonctionnalités", "DisplayName:AdminEmailAddress": "Adresse de messagerie d’administrateur", "DisplayName:AdminPassword": "Mot de passe d’administrateur" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hi.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hi.json index 70367305a9..8072bdf6f9 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hi.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hi.json @@ -16,8 +16,8 @@ "Permission:Edit": "संपादित करें", "Permission:Delete": "हटाएं", "Permission:ManageConnectionStrings": "कनेक्शन स्ट्रिंग्स प्रबंधित करें", - "Permission:ManageFeatures": "सुविधाओं को प्रबंधित करें", + "Permission:ManageFeatures": "सुविधाएं", "DisplayName:AdminEmailAddress": "ईमेल पता", "DisplayName:AdminPassword": "व्यवस्थापक का पारण शब्द" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hr.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hr.json index 7ca876277f..c646a9a6c1 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hr.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hr.json @@ -16,8 +16,9 @@ "Permission:Edit": "Uredi", "Permission:Delete": "Izbrisati", "Permission:ManageConnectionStrings": "Upravljanje vezom na bazu podataka", - "Permission:ManageFeatures": "Upravljanje značajkama", + "Permission:ManageFeatures": "Značajke", "DisplayName:AdminEmailAddress": "Adresa e-pošte administratora", "DisplayName:AdminPassword": "Administratorska lozinka" } } + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hu.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hu.json index 26787bca90..19085d7a6b 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hu.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/hu.json @@ -16,8 +16,8 @@ "Permission:Edit": "Szerkesztés", "Permission:Delete": "Törlés", "Permission:ManageConnectionStrings": "Kapcsolati beállítások kezelése", - "Permission:ManageFeatures": "Funkciók kezelése", + "Permission:ManageFeatures": "Funkciók", "DisplayName:AdminEmailAddress": "Admin email cím", "DisplayName:AdminPassword": "Admin jelszó" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/is.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/is.json index 5c170f9a27..902383427c 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/is.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/is.json @@ -16,8 +16,8 @@ "Permission:Edit": "Breyta", "Permission:Delete": "Eyða", "Permission:ManageConnectionStrings": "Umsjá tengistrengja", - "Permission:ManageFeatures": "Umsjá eiginleika", + "Permission:ManageFeatures": "Eiginleikar", "DisplayName:AdminEmailAddress": "Netfang stjórnanda (admin)", "DisplayName:AdminPassword": "Lykilorð stjórnanda (admin)" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/it.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/it.json index 5d95e2039c..e8c0b866ae 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/it.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/it.json @@ -16,8 +16,8 @@ "Permission:Edit": "Modifica", "Permission:Delete": "Elimina", "Permission:ManageConnectionStrings": "Gestisci le stringhe di connessione", - "Permission:ManageFeatures": "Gestisci le funzionalità", + "Permission:ManageFeatures": "Funzionalità", "DisplayName:AdminEmailAddress": "Indirizzo e-mail dell'amministratore", "DisplayName:AdminPassword": "Password dell'amministratore" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/nl.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/nl.json index f739a3eb27..f72e56afe9 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/nl.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/nl.json @@ -16,8 +16,8 @@ "Permission:Edit": "Bewerk", "Permission:Delete": "Verwijder", "Permission:ManageConnectionStrings": "Beheer connection strings", - "Permission:ManageFeatures": "Beheer functies", + "Permission:ManageFeatures": "Functies", "DisplayName:AdminEmailAddress": "Admin e-mail adres", "DisplayName:AdminPassword": "Admin wachtwoord" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/pl-PL.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/pl-PL.json index 95a7b39e0e..46f1ca47bf 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/pl-PL.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/pl-PL.json @@ -16,8 +16,8 @@ "Permission:Edit": "Edytuj", "Permission:Delete": "Usuń", "Permission:ManageConnectionStrings": "Zarządzaj connection string'ami", - "Permission:ManageFeatures": "Zarządzaj fukcjami", + "Permission:ManageFeatures": "Funkcje", "DisplayName:AdminEmailAddress": "Adres e-mail administratora", "DisplayName:AdminPassword": "Hasło administratora" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/pt-BR.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/pt-BR.json index d9f6b14a69..b621cc2069 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/pt-BR.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/pt-BR.json @@ -16,8 +16,8 @@ "Permission:Edit": "Editar", "Permission:Delete": "Excluir", "Permission:ManageConnectionStrings": "Gerenciar conexões (connection strings)", - "Permission:ManageFeatures": "Gerenciar Funcionalidades", + "Permission:ManageFeatures": "Recursos", "DisplayName:AdminEmailAddress": "Endereço de e-mail do administrador", "DisplayName:AdminPassword": "Senha do administrador" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ro-RO.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ro-RO.json index cb14140ddb..8c7915614d 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ro-RO.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ro-RO.json @@ -16,8 +16,8 @@ "Permission:Edit": "Editează", "Permission:Delete": "Şterge", "Permission:ManageConnectionStrings": "Administrează stringurile de conexiune", - "Permission:ManageFeatures": "Administrarea caracteristicilor", + "Permission:ManageFeatures": "Caracteristici", "DisplayName:AdminEmailAddress": "Adresa de email admin", "DisplayName:AdminPassword": "Parola admin" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ru.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ru.json index 2204cf35cf..f89a39074b 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ru.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/ru.json @@ -16,8 +16,8 @@ "Permission:Edit": "Редактировать", "Permission:Delete": "Удалить", "Permission:ManageConnectionStrings": "Управление строками подключения", - "Permission:ManageFeatures": "Управление функциями", + "Permission:ManageFeatures": "Возможности", "DisplayName:AdminEmailAddress": "Адрес электронной почты администратора", "DisplayName:AdminPassword": "Пароль администратора" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sk.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sk.json index 1aff65e71c..a1479764ba 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sk.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sk.json @@ -16,8 +16,8 @@ "Permission:Edit": "Upraviť", "Permission:Delete": "Zmazať", "Permission:ManageConnectionStrings": "Správa connection stringov", - "Permission:ManageFeatures": "Správa funkcií", + "Permission:ManageFeatures": "Funkcie", "DisplayName:AdminEmailAddress": "Emailová adresa administrátora", "DisplayName:AdminPassword": "Heslo administrátora" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sl.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sl.json index a21cfa702a..b559583420 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sl.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sl.json @@ -16,8 +16,8 @@ "Permission:Edit": "Urejanje", "Permission:Delete": "Brisanje", "Permission:ManageConnectionStrings": "Upravljanje connection string-ov", - "Permission:ManageFeatures": "Upravljanje funkcionalnosti", + "Permission:ManageFeatures": "Lastnosti", "DisplayName:AdminEmailAddress": "Admin e-poštni naslov", "DisplayName:AdminPassword": "Skrbniško geslo" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sv.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sv.json index f112b05001..9cb8c11c89 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sv.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/sv.json @@ -16,8 +16,8 @@ "Permission:Edit": "Redigera", "Permission:Delete": "Radera", "Permission:ManageConnectionStrings": "Hantera anslutningssträngar", - "Permission:ManageFeatures": "Hantera funktioner", + "Permission:ManageFeatures": "Funktioner", "DisplayName:AdminEmailAddress": "Admin E-postadress", "DisplayName:AdminPassword": "Lösenord för administratör" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json index 667a65fb69..24c2292e74 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/tr.json @@ -16,8 +16,9 @@ "Permission:Edit": "Düzenleme", "Permission:Delete": "Silme", "Permission:ManageConnectionStrings": "Bağlantı cümlelerini yönet", - "Permission:ManageFeatures": "Özellikleri yönet", + "Permission:ManageFeatures": "Özellikler", "DisplayName:AdminEmailAddress": "Admin Eposta Adresi", "DisplayName:AdminPassword": "Admin Şifresi" } } + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/vi.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/vi.json index 13f5afb095..2ca10f4f05 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/vi.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/vi.json @@ -16,8 +16,8 @@ "Permission:Edit": "Sửa", "Permission:Delete": "Xóa", "Permission:ManageConnectionStrings": "Quản lý chuỗi kết nối", - "Permission:ManageFeatures": "Quản lý các tính năng", + "Permission:ManageFeatures": "Tính năng", "DisplayName:AdminEmailAddress": "Địa chỉ Email Quản trị viên", "DisplayName:AdminPassword": "Mật khẩu quản trị" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json index 8cff1a09b7..d8334301bf 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hans.json @@ -16,8 +16,8 @@ "Permission:Edit": "编辑", "Permission:Delete": "删除", "Permission:ManageConnectionStrings": "管理连接字符串", - "Permission:ManageFeatures": "管理功能", + "Permission:ManageFeatures": "功能", "DisplayName:AdminEmailAddress": "管理员电子邮件地址", "DisplayName:AdminPassword": "管理员密码" } -} \ No newline at end of file +} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json index 59519aff67..0da3743f2e 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain.Shared/Volo/Abp/TenantManagement/Localization/Resources/zh-Hant.json @@ -16,8 +16,9 @@ "Permission:Edit": "編輯", "Permission:Delete": "刪除", "Permission:ManageConnectionStrings": "管理資料庫連線字串", - "Permission:ManageFeatures": "管理功能", + "Permission:ManageFeatures": "功能", "DisplayName:AdminEmailAddress": "管理者信箱", "DisplayName:AdminPassword": "管理者密碼" } } + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.csproj b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.csproj index 7a40ae4f71..dcb196ce86 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.csproj +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo.Abp.TenantManagement.Domain.csproj @@ -17,7 +17,7 @@ - + diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainMapperlyMappers.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainMapperlyMappers.cs new file mode 100644 index 0000000000..29ae9f21fe --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainMapperlyMappers.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Data; +using Volo.Abp.Mapperly; +using Volo.Abp.MultiTenancy; + +namespace Volo.Abp.TenantManagement.Domain.Volo.Abp.TenantManagement; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class TenantToTenantConfigurationMapper + : MapperBase +{ + [MapperIgnoreTarget(nameof(TenantConfiguration.EditionId))] + [MapperIgnoreTarget(nameof(TenantConfiguration.IsActive))] + public override partial TenantConfiguration Map(Tenant source); + + [MapperIgnoreTarget(nameof(TenantConfiguration.EditionId))] + [MapperIgnoreTarget(nameof(TenantConfiguration.IsActive))] + public override partial void Map(Tenant source, TenantConfiguration destination); + + protected virtual ConnectionStrings Map(List source) + { + var connStrings = new ConnectionStrings(); + + if (source == null) + { + return connStrings; + } + + foreach (var connectionString in source) + { + connStrings[connectionString.Name] = connectionString.Value; + } + + return connStrings; + } +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class TenantToTenantEtoMapper + : MapperBase +{ + public override partial TenantEto Map(Tenant source); + + public override partial void Map(Tenant source, TenantEto destination); +} \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainMappingProfile.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainMappingProfile.cs deleted file mode 100644 index 6fe1edafc6..0000000000 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainMappingProfile.cs +++ /dev/null @@ -1,36 +0,0 @@ -using AutoMapper; -using Volo.Abp.Data; -using Volo.Abp.MultiTenancy; - -namespace Volo.Abp.TenantManagement; - -public class AbpTenantManagementDomainMappingProfile : Profile -{ - public AbpTenantManagementDomainMappingProfile() - { - CreateMap() - .ForMember(ti => ti.ConnectionStrings, opts => - { - opts.MapFrom((tenant, ti) => - { - var connStrings = new ConnectionStrings(); - - if (tenant.ConnectionStrings == null) - { - return connStrings; - } - - foreach (var connectionString in tenant.ConnectionStrings) - { - connStrings[connectionString.Name] = connectionString.Value; - } - - return connStrings; - }); - }) - .ForMember(x => x.IsActive, x => x.Ignore()) - .ForMember(x => x.EditionId, x => x.Ignore()); - - CreateMap(); - } -} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainModule.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainModule.cs index 6bd09809e4..afce2e0809 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainModule.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Domain/Volo/Abp/TenantManagement/AbpTenantManagementDomainModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Data; using Volo.Abp.Domain; @@ -16,7 +16,7 @@ namespace Volo.Abp.TenantManagement; [DependsOn(typeof(AbpTenantManagementDomainSharedModule))] [DependsOn(typeof(AbpDataModule))] [DependsOn(typeof(AbpDddDomainModule))] -[DependsOn(typeof(AbpAutoMapperModule))] +[DependsOn(typeof(AbpMapperlyModule))] [DependsOn(typeof(AbpCachingModule))] public class AbpTenantManagementDomainModule : AbpModule { @@ -24,12 +24,7 @@ public class AbpTenantManagementDomainModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebAutoMapperProfile.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebAutoMapperProfile.cs deleted file mode 100644 index 4730033e60..0000000000 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebAutoMapperProfile.cs +++ /dev/null @@ -1,22 +0,0 @@ -using AutoMapper; -using Volo.Abp.AutoMapper; -using Volo.Abp.TenantManagement.Web.Pages.TenantManagement.Tenants; - -namespace Volo.Abp.TenantManagement.Web; - -public class AbpTenantManagementWebAutoMapperProfile : Profile -{ - public AbpTenantManagementWebAutoMapperProfile() - { - //List - CreateMap(); - - //CreateModal - CreateMap() - .MapExtraProperties(); - - //EditModal - CreateMap() - .MapExtraProperties(); - } -} diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebMapperlyMappers.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebMapperlyMappers.cs new file mode 100644 index 0000000000..d729809010 --- /dev/null +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebMapperlyMappers.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Riok.Mapperly.Abstractions; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Mapperly; +using Volo.Abp.TenantManagement.Web.Pages.TenantManagement.Tenants; + +namespace Volo.Abp.TenantManagement.Web; + +[Mapper] +[MapExtraProperties] +public partial class TenantDtoToTenantInfoModelMapper + : MapperBase +{ + public override partial EditModalModel.TenantInfoModel Map(TenantDto source); + + public override partial void Map(TenantDto source, EditModalModel.TenantInfoModel destination); +} + +[Mapper] +[MapExtraProperties] +public partial class CreateTenantInfoModelToTenantCreateDtoMapper + : TwoWayMapperBase +{ + public override partial TenantCreateDto Map(CreateModalModel.TenantInfoModel source); + + public override partial void Map(CreateModalModel.TenantInfoModel source, TenantCreateDto destination); + + public override partial CreateModalModel.TenantInfoModel ReverseMap(TenantCreateDto source); + + public override partial void ReverseMap(TenantCreateDto source, CreateModalModel.TenantInfoModel destination); +} + +[Mapper] +[MapExtraProperties] +public partial class TenantInfoModelToTenantUpdateDtoMapper + : MapperBase +{ + [MapperIgnoreSource(nameof(EditModalModel.TenantInfoModel.Id))] + [MapperIgnoreSource(nameof(EditModalModel.TenantInfoModel.ConcurrencyStamp))] + [MapperIgnoreTarget(nameof(TenantUpdateDto.ConcurrencyStamp))] + public override partial TenantUpdateDto Map(EditModalModel.TenantInfoModel source); + + [MapperIgnoreSource(nameof(EditModalModel.TenantInfoModel.Id))] + [MapperIgnoreSource(nameof(EditModalModel.TenantInfoModel.ConcurrencyStamp))] + [MapperIgnoreTarget(nameof(TenantUpdateDto.ConcurrencyStamp))] + public override partial void Map(EditModalModel.TenantInfoModel source, TenantUpdateDto destination); +} \ No newline at end of file diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebModule.cs b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebModule.cs index b3a63fb8ae..90dc1df3d6 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebModule.cs +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/AbpTenantManagementWebModule.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.PageToolbars; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.FeatureManagement; using Volo.Abp.Http.ProxyScripting.Generators.JQuery; using Volo.Abp.Localization; @@ -21,7 +21,7 @@ namespace Volo.Abp.TenantManagement.Web; [DependsOn(typeof(AbpTenantManagementApplicationContractsModule))] [DependsOn(typeof(AbpAspNetCoreMvcUiBootstrapModule))] [DependsOn(typeof(AbpFeatureManagementWebModule))] -[DependsOn(typeof(AbpAutoMapperModule))] +[DependsOn(typeof(AbpMapperlyModule))] public class AbpTenantManagementWebModule : AbpModule { private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); @@ -41,6 +41,8 @@ public class AbpTenantManagementWebModule : AbpModule public override void ConfigureServices(ServiceConfigurationContext context) { + context.Services.AddMapperlyObjectMapper(); + Configure(options => { options.MenuContributors.Add(new AbpTenantManagementWebMainMenuContributor()); @@ -51,12 +53,6 @@ public class AbpTenantManagementWebModule : AbpModule options.FileSets.AddEmbedded(); }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddProfile(validate: true); - }); - Configure(options => { options.Conventions.AuthorizePage("/TenantManagement/Tenants/Index", TenantManagementPermissions.Tenants.Default); diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml index 57859e28d3..8a17555bd9 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Pages/TenantManagement/Tenants/Index.cshtml @@ -8,12 +8,15 @@ @using Volo.Abp.TenantManagement.Localization @using Volo.Abp.TenantManagement.Web.Navigation @using Volo.Abp.TenantManagement.Web.Pages.TenantManagement.Tenants +@using Volo.Abp.UI.Navigation.Localization.Resource @model IndexModel @inject IHtmlLocalizer L +@inject IHtmlLocalizer LUiNavigation @inject IAuthorizationService Authorization @inject IPageLayout PageLayout @{ PageLayout.Content.Title = L["Tenants"].Value; + PageLayout.Content.BreadCrumb.Add(LUiNavigation["Menu:Administration"].Value); PageLayout.Content.BreadCrumb.Add(L["Menu:TenantManagement"].Value); PageLayout.Content.MenuItemName = TenantManagementMenuNames.Tenants; } diff --git a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Volo.Abp.TenantManagement.Web.csproj b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Volo.Abp.TenantManagement.Web.csproj index 0fe0724f15..59d7e74c6b 100644 --- a/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Volo.Abp.TenantManagement.Web.csproj +++ b/modules/tenant-management/src/Volo.Abp.TenantManagement.Web/Volo.Abp.TenantManagement.Web.csproj @@ -35,7 +35,7 @@ - + diff --git a/modules/virtual-file-explorer/app/DemoApp.csproj b/modules/virtual-file-explorer/app/DemoApp.csproj index 938e3f03b1..50aa357f96 100644 --- a/modules/virtual-file-explorer/app/DemoApp.csproj +++ b/modules/virtual-file-explorer/app/DemoApp.csproj @@ -13,7 +13,7 @@ - + diff --git a/modules/virtual-file-explorer/app/DemoAppModule.cs b/modules/virtual-file-explorer/app/DemoAppModule.cs index ae03d3f2ed..4ffbc6c5d5 100644 --- a/modules/virtual-file-explorer/app/DemoAppModule.cs +++ b/modules/virtual-file-explorer/app/DemoAppModule.cs @@ -1,4 +1,4 @@ -using DemoApp.Data; +using DemoApp.Data; using Microsoft.EntityFrameworkCore; using Volo.Abp; using Volo.Abp.Account; @@ -8,7 +8,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Data; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.SqlServer; @@ -32,7 +32,7 @@ namespace DemoApp; // ABP Framework packages typeof(AbpAspNetCoreMvcModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpSwashbuckleModule), typeof(AbpAspNetCoreSerilogModule), diff --git a/npm/ng-packs/apps/dev-app/project.json b/npm/ng-packs/apps/dev-app/project.json index 1213acbb2f..958cdfb3ee 100644 --- a/npm/ng-packs/apps/dev-app/project.json +++ b/npm/ng-packs/apps/dev-app/project.json @@ -6,13 +6,13 @@ "prefix": "app", "targets": { "build": { - "executor": "@angular-devkit/build-angular:browser", + "executor": "@angular-devkit/build-angular:application", "outputs": ["{options.outputPath}"], "options": { "outputPath": "dist/apps/dev-app", "index": "apps/dev-app/src/index.html", - "main": "apps/dev-app/src/main.ts", - "polyfills": "apps/dev-app/src/polyfills.ts", + "browser": "apps/dev-app/src/main.ts", + "polyfills": ["apps/dev-app/src/polyfills.ts"], "tsConfig": "apps/dev-app/tsconfig.app.json", "inlineStyleLanguage": "scss", "allowedCommonJsDependencies": ["chart.js", "js-sha256"], @@ -141,12 +141,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" diff --git a/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts b/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts index 4a6287a7c0..44bf9f269e 100644 --- a/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts +++ b/npm/ng-packs/apps/dev-app/src/app/home/home.component.ts @@ -1,12 +1,12 @@ import { AuthService, LocalizationPipe } from '@abp/ng.core'; import { Component, inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { ButtonComponent, CardBodyComponent, CardComponent } from '@abp/ng.theme.shared'; @Component({ selector: 'app-home', templateUrl: './home.component.html', - imports: [CommonModule, LocalizationPipe, CardComponent, CardBodyComponent, ButtonComponent], + imports: [NgTemplateOutlet, LocalizationPipe, CardComponent, CardBodyComponent, ButtonComponent], }) export class HomeComponent { protected readonly authService = inject(AuthService); diff --git a/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/account.service.ts b/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/account.service.ts index adeb2ee5c9..ad584495c6 100644 --- a/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/account.service.ts +++ b/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/account.service.ts @@ -1,12 +1,14 @@ import type { RegisterDto, ResetPasswordDto, SendPasswordResetCodeDto } from './models'; import { RestService } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import type { IdentityUserDto } from '../identity/models'; @Injectable({ providedIn: 'root', }) export class AccountService { + private restService = inject(RestService); + apiName = 'AbpAccount'; register = (input: RegisterDto) => @@ -32,6 +34,4 @@ export class AccountService { body: input, }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/profile.service.ts b/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/profile.service.ts index 0cea6b8cfb..a14c6cedfd 100644 --- a/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/profile.service.ts +++ b/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/profile.service.ts @@ -1,11 +1,13 @@ import type { ChangePasswordInput, ProfileDto, UpdateProfileDto } from './models'; import { RestService } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class ProfileService { + private restService = inject(RestService); + apiName = 'AbpAccount'; changePassword = (input: ChangePasswordInput) => @@ -30,6 +32,4 @@ export class ProfileService { body: input, }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/web/areas/account/controllers/account.service.ts b/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/web/areas/account/controllers/account.service.ts index 8adf5385b0..52c779655b 100644 --- a/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/web/areas/account/controllers/account.service.ts +++ b/npm/ng-packs/packages/account-core/proxy/src/lib/proxy/account/web/areas/account/controllers/account.service.ts @@ -1,11 +1,13 @@ import type { AbpLoginResult, UserLoginInfo } from './models/models'; import { RestService } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class AccountService { + private restService = inject(RestService); + apiName = 'AbpAccount'; checkPasswordByLogin = (login: UserLoginInfo) => @@ -30,6 +32,4 @@ export class AccountService { url: '/api/account/logout', }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/account-core/src/lib/auth-wrapper.service.ts b/npm/ng-packs/packages/account-core/src/lib/auth-wrapper.service.ts index 7bad8ef96a..10ca25380b 100644 --- a/npm/ng-packs/packages/account-core/src/lib/auth-wrapper.service.ts +++ b/npm/ng-packs/packages/account-core/src/lib/auth-wrapper.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Injector } from '@angular/core'; +import { Injectable, Injector, inject } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { ActivatedRoute } from '@angular/router'; @@ -6,6 +6,9 @@ import { ConfigStateService, MultiTenancyService } from '@abp/ng.core'; @Injectable() export class AuthWrapperService { + readonly multiTenancy = inject(MultiTenancyService); + private configState = inject(ConfigStateService); + isMultiTenancyEnabled$ = this.configState.getDeep$('multiTenancy.isEnabled'); get enableLocalLogin$(): Observable { @@ -25,11 +28,9 @@ export class AuthWrapperService { return this.isTenantBoxVisibleForCurrentRoute && this.multiTenancy.isTenantBoxVisible; } - constructor( - public readonly multiTenancy: MultiTenancyService, - private configState: ConfigStateService, - injector: Injector, - ) { + constructor() { + const injector = inject(Injector); + this.route = injector.get(ActivatedRoute); } diff --git a/npm/ng-packs/packages/account-core/src/lib/tenant-box.service.ts b/npm/ng-packs/packages/account-core/src/lib/tenant-box.service.ts index bcb37c3d48..2890d90776 100644 --- a/npm/ng-packs/packages/account-core/src/lib/tenant-box.service.ts +++ b/npm/ng-packs/packages/account-core/src/lib/tenant-box.service.ts @@ -5,11 +5,16 @@ import { SessionStateService, } from '@abp/ng.core'; import { ToasterService } from '@abp/ng.theme.shared'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { finalize } from 'rxjs/operators'; @Injectable() export class TenantBoxService { + private toasterService = inject(ToasterService); + private tenantService = inject(AbpTenantService); + private sessionState = inject(SessionStateService); + private configState = inject(ConfigStateService); + currentTenant$ = this.sessionState.getTenant$(); name?: string; @@ -18,13 +23,6 @@ export class TenantBoxService { modalBusy!: boolean; - constructor( - private toasterService: ToasterService, - private tenantService: AbpTenantService, - private sessionState: SessionStateService, - private configState: ConfigStateService, - ) {} - onSwitch() { const tenant = this.sessionState.getTenant(); this.name = tenant?.name || ''; diff --git a/npm/ng-packs/packages/account-core/tsconfig.lib.json b/npm/ng-packs/packages/account-core/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/account-core/tsconfig.lib.json +++ b/npm/ng-packs/packages/account-core/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.ts b/npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.ts index 9e1ae788f0..ad5db820ac 100644 --- a/npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/change-password/change-password.component.ts @@ -1,6 +1,6 @@ import { ProfileService } from '@abp/ng.account.core/proxy'; import { ButtonComponent, getPasswordValidators, ToasterService } from '@abp/ng.theme.shared'; -import { Component, Injector, OnInit } from '@angular/core'; +import { Component, Injector, OnInit, inject } from '@angular/core'; import { ReactiveFormsModule, UntypedFormBuilder, @@ -32,6 +32,12 @@ const PASSWORD_FIELDS = ['newPassword', 'repeatNewPassword']; export class ChangePasswordComponent implements OnInit, Account.ChangePasswordComponentInputs, Account.ChangePasswordComponentOutputs { + private fb = inject(UntypedFormBuilder); + private injector = inject(Injector); + private toasterService = inject(ToasterService); + private profileService = inject(ProfileService); + private manageProfileState = inject(ManageProfileStateService); + form!: UntypedFormGroup; inProgress?: boolean; @@ -44,14 +50,6 @@ export class ChangePasswordComponent return errors.concat(groupErrors.filter(({ key }) => key === 'passwordMismatch')); }; - constructor( - private fb: UntypedFormBuilder, - private injector: Injector, - private toasterService: ToasterService, - private profileService: ProfileService, - private manageProfileState: ManageProfileStateService, - ) {} - ngOnInit(): void { this.hideCurrentPassword = !this.manageProfileState.getProfile()?.hasPassword; diff --git a/npm/ng-packs/packages/account/src/lib/components/forgot-password/forgot-password.component.ts b/npm/ng-packs/packages/account/src/lib/components/forgot-password/forgot-password.component.ts index 815c95d71c..5b00896acb 100644 --- a/npm/ng-packs/packages/account/src/lib/components/forgot-password/forgot-password.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/forgot-password/forgot-password.component.ts @@ -1,5 +1,5 @@ import { AccountService } from '@abp/ng.account.core/proxy'; -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { ReactiveFormsModule, UntypedFormBuilder, @@ -9,7 +9,7 @@ import { import { finalize } from 'rxjs/operators'; import { LocalizationPipe } from '@abp/ng.core'; import { ButtonComponent } from '@abp/ng.theme.shared'; -import { RouterModule } from '@angular/router'; +import { RouterLink } from '@angular/router'; import { NgxValidateCoreModule } from '@ngx-validate/core'; @Component({ @@ -17,23 +17,23 @@ import { NgxValidateCoreModule } from '@ngx-validate/core'; templateUrl: 'forgot-password.component.html', imports: [ ReactiveFormsModule, - RouterModule, + RouterLink, LocalizationPipe, ButtonComponent, NgxValidateCoreModule, ], }) export class ForgotPasswordComponent { + private fb = inject(UntypedFormBuilder); + private accountService = inject(AccountService); + form: UntypedFormGroup; inProgress?: boolean; isEmailSent = false; - constructor( - private fb: UntypedFormBuilder, - private accountService: AccountService, - ) { + constructor() { this.form = this.fb.group({ email: ['', [Validators.required, Validators.email]], }); diff --git a/npm/ng-packs/packages/account/src/lib/components/login/login.component.ts b/npm/ng-packs/packages/account/src/lib/components/login/login.component.ts index 37a3e45442..9144b36ba6 100644 --- a/npm/ng-packs/packages/account/src/lib/components/login/login.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/login/login.component.ts @@ -5,7 +5,7 @@ import { UntypedFormGroup, Validators, } from '@angular/forms'; -import { RouterModule } from '@angular/router'; +import { RouterLink } from '@angular/router'; import { throwError } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { @@ -26,7 +26,7 @@ const { maxLength, required } = Validators; templateUrl: './login.component.html', imports: [ ReactiveFormsModule, - RouterModule, + RouterLink, LocalizationPipe, ButtonComponent, NgxValidateCoreModule, diff --git a/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts b/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts index 797bb9f492..4e749446f3 100644 --- a/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/manage-profile/manage-profile.component.ts @@ -1,10 +1,10 @@ import { ProfileService } from '@abp/ng.account.core/proxy'; import { fadeIn, LoadingDirective } from '@abp/ng.theme.shared'; import { transition, trigger, useAnimation } from '@angular/animations'; -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { eAccountComponents } from '../../enums/components'; import { ManageProfileStateService } from '../../services/manage-profile.state.service'; -import { CommonModule } from '@angular/common'; +import { NgClass, AsyncPipe } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { LocalizationPipe, ReplaceableTemplateDirective } from '@abp/ng.core'; import { PersonalSettingsComponent } from '../personal-settings/personal-settings.component'; @@ -23,7 +23,8 @@ import { ChangePasswordComponent } from '../change-password/change-password.comp `, ], imports: [ - CommonModule, + NgClass, + AsyncPipe, ReactiveFormsModule, PersonalSettingsComponent, ChangePasswordComponent, @@ -33,6 +34,9 @@ import { ChangePasswordComponent } from '../change-password/change-password.comp ], }) export class ManageProfileComponent implements OnInit { + protected profileService = inject(ProfileService); + protected manageProfileState = inject(ManageProfileStateService); + selectedTab = 0; changePasswordKey = eAccountComponents.ChangePassword; @@ -43,11 +47,6 @@ export class ManageProfileComponent implements OnInit { hideChangePasswordTab?: boolean; - constructor( - protected profileService: ProfileService, - protected manageProfileState: ManageProfileStateService, - ) {} - ngOnInit() { this.profileService.get().subscribe(profile => { this.manageProfileState.setProfile(profile); diff --git a/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings-half-row.component.ts b/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings-half-row.component.ts index ac99a51c23..be6287e954 100644 --- a/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings-half-row.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/personal-settings/personal-settings-half-row.component.ts @@ -1,4 +1,4 @@ -import { Component, Inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { EXTENSIONS_FORM_PROP, FormProp, @@ -24,12 +24,16 @@ import { LocalizationPipe } from '@abp/ng.core'; imports: [ReactiveFormsModule, LocalizationPipe], }) export class PersonalSettingsHalfRowComponent { + private propData = inject(EXTENSIONS_FORM_PROP); + public displayName: string; public name: string; public id: string; public formGroup!: UntypedFormGroup; - constructor(@Inject(EXTENSIONS_FORM_PROP) private propData: FormProp) { + constructor() { + const propData = this.propData; + this.displayName = propData.displayName; this.name = propData.name; this.id = propData.id || ''; diff --git a/npm/ng-packs/packages/account/src/lib/components/register/register.component.ts b/npm/ng-packs/packages/account/src/lib/components/register/register.component.ts index 18b4935c92..a826315311 100644 --- a/npm/ng-packs/packages/account/src/lib/components/register/register.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/register/register.component.ts @@ -6,7 +6,7 @@ import { LocalizationPipe, } from '@abp/ng.core'; import { ButtonComponent, getPasswordValidators, ToasterService } from '@abp/ng.theme.shared'; -import { Component, Injector, OnInit } from '@angular/core'; +import { Component, Injector, OnInit, inject } from '@angular/core'; import { ReactiveFormsModule, UntypedFormBuilder, @@ -18,7 +18,7 @@ import { catchError, finalize, switchMap } from 'rxjs/operators'; import { eAccountComponents } from '../../enums/components'; import { getRedirectUrl } from '../../utils/auth-utils'; import { NgxValidateCoreModule } from '@ngx-validate/core'; -import { RouterModule } from '@angular/router'; +import { RouterLink } from '@angular/router'; const { maxLength, required, email } = Validators; @@ -27,7 +27,7 @@ const { maxLength, required, email } = Validators; templateUrl: './register.component.html', imports: [ ReactiveFormsModule, - RouterModule, + RouterLink, NgxValidateCoreModule, LocalizationPipe, ButtonComponent, @@ -35,6 +35,13 @@ const { maxLength, required, email } = Validators; ], }) export class RegisterComponent implements OnInit { + protected fb = inject(UntypedFormBuilder); + protected accountService = inject(AccountService); + protected configState = inject(ConfigStateService); + protected toasterService = inject(ToasterService); + protected authService = inject(AuthService); + protected injector = inject(Injector); + form!: UntypedFormGroup; inProgress?: boolean; @@ -43,15 +50,6 @@ export class RegisterComponent implements OnInit { authWrapperKey = eAccountComponents.AuthWrapper; - constructor( - protected fb: UntypedFormBuilder, - protected accountService: AccountService, - protected configState: ConfigStateService, - protected toasterService: ToasterService, - protected authService: AuthService, - protected injector: Injector, - ) {} - ngOnInit() { this.init(); this.buildForm(); diff --git a/npm/ng-packs/packages/account/src/lib/components/reset-password/reset-password.component.ts b/npm/ng-packs/packages/account/src/lib/components/reset-password/reset-password.component.ts index d04a99ae98..f4567c48bb 100644 --- a/npm/ng-packs/packages/account/src/lib/components/reset-password/reset-password.component.ts +++ b/npm/ng-packs/packages/account/src/lib/components/reset-password/reset-password.component.ts @@ -1,13 +1,13 @@ import { AccountService } from '@abp/ng.account.core/proxy'; import { ButtonComponent, getPasswordValidators } from '@abp/ng.theme.shared'; -import { Component, Injector, OnInit } from '@angular/core'; +import { Component, Injector, OnInit, inject } from '@angular/core'; import { ReactiveFormsModule, UntypedFormBuilder, UntypedFormGroup, Validators, } from '@angular/forms'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { comparePasswords, NgxValidateCoreModule, Validation } from '@ngx-validate/core'; import { finalize } from 'rxjs/operators'; import { LocalizationPipe } from '@abp/ng.core'; @@ -19,13 +19,19 @@ const PASSWORD_FIELDS = ['password', 'confirmPassword']; templateUrl: './reset-password.component.html', imports: [ ReactiveFormsModule, - RouterModule, + RouterLink, NgxValidateCoreModule, LocalizationPipe, ButtonComponent, ], }) export class ResetPasswordComponent implements OnInit { + private fb = inject(UntypedFormBuilder); + private accountService = inject(AccountService); + private route = inject(ActivatedRoute); + private router = inject(Router); + private injector = inject(Injector); + form!: UntypedFormGroup; inProgress = false; @@ -38,14 +44,6 @@ export class ResetPasswordComponent implements OnInit { return errors.concat(groupErrors.filter(({ key }) => key === 'passwordMismatch')); }; - constructor( - private fb: UntypedFormBuilder, - private accountService: AccountService, - private route: ActivatedRoute, - private router: Router, - private injector: Injector, - ) {} - ngOnInit(): void { this.route.queryParams.subscribe(({ userId, resetToken }) => { if (!userId || !resetToken) this.router.navigateByUrl('/account/login'); diff --git a/npm/ng-packs/packages/account/tsconfig.lib.json b/npm/ng-packs/packages/account/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/account/tsconfig.lib.json +++ b/npm/ng-packs/packages/account/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/components/chart.js/src/chart.component.ts b/npm/ng-packs/packages/components/chart.js/src/chart.component.ts index c700a0bce8..9ab84aedcd 100644 --- a/npm/ng-packs/packages/components/chart.js/src/chart.component.ts +++ b/npm/ng-packs/packages/components/chart.js/src/chart.component.ts @@ -1,16 +1,17 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - ChangeDetectorRef, - Component, - ElementRef, - EventEmitter, - Input, - OnChanges, - OnDestroy, - Output, - SimpleChanges, - ViewChild, +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, + SimpleChanges, + ViewChild, + inject } from '@angular/core'; let Chart: any; @@ -35,6 +36,9 @@ let Chart: any; exportAs: 'abpChart', }) export class ChartComponent implements AfterViewInit, OnDestroy, OnChanges { + el = inject(ElementRef); + private cdr = inject(ChangeDetectorRef); + @Input() type!: string; @Input() data: any = {}; @@ -57,11 +61,6 @@ export class ChartComponent implements AfterViewInit, OnDestroy, OnChanges { chart: any; - constructor( - public el: ElementRef, - private cdr: ChangeDetectorRef, - ) {} - ngAfterViewInit() { import('chart.js/auto').then(module => { Chart = module.default; diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/abstract-actions/abstract-actions.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/abstract-actions/abstract-actions.component.ts index 394e125798..465a18b813 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/abstract-actions/abstract-actions.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/abstract-actions/abstract-actions.component.ts @@ -1,4 +1,4 @@ -import { Directive, Injector, Input } from '@angular/core'; +import { Directive, Injector, Input, inject } from '@angular/core'; import { ActionData, ActionList, InferredAction } from '../../models/actions'; import { ExtensionsService } from '../../services/extensions.service'; import { EXTENSIONS_ACTION_TYPE, EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; @@ -16,7 +16,9 @@ export abstract class AbstractActionsComponent< @Input() record!: InferredData['record']; - protected constructor(injector: Injector) { + protected constructor() { + const injector = inject(Injector); + super(); this.getInjected = injector.get.bind(injector); const extensions = injector.get(ExtensionsService); diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts index 9f4455b1c2..907118283e 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/date-time-picker/extensible-date-time-picker.component.ts @@ -8,7 +8,6 @@ import { SkipSelf, ViewChild, } from '@angular/core'; -import { CommonModule } from '@angular/common'; import { ControlContainer, ReactiveFormsModule } from '@angular/forms'; import { NgbDateAdapter, @@ -27,7 +26,6 @@ import { selfFactory } from '../../utils/factory.util'; @Component({ exportAs: 'abpExtensibleDateTimePicker', imports: [ - CommonModule, ReactiveFormsModule, NgbDatepickerModule, NgbTimepickerModule, diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts index 401bd15c3d..b1ba5c3fe6 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form-prop.component.ts @@ -1,7 +1,7 @@ import { EXTENSIONS_FORM_PROP, EXTENSIONS_FORM_PROP_DATA } from './../../tokens/extensions.token'; import { ABP, - LocalizationModule, + LocalizationPipe, PermissionDirective, ShowPasswordDirective, TrackByService, @@ -47,7 +47,7 @@ import { eExtensibleComponents } from '../../enums/components'; import { ExtensibleDateTimePickerComponent } from '../date-time-picker/extensible-date-time-picker.component'; import { NgxValidateCoreModule } from '@ngx-validate/core'; import { ExtensibleFormPropService } from '../../services/extensible-form-prop.service'; -import { CommonModule } from '@angular/common'; +import { AsyncPipe, NgClass, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-form-multiselect.component'; @@ -66,8 +66,11 @@ import { ExtensibleFormMultiselectComponent } from '../multi-select/extensible-f NgbTypeaheadModule, ShowPasswordDirective, PermissionDirective, - LocalizationModule, - CommonModule, + LocalizationPipe, + AsyncPipe, + NgClass, + NgComponentOutlet, + NgTemplateOutlet, FormsModule, ], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts index 33f6988811..9d81502291 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-form/extensible-form.component.ts @@ -17,14 +17,14 @@ import { ExtensionsService } from '../../services/extensions.service'; import { EXTENSIONS_IDENTIFIER } from '../../tokens/extensions.token'; import { selfFactory } from '../../utils/factory.util'; import { ExtensibleFormPropComponent } from './extensible-form-prop.component'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgTemplateOutlet } from '@angular/common'; import { PropDataDirective } from '../../directives/prop-data.directive'; @Component({ exportAs: 'abpExtensibleForm', selector: 'abp-extensible-form', templateUrl: './extensible-form.component.html', - imports: [CommonModule, PropDataDirective, ReactiveFormsModule, ExtensibleFormPropComponent], + imports: [NgClass, NgTemplateOutlet, PropDataDirective, ReactiveFormsModule, ExtensibleFormPropComponent], changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ { diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html index bfedbe8a44..366398e444 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.html @@ -7,6 +7,11 @@ (activate)="tableActivate.emit($event)" (select)="onSelect($event)" [selected]="selected" + (scroll)="onScroll($event)" + [scrollbarV]="infiniteScroll" + [style.height]="getTableHeight()" + [loadingIndicator]="infiniteScroll && isLoading" + [footerHeight]="infiniteScroll ? false : 50" > @if(selectable) { @@ -66,17 +71,24 @@ [prop]="prop.name" [sortable]="prop.sortable" > - + @if (prop.tooltip) { {{ column.name }} } @else { - {{ column.name }} + + {{ column.name }} + } diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts index fdb19e553d..98132bc5f6 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/extensible-table/extensible-table.component.ts @@ -10,16 +10,16 @@ import { Input, LOCALE_ID, OnChanges, + OnDestroy, Output, signal, SimpleChanges, TemplateRef, TrackByFunction, - ViewChild, } from '@angular/core'; import { AsyncPipe, NgComponentOutlet, NgTemplateOutlet } from '@angular/common'; -import { Observable, filter, map } from 'rxjs'; +import { Observable, filter, map, Subject, debounceTime, distinctUntilChanged } from 'rxjs'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { NgxDatatableModule, SelectionType } from '@swimlane/ngx-datatable'; @@ -28,7 +28,7 @@ import { ABP, ConfigStateService, ListService, - LocalizationModule, + LocalizationPipe, PermissionDirective, PermissionService, TimezoneService, @@ -66,7 +66,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; NgxDatatableDefaultDirective, NgxDatatableListDirective, PermissionDirective, - LocalizationModule, + LocalizationPipe, UtcToLocalPipe, AsyncPipe, NgTemplateOutlet, @@ -75,7 +75,7 @@ const DEFAULT_ACTIONS_COLUMN_WIDTH = 150; templateUrl: './extensible-table.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class ExtensibleTableComponent implements OnChanges, AfterViewInit { +export class ExtensibleTableComponent implements OnChanges, AfterViewInit, OnDestroy { readonly #injector = inject(Injector); readonly getInjected = this.#injector.get.bind(this.#injector); protected readonly cdr = inject(ChangeDetectorRef); @@ -113,11 +113,17 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn this._selectionType = typeof value === 'string' ? SelectionType[value] : value; } _selectionType: SelectionType = SelectionType.multiClick; - - + @Input() selected: any[] = []; @Output() selectionChange = new EventEmitter(); + // Infinite scroll configuration + @Input() infiniteScroll = false; + @Input() isLoading = false; + @Input() scrollThreshold = 10; + @Output() loadMore = new EventEmitter(); + @Input() tableHeight: number; + hasAtLeastOnePermittedAction: boolean; readonly propList: EntityPropList; @@ -129,6 +135,12 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn // Signal for actions column width private readonly _actionsColumnWidth = signal(DEFAULT_ACTIONS_COLUMN_WIDTH); + // Infinite scroll: debounced load more subject + private readonly loadMoreSubject = new Subject(); + private readonly loadMoreSubscription = this.loadMoreSubject + .pipe(debounceTime(100), distinctUntilChanged()) + .subscribe(() => this.triggerLoadMore()); + readonly columnWidths = computed(() => { const actionsColumn = this._actionsColumnWidth(); const widths = [actionsColumn]; @@ -216,7 +228,6 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn return record; }); - } isVisibleActions(rowData: any): boolean { @@ -247,10 +258,50 @@ export class ExtensibleTableComponent implements OnChanges, AfterViewIn this.selectionChange.emit(selected); } + onScroll(scrollEvent: Event): void { + if (!this.shouldHandleScroll()) { + return; + } + + const target = scrollEvent.target as HTMLElement; + if (!target) { + return; + } + + if (this.isNearScrollBottom(target)) { + this.loadMoreSubject.next(); + } + } + + private shouldHandleScroll(): boolean { + return this.infiniteScroll && !this.isLoading; + } + + private isNearScrollBottom(element: HTMLElement): boolean { + const { offsetHeight, scrollTop, scrollHeight } = element; + return offsetHeight + scrollTop >= scrollHeight - this.scrollThreshold; + } + + private triggerLoadMore(): void { + this.loadMore.emit(); + } + + getTableHeight() { + if (!this.infiniteScroll) return 'auto'; + + return this.tableHeight ? `${this.tableHeight}px` : 'auto'; + } + ngAfterViewInit(): void { - this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => { + if (!this.infiniteScroll) { + this.list?.requestStatus$?.pipe(filter(status => status === 'loading')).subscribe(() => { this.data = []; this.cdr.markForCheck(); }); + } + } + + ngOnDestroy(): void { + this.loadMoreSubscription.unsubscribe(); } } diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts index 5307a4a671..48bea482ca 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/grid-actions/grid-actions.component.ts @@ -1,15 +1,14 @@ -import { - ChangeDetectionStrategy, - Component, - Injector, - Input, - TrackByFunction, +import { + ChangeDetectionStrategy, + Component, + Input, + TrackByFunction, } from '@angular/core'; import { EntityAction, EntityActionList } from '../../models/entity-actions'; import { EXTENSIONS_ACTION_TYPE } from '../../tokens/extensions.token'; import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; import { NgbDropdownModule, NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; -import { LocalizationModule, PermissionDirective } from '@abp/ng.core'; +import { LocalizationPipe, PermissionDirective } from '@abp/ng.core'; import { EllipsisDirective } from '@abp/ng.theme.shared'; import { NgClass, NgTemplateOutlet } from '@angular/common'; @@ -20,7 +19,7 @@ import { NgClass, NgTemplateOutlet } from '@angular/common'; EllipsisDirective, PermissionDirective, NgClass, - LocalizationModule, + LocalizationPipe, NgTemplateOutlet, NgbTooltipModule, ], @@ -43,7 +42,7 @@ export class GridActionsComponent extends AbstractActionsComponent> = (_, item) => item.text; - constructor(injector: Injector) { - super(injector); + constructor() { + super(); } } diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/multi-select/extensible-form-multiselect.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/multi-select/extensible-form-multiselect.component.ts index 96860b31b4..900800ee59 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/multi-select/extensible-form-multiselect.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/multi-select/extensible-form-multiselect.component.ts @@ -1,6 +1,5 @@ import { Component, ChangeDetectionStrategy, forwardRef, input } from '@angular/core'; -import { NG_VALUE_ACCESSOR, ControlValueAccessor, ReactiveFormsModule } from '@angular/forms'; -import { CommonModule } from '@angular/common'; +import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; import { ABP, LocalizationPipe } from '@abp/ng.core'; import { FormProp } from '../../models/form-props'; import { NgxValidateCoreModule } from '@ngx-validate/core'; @@ -37,7 +36,7 @@ const EXTENSIBLE_FORM_MULTI_SELECT_CONTROL_VALUE_ACCESSOR = { `, providers: [EXTENSIBLE_FORM_MULTI_SELECT_CONTROL_VALUE_ACCESSOR], - imports: [LocalizationPipe, CommonModule, ReactiveFormsModule, NgxValidateCoreModule], + imports: [LocalizationPipe, NgxValidateCoreModule], changeDetection: ChangeDetectionStrategy.OnPush, }) export class ExtensibleFormMultiselectComponent implements ControlValueAccessor { diff --git a/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.ts b/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.ts index 7492b6f94e..9dadb7497a 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/components/page-toolbar/page-toolbar.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Injector, TrackByFunction } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Injector, TrackByFunction, inject } from '@angular/core'; import { HasCreateInjectorPipe, ToolbarAction, @@ -9,7 +9,7 @@ import { import { EXTENSIONS_ACTION_TYPE } from '../../tokens/extensions.token'; import { AbstractActionsComponent } from '../abstract-actions/abstract-actions.component'; import { CreateInjectorPipe } from '../../pipes/create-injector.pipe'; -import { LocalizationModule, PermissionDirective } from '@abp/ng.core'; +import { LocalizationPipe, PermissionDirective } from '@abp/ng.core'; import { NgClass, NgComponentOutlet } from '@angular/common'; @Component({ @@ -18,7 +18,7 @@ import { NgClass, NgComponentOutlet } from '@angular/common'; imports: [ CreateInjectorPipe, PermissionDirective, - LocalizationModule, + LocalizationPipe, NgClass, NgComponentOutlet, ], @@ -35,6 +35,8 @@ export class PageToolbarComponent extends AbstractActionsComponent> implements HasCreateInjectorPipe { + readonly injector: Injector; + defaultBtnClass = 'btn btn-sm btn-primary'; getData = () => this.data; @@ -42,8 +44,12 @@ export class PageToolbarComponent readonly trackByFn: TrackByFunction> = (_, item) => item.action || item.component; - constructor(public readonly injector: Injector) { - super(injector); + constructor() { + const injector = inject(Injector); + + super(); + + this.injector = injector; } asToolbarAction(value: ToolbarActionDefault): { value: ToolbarAction } { diff --git a/npm/ng-packs/packages/components/extensible/src/lib/directives/prop-data.directive.ts b/npm/ng-packs/packages/components/extensible/src/lib/directives/prop-data.directive.ts index 2bba45ff70..b7a05d314b 100644 --- a/npm/ng-packs/packages/components/extensible/src/lib/directives/prop-data.directive.ts +++ b/npm/ng-packs/packages/components/extensible/src/lib/directives/prop-data.directive.ts @@ -1,12 +1,13 @@ /* eslint-disable @angular-eslint/no-input-rename */ -import { - Directive, - Injector, - Input, - OnChanges, - OnDestroy, - TemplateRef, - ViewContainerRef, +import { + Directive, + Injector, + Input, + OnChanges, + OnDestroy, + TemplateRef, + ViewContainerRef, + inject } from '@angular/core'; import { PropData, PropList } from '../models/props'; @@ -18,6 +19,9 @@ export class PropDataDirective> extends PropData> implements OnChanges, OnDestroy { + private tempRef = inject>(TemplateRef); + private vcRef = inject(ViewContainerRef); + @Input('abpPropDataFromList') propList?: L; @Input('abpPropDataWithRecord') record!: InferredData['record']; @@ -26,11 +30,9 @@ export class PropDataDirective> readonly getInjected: InferredData['getInjected']; - constructor( - private tempRef: TemplateRef, - private vcRef: ViewContainerRef, - injector: Injector, - ) { + constructor() { + const injector = inject(Injector); + super(); this.getInjected = injector.get.bind(injector); diff --git a/npm/ng-packs/packages/components/page/src/page-part.directive.ts b/npm/ng-packs/packages/components/page/src/page-part.directive.ts index a89302916b..bfaf1e60b6 100644 --- a/npm/ng-packs/packages/components/page/src/page-part.directive.ts +++ b/npm/ng-packs/packages/components/page/src/page-part.directive.ts @@ -1,18 +1,17 @@ -import { - Directive, - TemplateRef, - ViewContainerRef, - Input, - InjectionToken, - Optional, - Inject, - OnInit, - OnDestroy, - Injector, - OnChanges, - SimpleChanges, - SimpleChange, -} from '@angular/core'; +import { + Directive, + TemplateRef, + ViewContainerRef, + Input, + InjectionToken, + OnInit, + OnDestroy, + Injector, + OnChanges, + SimpleChanges, + SimpleChange, + inject + } from '@angular/core'; import { Observable, Subscription, of } from 'rxjs'; export interface PageRenderStrategy { @@ -28,6 +27,11 @@ export const PAGE_RENDER_STRATEGY = new InjectionToken('PAGE selector: '[abpPagePart]', }) export class PagePartDirective implements OnInit, OnDestroy, OnChanges { + private templateRef = inject>(TemplateRef); + private viewContainer = inject(ViewContainerRef); + private renderLogic = inject(PAGE_RENDER_STRATEGY, { optional: true })!; + private injector = inject(Injector); + hasRendered = false; type!: string; subscription!: Subscription; @@ -48,13 +52,6 @@ export class PagePartDirective implements OnInit, OnDestroy, OnChanges { } }; - constructor( - private templateRef: TemplateRef, - private viewContainer: ViewContainerRef, - @Optional() @Inject(PAGE_RENDER_STRATEGY) private renderLogic: PageRenderStrategy, - private injector: Injector, - ) {} - ngOnChanges({ context }: SimpleChanges): void { if (this.renderLogic?.onContextUpdate) { this.renderLogic.onContextUpdate(context); diff --git a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts index 334ff11414..316200704b 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts +++ b/npm/ng-packs/packages/components/tree/src/lib/components/tree.component.ts @@ -28,7 +28,7 @@ import { of } from 'rxjs'; import { DISABLE_TREE_STYLE_LOADING_TOKEN } from '../disable-tree-style-loading.token'; import { TreeNodeTemplateDirective } from '../templates/tree-node-template.directive'; import { ExpandedIconTemplateDirective } from '../templates/expanded-icon-template.directive'; -import { CommonModule } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation'; export type DropEvent = NzFormatEmitEvent & { pos: number }; @@ -41,7 +41,7 @@ export type DropEvent = NzFormatEmitEvent & { pos: number }; providers: [SubscriptionService], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, + NgTemplateOutlet, NzTreeComponent, NgbDropdown, NgbDropdownMenu, diff --git a/npm/ng-packs/packages/components/tree/src/lib/templates/expanded-icon-template.directive.ts b/npm/ng-packs/packages/components/tree/src/lib/templates/expanded-icon-template.directive.ts index 35706bd25f..e7f1bdb913 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/templates/expanded-icon-template.directive.ts +++ b/npm/ng-packs/packages/components/tree/src/lib/templates/expanded-icon-template.directive.ts @@ -1,8 +1,8 @@ -import { Directive, TemplateRef } from '@angular/core'; +import { Directive, TemplateRef, inject } from '@angular/core'; + +@Directive({ + selector: '[abpTreeExpandedIconTemplate],[abp-tree-expanded-icon-template]', +}) +export class ExpandedIconTemplateDirective { template = inject>(TemplateRef); -@Directive({ - selector: '[abpTreeExpandedIconTemplate],[abp-tree-expanded-icon-template]', -}) -export class ExpandedIconTemplateDirective { - constructor(public template: TemplateRef) {} -} +} diff --git a/npm/ng-packs/packages/components/tree/src/lib/templates/tree-node-template.directive.ts b/npm/ng-packs/packages/components/tree/src/lib/templates/tree-node-template.directive.ts index 56c66af04b..70cf228021 100644 --- a/npm/ng-packs/packages/components/tree/src/lib/templates/tree-node-template.directive.ts +++ b/npm/ng-packs/packages/components/tree/src/lib/templates/tree-node-template.directive.ts @@ -1,8 +1,8 @@ -import { Directive, TemplateRef } from '@angular/core'; +import { Directive, TemplateRef, inject } from '@angular/core'; + +@Directive({ + selector: '[abpTreeNodeTemplate],[abp-tree-node-template]', +}) +export class TreeNodeTemplateDirective { template = inject>(TemplateRef); -@Directive({ - selector: '[abpTreeNodeTemplate],[abp-tree-node-template]', -}) -export class TreeNodeTemplateDirective { - constructor(public template: TemplateRef) {} -} +} diff --git a/npm/ng-packs/packages/components/tsconfig.lib.json b/npm/ng-packs/packages/components/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/components/tsconfig.lib.json +++ b/npm/ng-packs/packages/components/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts b/npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts index 50473e4df1..020f750555 100644 --- a/npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts +++ b/npm/ng-packs/packages/core/src/lib/abstracts/auth.guard.ts @@ -21,3 +21,9 @@ export const authGuard: CanActivateFn = () => { console.error('You should add @abp/ng-oauth packages or create your own auth packages.'); return false; }; + + +export const asyncAuthGuard: CanActivateFn = () => { + console.error('You should add @abp/ng-oauth packages or create your own auth packages.'); + return false; +}; \ No newline at end of file diff --git a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts index a5c22db189..7dca0cd388 100644 --- a/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/dynamic-layout.component.ts @@ -1,4 +1,4 @@ -import { Component, inject, isDevMode, OnInit, Optional, SkipSelf, Type } from '@angular/core'; +import { Component, inject, isDevMode, Type } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { eLayoutType } from '../enums/common'; import { ABP } from '../models'; @@ -12,7 +12,8 @@ import { findRoute, getRoutePath } from '../utils/route-utils'; import { TreeNode } from '../utils/tree-utils'; import { DYNAMIC_LAYOUTS_TOKEN } from '../tokens/dynamic-layout.token'; import { EnvironmentService } from '../services'; -import { CommonModule } from '@angular/common'; +import { NgComponentOutlet } from '@angular/common'; +import { filter, take } from 'rxjs'; @Component({ selector: 'abp-dynamic-layout', @@ -22,9 +23,9 @@ import { CommonModule } from '@angular/common'; } `, providers: [SubscriptionService], - imports: [CommonModule], + imports: [NgComponentOutlet], }) -export class DynamicLayoutComponent implements OnInit { +export class DynamicLayoutComponent { layout?: Type; layoutKey?: eLayoutType; readonly layouts = inject(DYNAMIC_LAYOUTS_TOKEN); @@ -39,24 +40,16 @@ export class DynamicLayoutComponent implements OnInit { protected readonly routerEvents = inject(RouterEvents); protected readonly environment = inject(EnvironmentService); - constructor(@Optional() @SkipSelf() dynamicLayoutComponent: DynamicLayoutComponent) { + constructor() { + const dynamicLayoutComponent = inject(DynamicLayoutComponent, { optional: true, skipSelf: true }); + if (dynamicLayoutComponent) { if (isDevMode()) console.warn('DynamicLayoutComponent must be used only in AppComponent.'); return; } this.checkLayoutOnNavigationEnd(); this.listenToLanguageChange(); - } - - ngOnInit(): void { - if (this.layout) { - return; - } - - const { oAuthConfig } = this.environment.getEnvironment(); - if (oAuthConfig.responseType === 'code') { - this.getLayout(); - } + this.listenToEnvironmentChange(); } private checkLayoutOnNavigationEnd() { @@ -118,4 +111,19 @@ export class DynamicLayoutComponent implements OnInit { private getComponent(key: string): ReplaceableComponents.ReplaceableComponent | undefined { return this.replaceableComponents.get(key); } + + private listenToEnvironmentChange() { + this.environment + .createOnUpdateStream(x => x.oAuthConfig) + .pipe( + take(1), + filter(config => config.responseType === 'code'), + ) + .subscribe(() => { + if (this.layout) { + return; + } + this.getLayout(); + }); + } } diff --git a/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts b/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts index 680c7919a8..6d6cc59191 100644 --- a/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/replaceable-route-container.component.ts @@ -1,10 +1,10 @@ -import { Component, OnInit, Type } from '@angular/core'; +import { Component, OnInit, Type, inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { distinctUntilChanged } from 'rxjs/operators'; import { ReplaceableComponents } from '../models/replaceable-components'; import { ReplaceableComponentsService } from '../services/replaceable-components.service'; import { SubscriptionService } from '../services/subscription.service'; -import { CommonModule } from '@angular/common'; +import { NgComponentOutlet } from '@angular/common'; @Component({ selector: 'abp-replaceable-route-container', @@ -12,21 +12,19 @@ import { CommonModule } from '@angular/common'; `, providers: [SubscriptionService], - imports: [CommonModule], + imports: [NgComponentOutlet], }) export class ReplaceableRouteContainerComponent implements OnInit { + private route = inject(ActivatedRoute); + private replaceableComponents = inject(ReplaceableComponentsService); + private subscription = inject(SubscriptionService); + defaultComponent!: Type; componentKey!: string; externalComponent?: Type; - constructor( - private route: ActivatedRoute, - private replaceableComponents: ReplaceableComponentsService, - private subscription: SubscriptionService, - ) {} - ngOnInit() { this.defaultComponent = this.route.snapshot.data.replaceableComponent.defaultComponent; this.componentKey = ( diff --git a/npm/ng-packs/packages/core/src/lib/components/router-outlet.component.ts b/npm/ng-packs/packages/core/src/lib/components/router-outlet.component.ts index 73af8037c2..4a0dc6c099 100644 --- a/npm/ng-packs/packages/core/src/lib/components/router-outlet.component.ts +++ b/npm/ng-packs/packages/core/src/lib/components/router-outlet.component.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core'; -import { RouterModule } from '@angular/router'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'abp-router-outlet', template: ` `, - imports: [RouterModule], + imports: [RouterOutlet], }) export class RouterOutletComponent {} diff --git a/npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts index f34d187939..398eaef9cc 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/autofocus.directive.ts @@ -1,9 +1,11 @@ -import { AfterViewInit, Directive, ElementRef, Input } from '@angular/core'; +import { AfterViewInit, Directive, ElementRef, Input, inject } from '@angular/core'; @Directive({ selector: '[autofocus]', }) export class AutofocusDirective implements AfterViewInit { + private elRef = inject(ElementRef); + private _delay = 0; @Input('autofocus') @@ -15,8 +17,6 @@ export class AutofocusDirective implements AfterViewInit { return this._delay; } - constructor(private elRef: ElementRef) {} - ngAfterViewInit(): void { setTimeout(() => this.elRef.nativeElement.focus(), this.delay as number); } diff --git a/npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts index 204db6b418..239aed688d 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/debounce.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, Input, OnInit, Output, inject } from '@angular/core'; import { fromEvent } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { SubscriptionService } from '../services/subscription.service'; @@ -8,15 +8,13 @@ import { SubscriptionService } from '../services/subscription.service'; providers: [SubscriptionService], }) export class InputEventDebounceDirective implements OnInit { + private el = inject(ElementRef); + private subscription = inject(SubscriptionService); + @Input() debounce = 300; @Output('input.debounce') readonly debounceEvent = new EventEmitter(); - constructor( - private el: ElementRef, - private subscription: SubscriptionService, - ) {} - ngOnInit(): void { const input$ = fromEvent(this.el.nativeElement, 'input').pipe( debounceTime(this.debounce), diff --git a/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts index 1e37e1c908..7b3bd0319b 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/for.directive.ts @@ -1,15 +1,16 @@ -import { - Directive, - EmbeddedViewRef, - Input, - IterableChangeRecord, - IterableChanges, - IterableDiffer, - IterableDiffers, - OnChanges, - TemplateRef, - TrackByFunction, - ViewContainerRef, +import { + Directive, + EmbeddedViewRef, + Input, + IterableChangeRecord, + IterableChanges, + IterableDiffer, + IterableDiffers, + OnChanges, + TemplateRef, + TrackByFunction, + ViewContainerRef, + inject } from '@angular/core'; import clone from 'just-clone'; import compare from 'just-compare'; @@ -36,6 +37,10 @@ class RecordView { selector: '[abpFor]', }) export class ForDirective implements OnChanges { + private tempRef = inject>(TemplateRef); + private vcRef = inject(ViewContainerRef); + private differs = inject(IterableDiffers); + // eslint-disable-next-line @angular-eslint/no-input-rename @Input('abpForOf') items!: any[]; @@ -73,12 +78,6 @@ export class ForDirective implements OnChanges { return this.trackBy || ((index: number, item: any) => (item as any).id || index); } - constructor( - private tempRef: TemplateRef, - private vcRef: ViewContainerRef, - private differs: IterableDiffers, - ) {} - private iterateOverAppliedOperations(changes: IterableChanges) { const rw: RecordView[] = []; diff --git a/npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts index 25972ccb71..879fbdf780 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/form-submit.directive.ts @@ -1,12 +1,12 @@ -import { - ChangeDetectorRef, - Directive, - ElementRef, - EventEmitter, - Input, - OnInit, - Output, - Self, +import { + ChangeDetectorRef, + Directive, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + inject } from '@angular/core'; import { FormGroupDirective, UntypedFormControl, UntypedFormGroup } from '@angular/forms'; import { fromEvent } from 'rxjs'; @@ -22,6 +22,11 @@ type Controls = { [key: string]: UntypedFormControl } | UntypedFormGroup[]; providers: [SubscriptionService], }) export class FormSubmitDirective implements OnInit { + private formGroupDirective = inject(FormGroupDirective, { self: true }); + private host = inject>(ElementRef); + private cdRef = inject(ChangeDetectorRef); + private subscription = inject(SubscriptionService); + @Input() debounce = 200; @@ -36,13 +41,6 @@ export class FormSubmitDirective implements OnInit { executedNgSubmit = false; - constructor( - @Self() private formGroupDirective: FormGroupDirective, - private host: ElementRef, - private cdRef: ChangeDetectorRef, - private subscription: SubscriptionService, - ) {} - ngOnInit() { this.subscription.addOne(this.formGroupDirective.ngSubmit, () => { if (this.markAsDirtyWhenSubmit) { diff --git a/npm/ng-packs/packages/core/src/lib/directives/init.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/init.directive.ts index 293384e4e1..1271f031ba 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/init.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/init.directive.ts @@ -1,12 +1,12 @@ -import { Directive, Output, EventEmitter, ElementRef, AfterViewInit } from '@angular/core'; +import { Directive, Output, EventEmitter, ElementRef, AfterViewInit, inject } from '@angular/core'; @Directive({ selector: '[abpInit]', }) export class InitDirective implements AfterViewInit { - @Output('abpInit') readonly init = new EventEmitter>(); + private elRef = inject(ElementRef); - constructor(private elRef: ElementRef) {} + @Output('abpInit') readonly init = new EventEmitter>(); ngAfterViewInit() { this.init.emit(this.elRef); diff --git a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts index f277bf50ee..50b805dba4 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/permission.directive.ts @@ -1,14 +1,13 @@ -import { - AfterViewInit, - ChangeDetectorRef, - Directive, - Inject, - Input, - OnChanges, - OnDestroy, - Optional, - TemplateRef, - ViewContainerRef, +import { + AfterViewInit, + ChangeDetectorRef, + Directive, + Input, + OnChanges, + OnDestroy, + TemplateRef, + ViewContainerRef, + inject } from '@angular/core'; import { ReplaySubject, Subscription } from 'rxjs'; import { distinctUntilChanged, take } from 'rxjs/operators'; @@ -20,6 +19,12 @@ import { QueueManager } from '../utils/queue'; selector: '[abpPermission]', }) export class PermissionDirective implements OnDestroy, OnChanges, AfterViewInit { + private templateRef = inject>(TemplateRef, { optional: true })!; + private vcRef = inject(ViewContainerRef); + private permissionService = inject(PermissionService); + private cdRef = inject(ChangeDetectorRef); + queue = inject(QUEUE_MANAGER); + @Input('abpPermission') condition: string | undefined; @Input('abpPermissionRunChangeDetection') runChangeDetection = true; @@ -30,14 +35,6 @@ export class PermissionDirective implements OnDestroy, OnChanges, AfterViewInit rendered = false; - constructor( - @Optional() private templateRef: TemplateRef, - private vcRef: ViewContainerRef, - private permissionService: PermissionService, - private cdRef: ChangeDetectorRef, - @Inject(QUEUE_MANAGER) public queue: QueueManager, - ) {} - private check() { if (this.subscription) { this.subscription.unsubscribe(); diff --git a/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts index 9a1bedcd71..7e948cc20f 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/replaceable-template.directive.ts @@ -1,13 +1,14 @@ -import { - Directive, - Injector, - Input, - OnChanges, - OnInit, - SimpleChanges, - TemplateRef, - Type, - ViewContainerRef, +import { + Directive, + Injector, + Input, + OnChanges, + OnInit, + SimpleChanges, + TemplateRef, + Type, + ViewContainerRef, + inject } from '@angular/core'; import compare from 'just-compare'; import { Subscription } from 'rxjs'; @@ -22,6 +23,12 @@ import { SubscriptionService } from '../services/subscription.service'; providers: [SubscriptionService], }) export class ReplaceableTemplateDirective implements OnInit, OnChanges { + private injector = inject(Injector); + private templateRef = inject>(TemplateRef); + private vcRef = inject(ViewContainerRef); + private replaceableComponents = inject(ReplaceableComponentsService); + private subscription = inject(SubscriptionService); + @Input('abpReplaceableTemplate') data!: ReplaceableComponents.ReplaceableTemplateDirectiveInput; @@ -40,13 +47,7 @@ export class ReplaceableTemplateDirective implements OnInit, OnChanges { initialized = false; - constructor( - private injector: Injector, - private templateRef: TemplateRef, - private vcRef: ViewContainerRef, - private replaceableComponents: ReplaceableComponentsService, - private subscription: SubscriptionService, - ) { + constructor() { this.context = { initTemplate: (ref: any) => { this.resetDefaultComponent(); diff --git a/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts b/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts index ddc92cb56b..a1af9bf07e 100644 --- a/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts +++ b/npm/ng-packs/packages/core/src/lib/directives/stop-propagation.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, EventEmitter, OnInit, Output } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, OnInit, Output, inject } from '@angular/core'; import { fromEvent } from 'rxjs'; import { SubscriptionService } from '../services/subscription.service'; @@ -7,12 +7,10 @@ import { SubscriptionService } from '../services/subscription.service'; providers: [SubscriptionService], }) export class StopPropagationDirective implements OnInit { - @Output('click.stop') readonly stopPropEvent = new EventEmitter(); + private el = inject(ElementRef); + private subscription = inject(SubscriptionService); - constructor( - private el: ElementRef, - private subscription: SubscriptionService, - ) {} + @Output('click.stop') readonly stopPropEvent = new EventEmitter(); ngOnInit(): void { this.subscription.addOne(fromEvent(this.el.nativeElement, 'click'), event => { diff --git a/npm/ng-packs/packages/core/src/lib/handlers/routes.handler.ts b/npm/ng-packs/packages/core/src/lib/handlers/routes.handler.ts index 76dc66b3ae..d8880b4fbf 100644 --- a/npm/ng-packs/packages/core/src/lib/handlers/routes.handler.ts +++ b/npm/ng-packs/packages/core/src/lib/handlers/routes.handler.ts @@ -1,4 +1,4 @@ -import { Injectable, Optional } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Route, Router } from '@angular/router'; import { ABP } from '../models'; import { RoutesService } from '../services/routes.service'; @@ -7,10 +7,10 @@ import { RoutesService } from '../services/routes.service'; providedIn: 'root', }) export class RoutesHandler { - constructor( - private routes: RoutesService, - @Optional() private router: Router, - ) { + private routes = inject(RoutesService); + private router = inject(Router, { optional: true })!; + + constructor() { this.addRoutes(); } diff --git a/npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts b/npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts index f5fc270e7c..b0d8d5e0b5 100644 --- a/npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts +++ b/npm/ng-packs/packages/core/src/lib/interceptors/api.interceptor.ts @@ -5,7 +5,7 @@ import { HttpRequest, HttpEvent, } from '@angular/common/http'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { finalize } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { HttpWaitService } from '../services'; @@ -14,7 +14,7 @@ import { HttpWaitService } from '../services'; providedIn: 'root', }) export class ApiInterceptor implements IApiInterceptor { - constructor(private httpWaitService: HttpWaitService) {} + private httpWaitService = inject(HttpWaitService); getAdditionalHeaders(existingHeaders?: HttpHeaders) { return existingHeaders || new HttpHeaders(); diff --git a/npm/ng-packs/packages/core/src/lib/pipes/localization.pipe.ts b/npm/ng-packs/packages/core/src/lib/pipes/localization.pipe.ts index 2dbac68eb7..bbb2a71752 100644 --- a/npm/ng-packs/packages/core/src/lib/pipes/localization.pipe.ts +++ b/npm/ng-packs/packages/core/src/lib/pipes/localization.pipe.ts @@ -1,4 +1,4 @@ -import { Injectable, Pipe, PipeTransform } from '@angular/core'; +import { Injectable, Pipe, PipeTransform, inject } from '@angular/core'; import { LocalizationWithDefault } from '../models/localization'; import { LocalizationService } from '../services/localization.service'; @@ -7,7 +7,8 @@ import { LocalizationService } from '../services/localization.service'; name: 'abpLocalization', }) export class LocalizationPipe implements PipeTransform { - constructor(private localization: LocalizationService) {} + private localization = inject(LocalizationService); + transform( value: string | LocalizationWithDefault = '', diff --git a/npm/ng-packs/packages/core/src/lib/pipes/short-date-time.pipe.ts b/npm/ng-packs/packages/core/src/lib/pipes/short-date-time.pipe.ts index b72ca45dde..8ed6dc55b4 100644 --- a/npm/ng-packs/packages/core/src/lib/pipes/short-date-time.pipe.ts +++ b/npm/ng-packs/packages/core/src/lib/pipes/short-date-time.pipe.ts @@ -1,5 +1,5 @@ import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE } from '@angular/common'; -import { Inject, LOCALE_ID, Optional, Pipe, PipeTransform } from '@angular/core'; +import { LOCALE_ID, Pipe, PipeTransform, inject } from '@angular/core'; import { ConfigStateService } from '../services'; import { getShortDateShortTimeFormat } from '../utils/date-utils'; @@ -8,11 +8,12 @@ import { getShortDateShortTimeFormat } from '../utils/date-utils'; pure: true, }) export class ShortDateTimePipe extends DatePipe implements PipeTransform { - constructor( - private configStateService: ConfigStateService, - @Inject(LOCALE_ID) locale: string, - @Inject(DATE_PIPE_DEFAULT_TIMEZONE) @Optional() defaultTimezone?: string | null, - ) { + private configStateService = inject(ConfigStateService); + + constructor() { + const locale = inject(LOCALE_ID); + const defaultTimezone = inject(DATE_PIPE_DEFAULT_TIMEZONE, { optional: true }); + super(locale, defaultTimezone); } diff --git a/npm/ng-packs/packages/core/src/lib/pipes/short-date.pipe.ts b/npm/ng-packs/packages/core/src/lib/pipes/short-date.pipe.ts index 26a7e1552f..9968009e49 100644 --- a/npm/ng-packs/packages/core/src/lib/pipes/short-date.pipe.ts +++ b/npm/ng-packs/packages/core/src/lib/pipes/short-date.pipe.ts @@ -1,5 +1,5 @@ import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE } from '@angular/common'; -import { Inject, LOCALE_ID, Optional, Pipe, PipeTransform } from '@angular/core'; +import { LOCALE_ID, Pipe, PipeTransform, inject } from '@angular/core'; import { ConfigStateService } from '../services'; import { getShortDateFormat } from '../utils/date-utils'; @@ -8,11 +8,12 @@ import { getShortDateFormat } from '../utils/date-utils'; pure: true, }) export class ShortDatePipe extends DatePipe implements PipeTransform { - constructor( - private configStateService: ConfigStateService, - @Inject(LOCALE_ID) locale: string, - @Inject(DATE_PIPE_DEFAULT_TIMEZONE) @Optional() defaultTimezone?: string | null, - ) { + private configStateService = inject(ConfigStateService); + + constructor() { + const locale = inject(LOCALE_ID); + const defaultTimezone = inject(DATE_PIPE_DEFAULT_TIMEZONE, { optional: true }); + super(locale, defaultTimezone); } diff --git a/npm/ng-packs/packages/core/src/lib/pipes/short-time.pipe.ts b/npm/ng-packs/packages/core/src/lib/pipes/short-time.pipe.ts index 54e70802f8..fac722af5e 100644 --- a/npm/ng-packs/packages/core/src/lib/pipes/short-time.pipe.ts +++ b/npm/ng-packs/packages/core/src/lib/pipes/short-time.pipe.ts @@ -1,5 +1,5 @@ import { DatePipe, DATE_PIPE_DEFAULT_TIMEZONE } from '@angular/common'; -import { Inject, LOCALE_ID, Optional, Pipe, PipeTransform } from '@angular/core'; +import { LOCALE_ID, Pipe, PipeTransform, inject } from '@angular/core'; import { ConfigStateService } from '../services'; import { getShortTimeFormat } from '../utils/date-utils'; @@ -8,11 +8,12 @@ import { getShortTimeFormat } from '../utils/date-utils'; pure: true, }) export class ShortTimePipe extends DatePipe implements PipeTransform { - constructor( - private configStateService: ConfigStateService, - @Inject(LOCALE_ID) locale: string, - @Inject(DATE_PIPE_DEFAULT_TIMEZONE) @Optional() defaultTimezone?: string | null, - ) { + private configStateService = inject(ConfigStateService); + + constructor() { + const locale = inject(LOCALE_ID); + const defaultTimezone = inject(DATE_PIPE_DEFAULT_TIMEZONE, { optional: true }); + super(locale, defaultTimezone); } diff --git a/npm/ng-packs/packages/core/src/lib/pipes/to-injector.pipe.ts b/npm/ng-packs/packages/core/src/lib/pipes/to-injector.pipe.ts index 5a1ac4df29..77663a1262 100644 --- a/npm/ng-packs/packages/core/src/lib/pipes/to-injector.pipe.ts +++ b/npm/ng-packs/packages/core/src/lib/pipes/to-injector.pipe.ts @@ -1,4 +1,4 @@ -import { InjectionToken, Injector, Pipe, PipeTransform } from '@angular/core'; +import { InjectionToken, Injector, Pipe, PipeTransform, inject } from '@angular/core'; export const INJECTOR_PIPE_DATA_TOKEN = new InjectionToken( 'INJECTOR_PIPE_DATA_TOKEN', @@ -8,7 +8,8 @@ export const INJECTOR_PIPE_DATA_TOKEN = new InjectionToken( name: 'toInjector', }) export class ToInjectorPipe implements PipeTransform { - constructor(private injector: Injector) {} + private injector = inject(Injector); + transform( value: any, token: InjectionToken = INJECTOR_PIPE_DATA_TOKEN, diff --git a/npm/ng-packs/packages/core/src/lib/providers/locale.provider.ts b/npm/ng-packs/packages/core/src/lib/providers/locale.provider.ts index ca90c4bc33..9d34802da1 100644 --- a/npm/ng-packs/packages/core/src/lib/providers/locale.provider.ts +++ b/npm/ng-packs/packages/core/src/lib/providers/locale.provider.ts @@ -1,10 +1,12 @@ -import { LOCALE_ID, Provider } from '@angular/core'; +import { LOCALE_ID, Provider, inject } from '@angular/core'; import { differentLocales } from '../constants/different-locales'; import { LocalizationService } from '../services/localization.service'; import { checkHasProp } from '../utils/common-utils'; export class LocaleId extends String { - constructor(private localizationService: LocalizationService) { + private localizationService = inject(LocalizationService); + + constructor() { super(); } @@ -24,5 +26,4 @@ export class LocaleId extends String { export const LocaleProvider: Provider = { provide: LOCALE_ID, useClass: LocaleId, - deps: [LocalizationService], -}; +}; \ No newline at end of file diff --git a/npm/ng-packs/packages/core/src/lib/proxy/pages/abp/multi-tenancy/abp-tenant.service.ts b/npm/ng-packs/packages/core/src/lib/proxy/pages/abp/multi-tenancy/abp-tenant.service.ts index 039ddc707e..d778e0a279 100644 --- a/npm/ng-packs/packages/core/src/lib/proxy/pages/abp/multi-tenancy/abp-tenant.service.ts +++ b/npm/ng-packs/packages/core/src/lib/proxy/pages/abp/multi-tenancy/abp-tenant.service.ts @@ -1,12 +1,14 @@ import { RestService } from '../../../../services'; import { Rest } from '../../../../models'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import type { FindTenantResultDto } from '../../../volo/abp/asp-net-core/mvc/multi-tenancy/models'; @Injectable({ providedIn: 'root', }) -export class AbpTenantService { +export class AbpTenantService { + private restService = inject(RestService); + apiName = 'abp'; @@ -24,6 +26,4 @@ export class AbpTenantService { url: `/api/abp/multi-tenancy/tenants/by-name/${name}`, }, { apiName: this.apiName,...config }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/api-exploring/abp-api-definition.service.ts b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/api-exploring/abp-api-definition.service.ts index a331106214..770faca476 100644 --- a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/api-exploring/abp-api-definition.service.ts +++ b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/api-exploring/abp-api-definition.service.ts @@ -1,12 +1,14 @@ import { RestService } from '../../../../../../services'; import { Rest } from '../../../../../../models'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import type { ApplicationApiDescriptionModel, ApplicationApiDescriptionModelRequestDto } from '../../../http/modeling/models'; @Injectable({ providedIn: 'root', }) -export class AbpApiDefinitionService { +export class AbpApiDefinitionService { + private restService = inject(RestService); + apiName = 'abp'; @@ -17,6 +19,4 @@ export class AbpApiDefinitionService { params: { includeTypes: model.includeTypes }, }, { apiName: this.apiName,...config }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts index 3c04af51fc..9dc35fa7b6 100644 --- a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts +++ b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service.ts @@ -1,12 +1,14 @@ import type { ApplicationConfigurationDto, ApplicationConfigurationRequestOptions } from './models'; import { RestService } from '../../../../../../services'; import { Rest } from '../../../../../../models'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) -export class AbpApplicationConfigurationService { +export class AbpApplicationConfigurationService { + private restService = inject(RestService); + apiName = 'abp'; @@ -17,6 +19,4 @@ export class AbpApplicationConfigurationService { params: { includeLocalizationResources: options.includeLocalizationResources }, }, { apiName: this.apiName, ...config }); - - constructor(private restService: RestService) { } } diff --git a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-localization.service.ts b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-localization.service.ts index 3b9fd80584..aa3f495b53 100644 --- a/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-localization.service.ts @@ -1,12 +1,14 @@ import type { ApplicationLocalizationDto, ApplicationLocalizationRequestDto } from './models'; import { RestService } from '../../../../../../services'; import { Rest } from '../../../../../../models'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) -export class AbpApplicationLocalizationService { +export class AbpApplicationLocalizationService { + private restService = inject(RestService); + apiName = 'abp'; @@ -17,6 +19,4 @@ export class AbpApplicationLocalizationService { params: { cultureName: input.cultureName, onlyDynamics: input.onlyDynamics }, }, { apiName: this.apiName,...config }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts b/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts index 475f6d76f3..1243e8b2ba 100644 --- a/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/config-state.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Optional } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { map, switchMap, take, tap } from 'rxjs/operators'; import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; @@ -15,6 +15,10 @@ import { InternalStore } from '../utils/internal-store-utils'; providedIn: 'root', }) export class ConfigStateService { + private abpConfigService = inject(AbpApplicationConfigurationService); + private abpApplicationLocalizationService = inject(AbpApplicationLocalizationService); + private readonly includeLocalizationResources = inject(INCUDE_LOCALIZATION_RESOURCES_TOKEN, { optional: true }); + private updateSubject = new Subject(); private readonly store = new InternalStore({} as ApplicationConfigurationDto); @@ -27,13 +31,7 @@ export class ConfigStateService { get createOnUpdateStream() { return this.store.sliceUpdate; } - constructor( - private abpConfigService: AbpApplicationConfigurationService, - private abpApplicationLocalizationService: AbpApplicationLocalizationService, - @Optional() - @Inject(INCUDE_LOCALIZATION_RESOURCES_TOKEN) - private readonly includeLocalizationResources: boolean | null, - ) { + constructor() { this.initUpdateStream(); } diff --git a/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts b/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts index dfcdea330a..d9aac2bd48 100644 --- a/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/content-projection.service.ts @@ -1,9 +1,10 @@ -import { Injectable, Injector, TemplateRef, Type } from '@angular/core'; +import { Injectable, Injector, TemplateRef, Type, inject } from '@angular/core'; import { ProjectionStrategy } from '../strategies/projection.strategy'; @Injectable({ providedIn: 'root' }) export class ContentProjectionService { - constructor(private injector: Injector) {} + private injector = inject(Injector); + projectContent | TemplateRef>( projectionStrategy: ProjectionStrategy, diff --git a/npm/ng-packs/packages/core/src/lib/services/http-wait.service.ts b/npm/ng-packs/packages/core/src/lib/services/http-wait.service.ts index d80599fc25..d8073d8760 100644 --- a/npm/ng-packs/packages/core/src/lib/services/http-wait.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/http-wait.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Injector } from '@angular/core'; +import { Injectable, Injector, inject } from '@angular/core'; import { HttpRequest } from '@angular/common/http'; import { InternalStore } from '../utils/internal-store-utils'; import { getPathName } from '../utils/http-utils'; @@ -26,7 +26,9 @@ export class HttpWaitService { private delay: number; private destroy$ = new Subject(); - constructor(injector: Injector) { + constructor() { + const injector = inject(Injector); + this.delay = injector.get(LOADER_DELAY, 500); } diff --git a/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts b/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts index f50102e15a..8358570db9 100644 --- a/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/lazy-load.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { concat, Observable, of, pipe, throwError } from 'rxjs'; import { delay, retryWhen, shareReplay, take, tap } from 'rxjs/operators'; import { LoadingStrategy } from '../strategies'; @@ -8,9 +8,9 @@ import { ResourceWaitService } from './resource-wait.service'; providedIn: 'root', }) export class LazyLoadService { - readonly loaded = new Map(); + private resourceWaitService = inject(ResourceWaitService); - constructor(private resourceWaitService: ResourceWaitService) {} + readonly loaded = new Map(); load(strategy: LoadingStrategy, retryTimes?: number, retryDelay?: number): Observable { if (this.loaded.has(strategy.path)) return of(new CustomEvent('load')); diff --git a/npm/ng-packs/packages/core/src/lib/services/list.service.ts b/npm/ng-packs/packages/core/src/lib/services/list.service.ts index 204833a66d..3cfc0ecac9 100644 --- a/npm/ng-packs/packages/core/src/lib/services/list.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/list.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Injector, OnDestroy } from '@angular/core'; +import { Injectable, Injector, OnDestroy, inject } from '@angular/core'; import { EMPTY, BehaviorSubject, @@ -119,7 +119,9 @@ export class ListService implements this.next(); }; - constructor(injector: Injector) { + constructor() { + const injector = inject(Injector); + const delay = injector.get(LIST_QUERY_DEBOUNCE_TIME, 300); this.delay = delay ? debounceTime(delay) : tap(); this.get(); diff --git a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts index e01fbb0067..d91679effb 100644 --- a/npm/ng-packs/packages/core/src/lib/services/localization.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/localization.service.ts @@ -1,5 +1,5 @@ import { registerLocaleData } from '@angular/common'; -import { Injectable, Injector, isDevMode, Optional, SkipSelf } from '@angular/core'; +import { Injectable, Injector, isDevMode, inject } from '@angular/core'; import { BehaviorSubject, combineLatest, from, Observable, Subject } from 'rxjs'; import { filter, map, switchMap } from 'rxjs/operators'; import { ABP } from '../models/common'; @@ -17,6 +17,10 @@ import { SessionStateService } from './session-state.service'; @Injectable({ providedIn: 'root' }) export class LocalizationService { + private sessionState = inject(SessionStateService); + private injector = inject(Injector); + private configState = inject(ConfigStateService); + private latestLang = this.sessionState.getLanguage(); private _languageChange$ = new Subject(); @@ -44,14 +48,9 @@ export class LocalizationService { return this._languageChange$.asObservable(); } - constructor( - private sessionState: SessionStateService, - private injector: Injector, - @Optional() - @SkipSelf() - otherInstance: LocalizationService, - private configState: ConfigStateService, - ) { + constructor() { + const otherInstance = inject(LocalizationService, { optional: true, skipSelf: true })!; + if (otherInstance) throw new Error('LocalizationService should have only one instance.'); this.listenToSetLanguage(); diff --git a/npm/ng-packs/packages/core/src/lib/services/multi-tenancy.service.ts b/npm/ng-packs/packages/core/src/lib/services/multi-tenancy.service.ts index 8fbde5cc8e..a88cba1c3d 100644 --- a/npm/ng-packs/packages/core/src/lib/services/multi-tenancy.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/multi-tenancy.service.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { map, switchMap } from 'rxjs/operators'; import { AbpTenantService } from '../proxy/pages/abp/multi-tenancy'; import { @@ -7,11 +7,15 @@ import { } from '../proxy/volo/abp/asp-net-core/mvc/multi-tenancy/models'; import { TENANT_KEY } from '../tokens/tenant-key.token'; import { ConfigStateService } from './config-state.service'; -import { RestService } from './rest.service'; import { SessionStateService } from './session-state.service'; @Injectable({ providedIn: 'root' }) export class MultiTenancyService { + private sessionState = inject(SessionStateService); + private tenantService = inject(AbpTenantService); + private configStateService = inject(ConfigStateService); + tenantKey = inject(TENANT_KEY); + domainTenant: CurrentTenantDto | null = null; isTenantBoxVisible = true; @@ -23,14 +27,6 @@ export class MultiTenancyService { return this.configStateService.refreshAppState().pipe(map(_ => tenant)); }; - constructor( - private restService: RestService, - private sessionState: SessionStateService, - private tenantService: AbpTenantService, - private configStateService: ConfigStateService, - @Inject(TENANT_KEY) public tenantKey: string, - ) { } - setTenantByName(tenantName: string) { return this.tenantService .findTenantByName(tenantName) diff --git a/npm/ng-packs/packages/core/src/lib/services/permission.service.ts b/npm/ng-packs/packages/core/src/lib/services/permission.service.ts index 70724ac814..b45b2a36f5 100644 --- a/npm/ng-packs/packages/core/src/lib/services/permission.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/permission.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { map } from 'rxjs/operators'; import { ABP } from '../models/common'; import { ApplicationConfigurationDto } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/models'; @@ -6,7 +6,8 @@ import { ConfigStateService } from './config-state.service'; @Injectable({ providedIn: 'root' }) export class PermissionService { - constructor(protected configState: ConfigStateService) {} + protected configState = inject(ConfigStateService); + getGrantedPolicy$(key: string) { return this.getStream().pipe( diff --git a/npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts b/npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts index 1f02daa389..c156a237aa 100644 --- a/npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/replaceable-components.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NgZone } from '@angular/core'; +import { Injectable, NgZone, inject } from '@angular/core'; import { Router } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -8,6 +8,9 @@ import { reloadRoute } from '../utils/route-utils'; @Injectable({ providedIn: 'root' }) export class ReplaceableComponentsService { + private ngZone = inject(NgZone); + private router = inject(Router); + private readonly store: InternalStore; get replaceableComponents$(): Observable { @@ -22,7 +25,7 @@ export class ReplaceableComponentsService { return this.store.sliceUpdate(state => state); } - constructor(private ngZone: NgZone, private router: Router) { + constructor() { this.store = new InternalStore([] as ReplaceableComponents.ReplaceableComponent[]); } diff --git a/npm/ng-packs/packages/core/src/lib/services/rest.service.ts b/npm/ng-packs/packages/core/src/lib/services/rest.service.ts index 83f91777ca..a331b6c742 100644 --- a/npm/ng-packs/packages/core/src/lib/services/rest.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/rest.service.ts @@ -1,5 +1,5 @@ import { HttpClient, HttpParameterCodec, HttpParams, HttpRequest } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Observable, throwError } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { ExternalHttpClient } from '../clients/http.client'; @@ -14,13 +14,12 @@ import { HttpErrorReporterService } from './http-error-reporter.service'; providedIn: 'root', }) export class RestService { - constructor( - @Inject(CORE_OPTIONS) protected options: ABP.Root, - protected http: HttpClient, - protected externalHttp: ExternalHttpClient, - protected environment: EnvironmentService, - protected httpErrorReporter: HttpErrorReporterService, - ) { } + protected options = inject(CORE_OPTIONS); + protected http = inject(HttpClient); + protected externalHttp = inject(ExternalHttpClient); + protected environment = inject(EnvironmentService); + protected httpErrorReporter = inject(HttpErrorReporterService); + protected getApiFromStore(apiName: string | undefined): string { return this.environment.getApiUrl(apiName); diff --git a/npm/ng-packs/packages/core/src/lib/services/router-events.service.ts b/npm/ng-packs/packages/core/src/lib/services/router-events.service.ts index 0e17b37b04..9722ab7660 100644 --- a/npm/ng-packs/packages/core/src/lib/services/router-events.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/router-events.service.ts @@ -7,7 +7,6 @@ import { Router, RouterEvent, Event, - RouterState, } from '@angular/router'; import { Observable } from 'rxjs'; import { filter } from 'rxjs/operators'; diff --git a/npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts b/npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts index 96e66a00e3..f68f6d065a 100644 --- a/npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/router-wait.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Injector } from '@angular/core'; +import { Injectable, Injector, inject } from '@angular/core'; import { NavigationStart } from '@angular/router'; import { of, Subject, timer } from 'rxjs'; import { map, mapTo, switchMap, takeUntil, tap } from 'rxjs/operators'; @@ -14,10 +14,14 @@ export interface RouterWaitState { providedIn: 'root', }) export class RouterWaitService { + private routerEvents = inject(RouterEvents); + private store = new InternalStore({ loading: false }); private destroy$ = new Subject(); private delay: number; - constructor(private routerEvents: RouterEvents, injector: Injector) { + constructor() { + const injector = inject(Injector); + this.delay = injector.get(LOADER_DELAY, 500); this.updateLoadingStatusOnNavigationEvents(); } diff --git a/npm/ng-packs/packages/core/src/lib/services/routes.service.ts b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts index 393c724423..175bf76446 100644 --- a/npm/ng-packs/packages/core/src/lib/services/routes.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/routes.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Injector, OnDestroy } from '@angular/core'; +import { Injectable, Injector, OnDestroy, inject } from '@angular/core'; import { BehaviorSubject, Observable, Subscription, map } from 'rxjs'; import { ABP } from '../models/common'; import { OTHERS_GROUP } from '../tokens'; @@ -201,6 +201,8 @@ export abstract class AbstractNavTreeService extends AbstractTreeService implements OnDestroy { + protected injector = inject(Injector); + private subscription: Subscription; private permissionService: PermissionService; private compareFunc; @@ -211,8 +213,10 @@ export abstract class AbstractNavTreeService return this.compareFunc(a, b); }; - constructor(protected injector: Injector) { + constructor() { super(); + const injector = this.injector; + const configState = this.injector.get(ConfigStateService); this.subscription = configState .createOnUpdateStream(state => state) diff --git a/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts b/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts index d6f423983c..7e519dc742 100644 --- a/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts +++ b/npm/ng-packs/packages/core/src/lib/services/session-state.service.ts @@ -12,6 +12,9 @@ import { AbpLocalStorageService } from './local-storage.service'; providedIn: 'root', }) export class SessionStateService { + private configState = inject(ConfigStateService); + private localStorageService = inject(AbpLocalStorageService); + private readonly store = new InternalStore({} as Session.State); protected readonly document = inject(DOCUMENT); @@ -19,10 +22,7 @@ export class SessionStateService { this.localStorageService.setItem('abpSession', JSON.stringify(this.store.state)); }; - constructor( - private configState: ConfigStateService, - private localStorageService: AbpLocalStorageService, - ) { + constructor() { this.init(); this.setInitialLanguage(); } diff --git a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts index 411a9e8041..aac6c41fcd 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/dynamic-layout.component.spec.ts @@ -1,201 +1,201 @@ -import { HttpClient } from '@angular/common/http'; -import { Component, NgModule } from '@angular/core'; -import { ActivatedRoute, RouterModule } from '@angular/router'; -import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; -import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; -import { eLayoutType } from '../enums/common'; -import { ABP } from '../models'; -import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; -import { ReplaceableComponentsService, RoutesService } from '../services'; -import { mockRoutesService } from './routes.service.spec'; - -@Component({ - selector: 'abp-layout-application', - template: '', -}) -class DummyApplicationLayoutComponent {} - -@Component({ - selector: 'abp-layout-account', - template: '', -}) -class DummyAccountLayoutComponent {} - -@Component({ - selector: 'abp-layout-empty', - template: '', -}) -class DummyEmptyLayoutComponent {} - -const LAYOUTS = [ - DummyApplicationLayoutComponent, - DummyAccountLayoutComponent, - DummyEmptyLayoutComponent, -]; - -@NgModule({ - imports: [RouterModule], - declarations: [...LAYOUTS], -}) -class DummyLayoutModule {} - -@Component({ - selector: 'abp-dummy', - template: '{{route.snapshot.data?.name}} works!', -}) -class DummyComponent { - constructor(public route: ActivatedRoute) {} -} - -const routes: ABP.Route[] = [ - { - path: '', - name: 'Root', - }, - { - path: '/parentWithLayout', - name: 'ParentWithLayout', - parentName: 'Root', - layout: eLayoutType.application, - }, - { - path: '/parentWithLayout/childWithoutLayout', - name: 'ChildWithoutLayout', - parentName: 'ParentWithLayout', - }, - { - path: '/parentWithLayout/childWithLayout', - name: 'ChildWithLayout', - parentName: 'ParentWithLayout', - layout: eLayoutType.account, - }, - { - path: '/withData', - name: 'WithData', - layout: eLayoutType.application, - }, -]; - -describe('DynamicLayoutComponent', () => { - const createComponent = createRoutingFactory({ - component: RouterOutletComponent, - stubsEnabled: false, - declarations: [DummyComponent, DynamicLayoutComponent], - mocks: [AbpApplicationConfigurationService, HttpClient], - providers: [ - { - provide: RoutesService, - useFactory: () => mockRoutesService(), - }, - ReplaceableComponentsService, - ], - imports: [RouterModule, DummyLayoutModule], - routes: [ - { path: '', component: RouterOutletComponent }, - { - path: 'parentWithLayout', - component: DynamicLayoutComponent, - children: [ - { - path: 'childWithoutLayout', - component: DummyComponent, - data: { name: 'childWithoutLayout' }, - }, - { - path: 'childWithLayout', - component: DummyComponent, - data: { name: 'childWithLayout' }, - }, - ], - }, - { - path: 'withData', - component: DynamicLayoutComponent, - children: [ - { - path: '', - component: DummyComponent, - data: { name: 'withData' }, - }, - ], - data: { layout: eLayoutType.empty }, - }, - { - path: 'withoutLayout', - component: DynamicLayoutComponent, - children: [ - { - path: '', - component: DummyComponent, - data: { name: 'withoutLayout' }, - }, - ], - data: { layout: null }, - }, - ], - }); - - let spectator: SpectatorRouting; - let replaceableComponents: ReplaceableComponentsService; - - beforeEach(async () => { - spectator = createComponent(); - replaceableComponents = spectator.inject(ReplaceableComponentsService); - const routesService = spectator.inject(RoutesService); - routesService.add(routes); - - replaceableComponents.add({ - key: 'Theme.ApplicationLayoutComponent', - component: DummyApplicationLayoutComponent, - }); - replaceableComponents.add({ - key: 'Theme.AccountLayoutComponent', - component: DummyAccountLayoutComponent, - }); - replaceableComponents.add({ - key: 'Theme.EmptyLayoutComponent', - component: DummyEmptyLayoutComponent, - }); - }); - - it('should handle application layout from parent abp route and display it', async () => { - spectator.router.navigateByUrl('/parentWithLayout/childWithoutLayout'); - await spectator.fixture.whenStable(); - spectator.detectComponentChanges(); - expect(spectator.query('abp-dynamic-layout')).toBeTruthy(); - expect(spectator.query('abp-layout-application')).toBeTruthy(); - }); - - it('should handle account layout from own property and display it', async () => { - spectator.router.navigateByUrl('/parentWithLayout/childWithLayout'); - await spectator.fixture.whenStable(); - spectator.detectComponentChanges(); - expect(spectator.query('abp-layout-account')).toBeTruthy(); - }); - - it('should handle empty layout from route data and display it', async () => { - spectator.router.navigateByUrl('/withData'); - await spectator.fixture.whenStable(); - spectator.detectComponentChanges(); - expect(spectator.query('abp-layout-empty')).toBeTruthy(); - }); - - it('should display empty layout when layout is null', async () => { - spectator.router.navigateByUrl('/withoutLayout'); - await spectator.fixture.whenStable(); - spectator.detectComponentChanges(); - expect(spectator.query('abp-layout-empty')).toBeTruthy(); - }); - - it('should not display any layout when layouts are empty', async () => { - const spy = jest.spyOn(replaceableComponents, 'get'); - spy.mockReturnValue(null); - spectator.detectChanges(); - - spectator.router.navigateByUrl('/withoutLayout'); - await spectator.fixture.whenStable(); - spectator.detectComponentChanges(); - - expect(spectator.query('abp-layout-empty')).toBeFalsy(); - }); -}); +import { HttpClient } from '@angular/common/http'; +import { Component, NgModule, inject as inject_1 } from '@angular/core'; +import { ActivatedRoute, RouterModule } from '@angular/router'; +import { createRoutingFactory, SpectatorRouting } from '@ngneat/spectator/jest'; +import { DynamicLayoutComponent, RouterOutletComponent } from '../components'; +import { eLayoutType } from '../enums/common'; +import { ABP } from '../models'; +import { AbpApplicationConfigurationService } from '../proxy/volo/abp/asp-net-core/mvc/application-configurations/abp-application-configuration.service'; +import { ReplaceableComponentsService, RoutesService } from '../services'; +import { mockRoutesService } from './routes.service.spec'; + +@Component({ + selector: 'abp-layout-application', + template: '', +}) +class DummyApplicationLayoutComponent {} + +@Component({ + selector: 'abp-layout-account', + template: '', +}) +class DummyAccountLayoutComponent {} + +@Component({ + selector: 'abp-layout-empty', + template: '', +}) +class DummyEmptyLayoutComponent {} + +const LAYOUTS = [ + DummyApplicationLayoutComponent, + DummyAccountLayoutComponent, + DummyEmptyLayoutComponent, +]; + +@NgModule({ + imports: [RouterModule], + declarations: [...LAYOUTS], +}) +class DummyLayoutModule {} + +@Component({ + selector: 'abp-dummy', + template: '{{route.snapshot.data?.name}} works!', +}) +class DummyComponent { route = inject_1(ActivatedRoute); + +} + +const routes: ABP.Route[] = [ + { + path: '', + name: 'Root', + }, + { + path: '/parentWithLayout', + name: 'ParentWithLayout', + parentName: 'Root', + layout: eLayoutType.application, + }, + { + path: '/parentWithLayout/childWithoutLayout', + name: 'ChildWithoutLayout', + parentName: 'ParentWithLayout', + }, + { + path: '/parentWithLayout/childWithLayout', + name: 'ChildWithLayout', + parentName: 'ParentWithLayout', + layout: eLayoutType.account, + }, + { + path: '/withData', + name: 'WithData', + layout: eLayoutType.application, + }, +]; + +describe('DynamicLayoutComponent', () => { + const createComponent = createRoutingFactory({ + component: RouterOutletComponent, + stubsEnabled: false, + declarations: [DummyComponent, DynamicLayoutComponent], + mocks: [AbpApplicationConfigurationService, HttpClient], + providers: [ + { + provide: RoutesService, + useFactory: () => mockRoutesService(), + }, + ReplaceableComponentsService, + ], + imports: [RouterModule, DummyLayoutModule], + routes: [ + { path: '', component: RouterOutletComponent }, + { + path: 'parentWithLayout', + component: DynamicLayoutComponent, + children: [ + { + path: 'childWithoutLayout', + component: DummyComponent, + data: { name: 'childWithoutLayout' }, + }, + { + path: 'childWithLayout', + component: DummyComponent, + data: { name: 'childWithLayout' }, + }, + ], + }, + { + path: 'withData', + component: DynamicLayoutComponent, + children: [ + { + path: '', + component: DummyComponent, + data: { name: 'withData' }, + }, + ], + data: { layout: eLayoutType.empty }, + }, + { + path: 'withoutLayout', + component: DynamicLayoutComponent, + children: [ + { + path: '', + component: DummyComponent, + data: { name: 'withoutLayout' }, + }, + ], + data: { layout: null }, + }, + ], + }); + + let spectator: SpectatorRouting; + let replaceableComponents: ReplaceableComponentsService; + + beforeEach(async () => { + spectator = createComponent(); + replaceableComponents = spectator.inject(ReplaceableComponentsService); + const routesService = spectator.inject(RoutesService); + routesService.add(routes); + + replaceableComponents.add({ + key: 'Theme.ApplicationLayoutComponent', + component: DummyApplicationLayoutComponent, + }); + replaceableComponents.add({ + key: 'Theme.AccountLayoutComponent', + component: DummyAccountLayoutComponent, + }); + replaceableComponents.add({ + key: 'Theme.EmptyLayoutComponent', + component: DummyEmptyLayoutComponent, + }); + }); + + it('should handle application layout from parent abp route and display it', async () => { + spectator.router.navigateByUrl('/parentWithLayout/childWithoutLayout'); + await spectator.fixture.whenStable(); + spectator.detectComponentChanges(); + expect(spectator.query('abp-dynamic-layout')).toBeTruthy(); + expect(spectator.query('abp-layout-application')).toBeTruthy(); + }); + + it('should handle account layout from own property and display it', async () => { + spectator.router.navigateByUrl('/parentWithLayout/childWithLayout'); + await spectator.fixture.whenStable(); + spectator.detectComponentChanges(); + expect(spectator.query('abp-layout-account')).toBeTruthy(); + }); + + it('should handle empty layout from route data and display it', async () => { + spectator.router.navigateByUrl('/withData'); + await spectator.fixture.whenStable(); + spectator.detectComponentChanges(); + expect(spectator.query('abp-layout-empty')).toBeTruthy(); + }); + + it('should display empty layout when layout is null', async () => { + spectator.router.navigateByUrl('/withoutLayout'); + await spectator.fixture.whenStable(); + spectator.detectComponentChanges(); + expect(spectator.query('abp-layout-empty')).toBeTruthy(); + }); + + it('should not display any layout when layouts are empty', async () => { + const spy = jest.spyOn(replaceableComponents, 'get'); + spy.mockReturnValue(null); + spectator.detectChanges(); + + spectator.router.navigateByUrl('/withoutLayout'); + await spectator.fixture.whenStable(); + spectator.detectComponentChanges(); + + expect(spectator.query('abp-layout-empty')).toBeFalsy(); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts b/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts index 10e59c86f6..94fdf8a527 100644 --- a/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts +++ b/npm/ng-packs/packages/core/src/lib/tests/replaceable-template.directive.spec.ts @@ -1,178 +1,174 @@ -import { Component, EventEmitter, Inject, Input, Optional, Output } from '@angular/core'; -import { Router } from '@angular/router'; -import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; -import { BehaviorSubject } from 'rxjs'; -import { ReplaceableTemplateDirective } from '../directives/replaceable-template.directive'; -import { ReplaceableComponents } from '../models/replaceable-components'; -import { ReplaceableComponentsService } from '../services/replaceable-components.service'; - -@Component({ - selector: 'abp-default-component', - template: '

default

', - exportAs: 'abpDefaultComponent', -}) -class DefaultComponent { - @Input() - oneWay; - - @Input() - twoWay: boolean; - - @Output() - readonly twoWayChange = new EventEmitter(); - - @Output() - readonly someOutput = new EventEmitter(); - - setTwoWay(value) { - this.twoWay = value; - this.twoWayChange.emit(value); - } -} - -@Component({ - selector: 'abp-external-component', - template: '

external

', -}) -class ExternalComponent { - constructor( - @Optional() - @Inject('REPLACEABLE_DATA') - public data: ReplaceableComponents.ReplaceableTemplateData, - ) {} -} - -describe('ReplaceableTemplateDirective', () => { - let spectator: SpectatorDirective; - const get$Res = new BehaviorSubject(undefined); - - const createDirective = createDirectiveFactory({ - directive: ReplaceableTemplateDirective, - declarations: [DefaultComponent, ExternalComponent], - entryComponents: [ExternalComponent], - mocks: [Router], - providers: [{ provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } }], - }); - - describe('without external component', () => { - const twoWayChange = jest.fn(a => a); - const someOutput = jest.fn(a => a); - - beforeEach(() => { - spectator = createDirective( - ` -
- -
- `, - { - hostProps: { - oneWay: { label: 'Test' }, - twoWay: false, - twoWayChange, - someOutput, - }, - }, - ); - - const component = spectator.query(DefaultComponent); - spectator.directive.context.initTemplate(component); - spectator.detectChanges(); - }); - - afterEach(() => twoWayChange.mockClear()); - - it('should display the default template when store response is undefined', () => { - expect(spectator.query('abp-default-component')).toBeTruthy(); - }); - - it('should be setted inputs and outputs', () => { - const component = spectator.query(DefaultComponent); - expect(component.oneWay).toEqual({ label: 'Test' }); - expect(component.twoWay).toEqual(false); - }); - - it('should change the component inputs', () => { - const component = spectator.query(DefaultComponent); - spectator.setHostInput({ oneWay: 'test' }); - component.setTwoWay(true); - component.someOutput.emit('someOutput emitted'); - expect(component.oneWay).toBe('test'); - expect(twoWayChange).toHaveBeenCalledWith(true); - expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); - }); - }); - - describe('with external component', () => { - const twoWayChange = jest.fn(a => a); - const someOutput = jest.fn(a => a); - - beforeEach(() => { - spectator = createDirective( - ` -
- -
- `, - { hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, - ); - - get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); - }); - - afterEach(() => twoWayChange.mockClear()); - - it('should display the external component', () => { - expect(spectator.query('p')).toHaveText('external'); - }); - - it('should be injected the data object', () => { - const externalComponent = spectator.query(ExternalComponent); - expect(externalComponent.data).toEqual({ - componentKey: 'TestModule.TestComponent', - inputs: { oneWay: { label: 'Test' }, twoWay: false }, - outputs: { someOutput, twoWayChange }, - }); - }); - - it('should be worked all data properties', () => { - const externalComponent = spectator.query(ExternalComponent); - spectator.setHostInput({ oneWay: 'test' }); - externalComponent.data.inputs.twoWay = true; - externalComponent.data.outputs.someOutput('someOutput emitted'); - expect(externalComponent.data.inputs.oneWay).toBe('test'); - expect(twoWayChange).toHaveBeenCalledWith(true); - expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); - - spectator.setHostInput({ twoWay: 'twoWay test' }); - expect(externalComponent.data.inputs.twoWay).toBe('twoWay test'); - }); - - it('should be worked correctly the default component when the external component has been removed from store', () => { - expect(spectator.query('p')).toHaveText('external'); - const externalComponent = spectator.query(ExternalComponent); - spectator.setHostInput({ oneWay: 'test' }); - externalComponent.data.inputs.twoWay = true; - get$Res.next({ component: null, key: 'TestModule.TestComponent' }); - spectator.detectChanges(); - const component = spectator.query(DefaultComponent); - spectator.directive.context.initTemplate(component); - expect(spectator.query('abp-default-component')).toBeTruthy(); - - expect(component.oneWay).toEqual('test'); - expect(component.twoWay).toEqual(true); - }); - - it('should reset default component subscriptions', () => { - get$Res.next({ component: null, key: 'TestModule.TestComponent' }); - const component = spectator.query(DefaultComponent); - spectator.directive.context.initTemplate(component); - spectator.detectChanges(); - const unsubscribe = jest.fn(() => {}); - spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe; - - get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); - expect(unsubscribe).toHaveBeenCalled(); - }); - }); -}); +import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/jest'; +import { BehaviorSubject } from 'rxjs'; +import { ReplaceableTemplateDirective } from '../directives/replaceable-template.directive'; +import { ReplaceableComponents } from '../models/replaceable-components'; +import { ReplaceableComponentsService } from '../services/replaceable-components.service'; + +@Component({ + selector: 'abp-default-component', + template: '

default

', + exportAs: 'abpDefaultComponent', +}) +class DefaultComponent { + @Input() + oneWay; + + @Input() + twoWay: boolean; + + @Output() + readonly twoWayChange = new EventEmitter(); + + @Output() + readonly someOutput = new EventEmitter(); + + setTwoWay(value) { + this.twoWay = value; + this.twoWayChange.emit(value); + } +} + +@Component({ + selector: 'abp-external-component', + template: '

external

', +}) +class ExternalComponent { data = inject>('REPLACEABLE_DATA' as any, { optional: true })!; + +} + +describe('ReplaceableTemplateDirective', () => { + let spectator: SpectatorDirective; + const get$Res = new BehaviorSubject(undefined); + + const createDirective = createDirectiveFactory({ + directive: ReplaceableTemplateDirective, + declarations: [DefaultComponent, ExternalComponent], + entryComponents: [ExternalComponent], + mocks: [Router], + providers: [{ provide: ReplaceableComponentsService, useValue: { get$: () => get$Res } }], + }); + + describe('without external component', () => { + const twoWayChange = jest.fn(a => a); + const someOutput = jest.fn(a => a); + + beforeEach(() => { + spectator = createDirective( + ` +
+ +
+ `, + { + hostProps: { + oneWay: { label: 'Test' }, + twoWay: false, + twoWayChange, + someOutput, + }, + }, + ); + + const component = spectator.query(DefaultComponent); + spectator.directive.context.initTemplate(component); + spectator.detectChanges(); + }); + + afterEach(() => twoWayChange.mockClear()); + + it('should display the default template when store response is undefined', () => { + expect(spectator.query('abp-default-component')).toBeTruthy(); + }); + + it('should be setted inputs and outputs', () => { + const component = spectator.query(DefaultComponent); + expect(component.oneWay).toEqual({ label: 'Test' }); + expect(component.twoWay).toEqual(false); + }); + + it('should change the component inputs', () => { + const component = spectator.query(DefaultComponent); + spectator.setHostInput({ oneWay: 'test' }); + component.setTwoWay(true); + component.someOutput.emit('someOutput emitted'); + expect(component.oneWay).toBe('test'); + expect(twoWayChange).toHaveBeenCalledWith(true); + expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); + }); + }); + + describe('with external component', () => { + const twoWayChange = jest.fn(a => a); + const someOutput = jest.fn(a => a); + + beforeEach(() => { + spectator = createDirective( + ` +
+ +
+ `, + { hostProps: { oneWay: { label: 'Test' }, twoWay: false, twoWayChange, someOutput } }, + ); + + get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); + }); + + afterEach(() => twoWayChange.mockClear()); + + it('should display the external component', () => { + expect(spectator.query('p')).toHaveText('external'); + }); + + it('should be injected the data object', () => { + const externalComponent = spectator.query(ExternalComponent); + expect(externalComponent.data).toEqual({ + componentKey: 'TestModule.TestComponent', + inputs: { oneWay: { label: 'Test' }, twoWay: false }, + outputs: { someOutput, twoWayChange }, + }); + }); + + it('should be worked all data properties', () => { + const externalComponent = spectator.query(ExternalComponent); + spectator.setHostInput({ oneWay: 'test' }); + externalComponent.data.inputs.twoWay = true; + externalComponent.data.outputs.someOutput('someOutput emitted'); + expect(externalComponent.data.inputs.oneWay).toBe('test'); + expect(twoWayChange).toHaveBeenCalledWith(true); + expect(someOutput).toHaveBeenCalledWith('someOutput emitted'); + + spectator.setHostInput({ twoWay: 'twoWay test' }); + expect(externalComponent.data.inputs.twoWay).toBe('twoWay test'); + }); + + it('should be worked correctly the default component when the external component has been removed from store', () => { + expect(spectator.query('p')).toHaveText('external'); + const externalComponent = spectator.query(ExternalComponent); + spectator.setHostInput({ oneWay: 'test' }); + externalComponent.data.inputs.twoWay = true; + get$Res.next({ component: null, key: 'TestModule.TestComponent' }); + spectator.detectChanges(); + const component = spectator.query(DefaultComponent); + spectator.directive.context.initTemplate(component); + expect(spectator.query('abp-default-component')).toBeTruthy(); + + expect(component.oneWay).toEqual('test'); + expect(component.twoWay).toEqual(true); + }); + + it('should reset default component subscriptions', () => { + get$Res.next({ component: null, key: 'TestModule.TestComponent' }); + const component = spectator.query(DefaultComponent); + spectator.directive.context.initTemplate(component); + spectator.detectChanges(); + const unsubscribe = jest.fn(() => {}); + spectator.directive.defaultComponentSubscriptions.twoWayChange.unsubscribe = unsubscribe; + + get$Res.next({ component: ExternalComponent, key: 'TestModule.TestComponent' }); + expect(unsubscribe).toHaveBeenCalled(); + }); + }); +}); diff --git a/npm/ng-packs/packages/core/src/lib/validators/unique-character.validator.ts b/npm/ng-packs/packages/core/src/lib/validators/unique-character.validator.ts index 509dc66c7f..150a6f1632 100644 --- a/npm/ng-packs/packages/core/src/lib/validators/unique-character.validator.ts +++ b/npm/ng-packs/packages/core/src/lib/validators/unique-character.validator.ts @@ -1,4 +1,4 @@ -import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { AbstractControl, ValidatorFn } from '@angular/forms'; import { isNullOrEmpty } from '../utils'; export interface UniqueCharacterError { diff --git a/npm/ng-packs/packages/core/testing/src/lib/services/mock-permission.service.ts b/npm/ng-packs/packages/core/testing/src/lib/services/mock-permission.service.ts index b4d180b17a..e7f03cdf48 100644 --- a/npm/ng-packs/packages/core/testing/src/lib/services/mock-permission.service.ts +++ b/npm/ng-packs/packages/core/testing/src/lib/services/mock-permission.service.ts @@ -1,12 +1,17 @@ import { ConfigStateService, PermissionService } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MockPermissionService extends PermissionService { - constructor(protected configState: ConfigStateService) { - super(configState); + protected configState: ConfigStateService; + + constructor() { + const configState = inject(ConfigStateService); + super(); + this.configState = configState; + this.grantAllPolicies(); } diff --git a/npm/ng-packs/packages/core/testing/src/lib/services/mock-rest.service.ts b/npm/ng-packs/packages/core/testing/src/lib/services/mock-rest.service.ts index 7fd171c1d4..57d326ac4f 100644 --- a/npm/ng-packs/packages/core/testing/src/lib/services/mock-rest.service.ts +++ b/npm/ng-packs/packages/core/testing/src/lib/services/mock-rest.service.ts @@ -3,24 +3,33 @@ import { CORE_OPTIONS, EnvironmentService, ExternalHttpClient, - HttpErrorReporterService, RestService, } from '@abp/ng.core'; import { HttpClient } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { Observable, throwError } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class MockRestService extends RestService { - constructor( - @Inject(CORE_OPTIONS) protected options: ABP.Root, - protected http: HttpClient, - protected externalhttp: ExternalHttpClient, - protected environment: EnvironmentService, - ) { - super(options, http,externalhttp, environment, null as unknown as HttpErrorReporterService); + protected options: ABP.Root; + protected http: HttpClient; + protected externalhttp: ExternalHttpClient; + protected environment: EnvironmentService; + + constructor() { + const options = inject(CORE_OPTIONS); + const http = inject(HttpClient); + const externalhttp = inject(ExternalHttpClient); + const environment = inject(EnvironmentService); + + super(); + + this.options = options; + this.http = http; + this.externalhttp = externalhttp; + this.environment = environment; } handleError(err: any): Observable { diff --git a/npm/ng-packs/packages/core/tsconfig.lib.json b/npm/ng-packs/packages/core/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/core/tsconfig.lib.json +++ b/npm/ng-packs/packages/core/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/feature-management/proxy/src/lib/proxy/feature-management/features.service.ts b/npm/ng-packs/packages/feature-management/proxy/src/lib/proxy/feature-management/features.service.ts index 189076428d..67616fd4d1 100644 --- a/npm/ng-packs/packages/feature-management/proxy/src/lib/proxy/feature-management/features.service.ts +++ b/npm/ng-packs/packages/feature-management/proxy/src/lib/proxy/feature-management/features.service.ts @@ -1,11 +1,13 @@ import type { GetFeatureListResultDto, UpdateFeaturesDto } from './models'; import { RestService } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class FeaturesService { + private restService = inject(RestService); + apiName = 'AbpFeatureManagement'; delete = (providerName: string, providerKey: string) => @@ -38,6 +40,4 @@ export class FeaturesService { }, { apiName: this.apiName }, ); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts index bfadbf65fb..0355ef2bae 100644 --- a/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts +++ b/npm/ng-packs/packages/feature-management/src/lib/components/feature-management/feature-management.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, Output, inject } from '@angular/core'; -import { CommonModule, NgTemplateOutlet } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { ConfigStateService, LocalizationPipe, TrackByService } from '@abp/ng.core'; import { @@ -35,14 +35,13 @@ const DEFAULT_PROVIDER_NAME = 'D'; templateUrl: './feature-management.component.html', exportAs: 'abpFeatureManagement', imports: [ - CommonModule, + NgTemplateOutlet, ButtonComponent, ModalComponent, LocalizationPipe, FormsModule, NgbNavModule, FreeTextInputDirective, - NgTemplateOutlet, ModalCloseDirective, ], }) diff --git a/npm/ng-packs/packages/feature-management/tsconfig.lib.json b/npm/ng-packs/packages/feature-management/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/feature-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/feature-management/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-role.service.ts b/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-role.service.ts index 6e296c171b..c1aabf7091 100644 --- a/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-role.service.ts +++ b/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-role.service.ts @@ -1,12 +1,14 @@ import type { GetIdentityRolesInput, IdentityRoleCreateDto, IdentityRoleDto, IdentityRoleUpdateDto } from './models'; import { RestService } from '@abp/ng.core'; import type { ListResultDto, PagedResultDto } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class IdentityRoleService { + private restService = inject(RestService); + apiName = 'AbpIdentity'; create = (input: IdentityRoleCreateDto) => @@ -53,6 +55,4 @@ export class IdentityRoleService { body: input, }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-user-lookup.service.ts b/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-user-lookup.service.ts index af4583aa28..cf8d01e81f 100644 --- a/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-user-lookup.service.ts +++ b/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-user-lookup.service.ts @@ -1,13 +1,15 @@ import type { UserLookupCountInputDto, UserLookupSearchInputDto } from './models'; import { RestService } from '@abp/ng.core'; import type { ListResultDto } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import type { UserData } from '../users/models'; @Injectable({ providedIn: 'root', }) export class IdentityUserLookupService { + private restService = inject(RestService); + apiName = 'AbpIdentity'; findById = (id: string) => @@ -39,6 +41,4 @@ export class IdentityUserLookupService { params: { filter: input.filter, sorting: input.sorting, skipCount: input.skipCount, maxResultCount: input.maxResultCount }, }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-user.service.ts b/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-user.service.ts index c3b6f203f2..a538a07feb 100644 --- a/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-user.service.ts +++ b/npm/ng-packs/packages/identity/proxy/src/lib/proxy/identity/identity-user.service.ts @@ -1,12 +1,14 @@ import type { GetIdentityUsersInput, IdentityRoleDto, IdentityUserCreateDto, IdentityUserDto, IdentityUserUpdateDto, IdentityUserUpdateRolesDto } from './models'; import { RestService } from '@abp/ng.core'; import type { ListResultDto, PagedResultDto } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class IdentityUserService { + private restService = inject(RestService); + apiName = 'AbpIdentity'; create = (input: IdentityUserCreateDto) => @@ -84,6 +86,4 @@ export class IdentityUserService { body: input, }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/identity/tsconfig.lib.json b/npm/ng-packs/packages/identity/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/identity/tsconfig.lib.json +++ b/npm/ng-packs/packages/identity/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts b/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts index 6f751804cd..1311a8d7a8 100644 --- a/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts +++ b/npm/ng-packs/packages/oauth/src/lib/guards/oauth.guard.ts @@ -6,10 +6,10 @@ import { CanActivateFn, } from '@angular/router'; -import { Observable } from 'rxjs'; +import { Observable, timer, filter, take, map, firstValueFrom, timeout, catchError, of } from 'rxjs'; import { OAuthService } from 'angular-oauth2-oidc'; -import { AuthService, IAbpGuard } from '@abp/ng.core'; +import { AuthService, IAbpGuard, EnvironmentService } from '@abp/ng.core'; /** * @deprecated Use `abpOAuthGuard` *function* instead. @@ -53,3 +53,36 @@ export const abpOAuthGuard: CanActivateFn = ( authService.navigateToLogin(params); return false; }; + +export const asyncAbpOAuthGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => { + const oAuthService = inject(OAuthService); + const authService = inject(AuthService); + const environmentService = inject(EnvironmentService); + + const { oAuthConfig } = environmentService.getEnvironment(); + + if (oAuthConfig?.responseType === 'code') { + return firstValueFrom( + timer(0, 100).pipe( + map(() => oAuthService.hasValidAccessToken()), + filter(Boolean), + take(1), + timeout(3000), + catchError(() => { + authService.navigateToLogin({ returnUrl: state.url }); + return of(false); + }) + ) + ); + } + + if (oAuthService.hasValidAccessToken()) { + return true; + } + + authService.navigateToLogin({ returnUrl: state.url }); + return false; +}; diff --git a/npm/ng-packs/packages/oauth/src/lib/handlers/oauth-configuration.handler.ts b/npm/ng-packs/packages/oauth/src/lib/handlers/oauth-configuration.handler.ts index a2a9228b78..42ddb89c90 100644 --- a/npm/ng-packs/packages/oauth/src/lib/handlers/oauth-configuration.handler.ts +++ b/npm/ng-packs/packages/oauth/src/lib/handlers/oauth-configuration.handler.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { AuthConfig, OAuthService } from "angular-oauth2-oidc"; import compare from 'just-compare'; import { filter, map } from 'rxjs/operators'; @@ -8,11 +8,11 @@ import { ABP, EnvironmentService, CORE_OPTIONS } from '@abp/ng.core'; providedIn: 'root', }) export class OAuthConfigurationHandler { - constructor( - private oAuthService: OAuthService, - private environmentService: EnvironmentService, - @Inject(CORE_OPTIONS) private options: ABP.Root, - ) { + private oAuthService = inject(OAuthService); + private environmentService = inject(EnvironmentService); + private options = inject(CORE_OPTIONS); + + constructor() { this.listenToSetEnvironment(); } diff --git a/npm/ng-packs/packages/oauth/src/lib/interceptors/api.interceptor.ts b/npm/ng-packs/packages/oauth/src/lib/interceptors/api.interceptor.ts index 07fad0428a..c2039210f4 100644 --- a/npm/ng-packs/packages/oauth/src/lib/interceptors/api.interceptor.ts +++ b/npm/ng-packs/packages/oauth/src/lib/interceptors/api.interceptor.ts @@ -1,5 +1,5 @@ import { HttpEvent, HttpHandler, HttpHeaders, HttpRequest } from '@angular/common/http'; -import { Inject, Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; import { OAuthService } from 'angular-oauth2-oidc'; import { finalize } from 'rxjs/operators'; import { Observable } from 'rxjs'; @@ -15,12 +15,10 @@ import { providedIn: 'root', }) export class OAuthApiInterceptor implements IApiInterceptor { - constructor( - private oAuthService: OAuthService, - private sessionState: SessionStateService, - private httpWaitService: HttpWaitService, - @Inject(TENANT_KEY) private tenantKey: string, - ) {} + private oAuthService = inject(OAuthService); + private sessionState = inject(SessionStateService); + private httpWaitService = inject(HttpWaitService); + private tenantKey = inject(TENANT_KEY); intercept(request: HttpRequest, next: HttpHandler): Observable> { this.httpWaitService.addRequest(request); diff --git a/npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts b/npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts index 92ecc86012..534882dd13 100644 --- a/npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts +++ b/npm/ng-packs/packages/oauth/src/lib/providers/oauth-module-config.provider.ts @@ -2,6 +2,7 @@ import { AuthService, AuthGuard, authGuard, + asyncAuthGuard, ApiInterceptor, PIPE_TO_LOGIN_FN_KEY, CHECK_AUTHENTICATION_STATE_FN_KEY, @@ -11,7 +12,7 @@ import { import { Provider, makeEnvironmentProviders, inject, provideAppInitializer } from '@angular/core'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; -import { AbpOAuthGuard, abpOAuthGuard } from '../guards'; +import { AbpOAuthGuard, abpOAuthGuard, asyncAbpOAuthGuard, } from '../guards'; import { OAuthConfigurationHandler } from '../handlers'; import { OAuthApiInterceptor } from '../interceptors'; import { AbpOAuthService, OAuthErrorFilterService } from '../services'; @@ -31,6 +32,10 @@ export function provideAbpOAuth() { { provide: authGuard, useValue: abpOAuthGuard, + }, + { + provide: asyncAuthGuard, + useValue: asyncAbpOAuthGuard, }, { provide: ApiInterceptor, diff --git a/npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts b/npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts index a490f92dd2..51c97a8101 100644 --- a/npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts +++ b/npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Injector } from '@angular/core'; +import { Injectable, Injector, inject } from '@angular/core'; import { Params } from '@angular/router'; import { from, Observable, lastValueFrom, EMPTY } from 'rxjs'; import { filter, map, switchMap, take, tap } from 'rxjs/operators'; @@ -13,6 +13,8 @@ import { HttpHeaders } from '@angular/common/http'; providedIn: 'root', }) export class AbpOAuthService implements IAuthService { + protected injector = inject(Injector); + private strategy!: AuthFlowStrategy; private readonly oAuthService: OAuthService; @@ -28,7 +30,7 @@ export class AbpOAuthService implements IAuthService { return this.strategy.isInternalAuth; } - constructor(protected injector: Injector) { + constructor() { this.oAuthService = this.injector.get(OAuthService); } diff --git a/npm/ng-packs/packages/oauth/tsconfig.lib.json b/npm/ng-packs/packages/oauth/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/oauth/tsconfig.lib.json +++ b/npm/ng-packs/packages/oauth/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts b/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts index 45ac0b40e1..dbd13ca778 100644 --- a/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts +++ b/npm/ng-packs/packages/permission-management/proxy/src/lib/proxy/permissions.service.ts @@ -1,11 +1,13 @@ import type { GetPermissionListResultDto, UpdatePermissionsDto } from './models'; import { RestService } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class PermissionsService { + private restService = inject(RestService); + apiName = 'AbpPermissionManagement'; get = (providerName: string, providerKey: string) => @@ -24,6 +26,4 @@ export class PermissionsService { body: input, }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts b/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts index 671e78893f..dd0492a411 100644 --- a/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts +++ b/npm/ng-packs/packages/permission-management/src/lib/components/permission-management.component.ts @@ -30,7 +30,7 @@ import { import { concat, of } from 'rxjs'; import { finalize, switchMap, take, tap } from 'rxjs/operators'; import { PermissionManagement } from '../models'; -import { CommonModule } from '@angular/common'; +import { NgStyle } from '@angular/common'; import { FormsModule } from '@angular/forms'; type PermissionWithStyle = PermissionGrantInfoDto & { @@ -94,7 +94,7 @@ type PermissionWithGroupName = PermissionGrantInfoDto & { ], imports: [ FormsModule, - CommonModule, + NgStyle, ModalComponent, LocalizationPipe, ButtonComponent, diff --git a/npm/ng-packs/packages/permission-management/tsconfig.lib.json b/npm/ng-packs/packages/permission-management/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/permission-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/permission-management/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template b/npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template index aae3b43471..c445286162 100644 --- a/npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template +++ b/npm/ng-packs/packages/schematics/src/commands/api/files-service/proxy/__namespace@dir__/__name@kebab__.service.ts.template @@ -5,6 +5,7 @@ providedIn: 'root', }) export class <%= name %>Service { + private restService = inject(RestService); apiName = '<%= apiName %>';<% for (let {body, signature} of methods) { %> <% @@ -20,12 +21,14 @@ export class <%= name %>Service { if (isBlob) { %> responseType: 'blob',<% } %> url: <%= body.url %>,<% - if (body.params.length) { %> + if (body.dictParamVar && !body.params.length) { %> + params: <%= body.dictParamVar %>,<% } %><% + if (body.dictParamVar && body.params.length) { %> + params: { ...<%= body.dictParamVar %>, <%= body.params.join(', ') %> },<% } %><% + if (!body.dictParamVar && body.params.length) { %> params: { <%= body.params.join(', ') %> },<% } if (body.body) { %> body: <%= body.body %>,<% } %> }, { apiName: this.apiName,...config });<% } %> - - constructor(private restService: RestService) {} -} +} \ No newline at end of file diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template index 5b574d313d..7b5ac72780 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template @@ -10,7 +10,7 @@ "types": [], "lib": [ "dom", - "es2018" + "es2020" ] }, "exclude": [ diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template index 5b574d313d..7b5ac72780 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package/__libraryName@kebab__/tsconfig.lib.json.template @@ -10,7 +10,7 @@ "types": [], "lib": [ "dom", - "es2018" + "es2020" ] }, "exclude": [ diff --git a/npm/ng-packs/packages/schematics/src/models/method.ts b/npm/ng-packs/packages/schematics/src/models/method.ts index 043df6d458..404fa9d233 100644 --- a/npm/ng-packs/packages/schematics/src/models/method.ts +++ b/npm/ng-packs/packages/schematics/src/models/method.ts @@ -1,11 +1,12 @@ import { eBindingSourceId, eMethodModifier } from '../enums'; import { camel, camelizeHyphen } from '../utils/text'; -import { getParamName, getParamValueName } from '../utils/methods'; +import { getParamName, getParamValueName, isDictionaryType } from '../utils/methods'; import { ParameterInBody } from './api-definition'; import { Property } from './model'; import { Omissible } from './util'; import { VOLO_REMOTE_STREAM_CONTENT } from '../constants'; // eslint-disable-next-line @typescript-eslint/no-var-requires + const shouldQuote = require('should-quote'); export class Method { @@ -40,6 +41,7 @@ export class Body { body?: string; method: string; params: string[] = []; + dictParamVar?: string; responseTypeWithNamespace: string; requestType = 'any'; responseType: string; @@ -57,6 +59,10 @@ export class Body { switch (bindingSourceId) { case eBindingSourceId.Model: case eBindingSourceId.Query: + if (isDictionaryType(param.type, param.typeSimple)) { + this.dictParamVar = value; + break; + } this.params.push(paramName === value ? value : `${getParamName(paramName)}: ${value}`); break; case eBindingSourceId.FormFile: diff --git a/npm/ng-packs/packages/schematics/src/utils/methods.ts b/npm/ng-packs/packages/schematics/src/utils/methods.ts index 6767adf540..d3fc935819 100644 --- a/npm/ng-packs/packages/schematics/src/utils/methods.ts +++ b/npm/ng-packs/packages/schematics/src/utils/methods.ts @@ -1,11 +1,9 @@ import { camel } from './text'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const shouldQuote = require('should-quote'); export const getParamName = (paramName: string) => shouldQuote(paramName) ? `["${paramName}"]` : paramName; -// check dot exists in param name and camelize access continuously export const getParamValueName = (paramName: string, descriptorName: string) => { if (paramName.includes('.')) { const splitted = paramName.split('.'); @@ -17,3 +15,8 @@ export const getParamValueName = (paramName: string, descriptorName: string) => } return `${descriptorName}.${paramName}`; }; + +export function isDictionaryType(type?: string, typeSimple?: string): boolean { + const haystacks = [type || '', typeSimple || '']; + return haystacks.some(t => /(^|\b)(System\.Collections\.Generic\.)?(I)?Dictionary\s* @@ -33,6 +35,4 @@ export class EmailSettingsService { body: input, }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/setting-management/proxy/src/lib/proxy/email-settings.service.ts b/npm/ng-packs/packages/setting-management/proxy/src/lib/proxy/email-settings.service.ts index a05cfc27a7..af6a2f5843 100644 --- a/npm/ng-packs/packages/setting-management/proxy/src/lib/proxy/email-settings.service.ts +++ b/npm/ng-packs/packages/setting-management/proxy/src/lib/proxy/email-settings.service.ts @@ -1,11 +1,13 @@ import type { EmailSettingsDto, SendTestEmailInput, UpdateEmailSettingsDto } from './models'; import { RestService, Rest } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) -export class EmailSettingsService { +export class EmailSettingsService { + private restService = inject(RestService); + apiName = 'SettingManagement'; @@ -33,6 +35,4 @@ export class EmailSettingsService { body: input, }, { apiName: this.apiName,...config }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/setting-management/proxy/src/lib/proxy/time-zone-settings.service.ts b/npm/ng-packs/packages/setting-management/proxy/src/lib/proxy/time-zone-settings.service.ts index 7141bce27d..1a20916673 100644 --- a/npm/ng-packs/packages/setting-management/proxy/src/lib/proxy/time-zone-settings.service.ts +++ b/npm/ng-packs/packages/setting-management/proxy/src/lib/proxy/time-zone-settings.service.ts @@ -1,11 +1,13 @@ import type { NameValue } from './volo/abp/models'; import { RestService, Rest } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) -export class TimeZoneSettingsService { +export class TimeZoneSettingsService { + private restService = inject(RestService); + apiName = 'SettingManagement'; @@ -33,6 +35,4 @@ export class TimeZoneSettingsService { params: { timezone }, }, { apiName: this.apiName,...config }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts index 7b26a647ce..1cec644ed6 100644 --- a/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts +++ b/npm/ng-packs/packages/setting-management/src/lib/components/setting-management.component.ts @@ -2,13 +2,13 @@ import { ABP, ForDirective, LocalizationPipe, PermissionDirective } from '@abp/n import { SettingTabsService } from '@abp/ng.setting-management/config'; import { Component, inject, OnDestroy, OnInit, TrackByFunction } from '@angular/core'; import { Subscription } from 'rxjs'; -import { CommonModule } from '@angular/common'; +import { NgComponentOutlet } from '@angular/common'; import { PageComponent } from '@abp/ng.components/page'; @Component({ selector: 'abp-setting-management', templateUrl: './setting-management.component.html', - imports: [CommonModule, PageComponent, LocalizationPipe, PermissionDirective, ForDirective], + imports: [NgComponentOutlet, PageComponent, LocalizationPipe, PermissionDirective, ForDirective], }) export class SettingManagementComponent implements OnDestroy, OnInit { private settingTabsService = inject(SettingTabsService); diff --git a/npm/ng-packs/packages/setting-management/tsconfig.lib.json b/npm/ng-packs/packages/setting-management/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/setting-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/setting-management/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/tenant.service.ts b/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/tenant.service.ts index 61d1667be4..28174f8ad8 100644 --- a/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/tenant.service.ts +++ b/npm/ng-packs/packages/tenant-management/proxy/src/lib/proxy/tenant.service.ts @@ -1,12 +1,14 @@ import type { GetTenantsInput, TenantCreateDto, TenantDto, TenantUpdateDto } from './models'; import { RestService } from '@abp/ng.core'; import type { PagedResultDto } from '@abp/ng.core'; -import { Injectable } from '@angular/core'; +import { Injectable, inject } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class TenantService { + private restService = inject(RestService); + apiName = 'AbpTenantManagement'; create = (input: TenantCreateDto) => @@ -69,6 +71,4 @@ export class TenantService { params: { defaultConnectionString }, }, { apiName: this.apiName }); - - constructor(private restService: RestService) {} } diff --git a/npm/ng-packs/packages/tenant-management/tsconfig.lib.json b/npm/ng-packs/packages/tenant-management/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/tenant-management/tsconfig.lib.json +++ b/npm/ng-packs/packages/tenant-management/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts index a6e1b6ad91..3600c615c9 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/account-layout.component.ts @@ -1,13 +1,13 @@ -import { AfterViewInit, Component } from '@angular/core'; +import { AfterViewInit, Component, inject } from '@angular/core'; import { eLayoutType, ReplaceableTemplateDirective, SubscriptionService } from '@abp/ng.core'; import { LayoutService } from '../../services/layout.service'; -import { CommonModule } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { LogoComponent } from '../logo/logo.component'; import { RoutesComponent } from '../routes/routes.component'; import { NavItemsComponent } from '../nav-items/nav-items.component'; import { AuthWrapperComponent } from './auth-wrapper/auth-wrapper.component'; import { PageAlertContainerComponent } from '../page-alert-container/page-alert-container.component'; -import { RouterModule } from '@angular/router'; +import { RouterOutlet } from '@angular/router'; import { collapseWithMargin } from '@abp/ng.theme.shared'; @Component({ @@ -16,24 +16,24 @@ import { collapseWithMargin } from '@abp/ng.theme.shared'; animations: [collapseWithMargin], providers: [LayoutService, SubscriptionService], imports: [ - CommonModule, + NgTemplateOutlet, LogoComponent, RoutesComponent, NavItemsComponent, AuthWrapperComponent, PageAlertContainerComponent, ReplaceableTemplateDirective, - RouterModule, + RouterOutlet, ], }) export class AccountLayoutComponent implements AfterViewInit { + service = inject(LayoutService); + // required for dynamic component static type = eLayoutType.account; authWrapperKey = 'Account.AuthWrapperComponent'; - constructor(public service: LayoutService) {} - ngAfterViewInit() { this.service.subscribeWindowSize(); } diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/auth-wrapper/auth-wrapper.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/auth-wrapper/auth-wrapper.component.ts index 5094551ed2..e87c55dd04 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/auth-wrapper/auth-wrapper.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/auth-wrapper/auth-wrapper.component.ts @@ -1,6 +1,6 @@ import { AuthWrapperService } from '@abp/ng.account.core'; -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; import { LocalizationPipe, ReplaceableTemplateDirective } from '@abp/ng.core'; import { TenantBoxComponent } from '../tenant-box/tenant-box.component'; @@ -8,8 +8,8 @@ import { TenantBoxComponent } from '../tenant-box/tenant-box.component'; selector: 'abp-auth-wrapper', templateUrl: './auth-wrapper.component.html', providers: [AuthWrapperService], - imports: [CommonModule, TenantBoxComponent, ReplaceableTemplateDirective, LocalizationPipe], + imports: [AsyncPipe, TenantBoxComponent, ReplaceableTemplateDirective, LocalizationPipe], }) export class AuthWrapperComponent { - constructor(public service: AuthWrapperService) {} -} + service = inject(AuthWrapperService); +} \ No newline at end of file diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/tenant-box/tenant-box.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/tenant-box/tenant-box.component.ts index 4aadd1effb..c9897fed59 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/tenant-box/tenant-box.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/account-layout/tenant-box/tenant-box.component.ts @@ -1,6 +1,6 @@ import { TenantBoxService } from '@abp/ng.account.core'; -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, inject } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; import { LocalizationPipe } from '@abp/ng.core'; import { ButtonComponent, ModalCloseDirective, ModalComponent } from '@abp/ng.theme.shared'; import { FormsModule } from '@angular/forms'; @@ -10,7 +10,7 @@ import { FormsModule } from '@angular/forms'; templateUrl: './tenant-box.component.html', providers: [TenantBoxService], imports: [ - CommonModule, + AsyncPipe, FormsModule, ModalComponent, LocalizationPipe, @@ -19,5 +19,5 @@ import { FormsModule } from '@angular/forms'; ], }) export class TenantBoxComponent { - constructor(public service: TenantBoxService) {} + service = inject(TenantBoxService); } diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.ts index 2cfeb978b2..0cebc476a5 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/application-layout/application-layout.component.ts @@ -2,8 +2,8 @@ import { eLayoutType, ReplaceableTemplateDirective, SubscriptionService } from ' import { collapseWithMargin, slideFromBottom } from '@abp/ng.theme.shared'; import { AfterViewInit, Component, inject } from '@angular/core'; import { LayoutService } from '../../services/layout.service'; -import { CommonModule } from '@angular/common'; -import { RouterModule } from '@angular/router'; +import { NgTemplateOutlet } from '@angular/common'; +import { RouterOutlet } from '@angular/router'; import { LogoComponent } from '../logo/logo.component'; import { PageAlertContainerComponent } from '../page-alert-container/page-alert-container.component'; import { RoutesComponent } from '../routes/routes.component'; @@ -15,13 +15,13 @@ import { NavItemsComponent } from '../nav-items/nav-items.component'; animations: [slideFromBottom, collapseWithMargin], providers: [LayoutService, SubscriptionService], imports: [ - CommonModule, + NgTemplateOutlet, LogoComponent, PageAlertContainerComponent, RoutesComponent, NavItemsComponent, ReplaceableTemplateDirective, - RouterModule, + RouterOutlet, ], }) export class ApplicationLayoutComponent implements AfterViewInit { diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts index 16cd0951e5..2153ecf2c5 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/empty-layout/empty-layout.component.ts @@ -1,11 +1,11 @@ import { Component } from '@angular/core'; import { eLayoutType } from '@abp/ng.core'; -import { RouterModule } from '@angular/router'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'abp-layout-empty', template: ` `, - imports: [RouterModule], + imports: [RouterOutlet], }) export class EmptyLayoutComponent { static type = eLayoutType.empty; diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/logo/logo.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/logo/logo.component.ts index fcaeacdf1a..0bc2266e0c 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/logo/logo.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/logo/logo.component.ts @@ -1,6 +1,6 @@ import { ApplicationInfo, EnvironmentService } from '@abp/ng.core'; -import { Component } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { RouterLink } from '@angular/router'; +import { Component, inject } from '@angular/core'; @Component({ selector: 'abp-logo', @@ -13,12 +13,12 @@ import { CommonModule } from '@angular/common'; } `, - imports: [CommonModule], + imports: [RouterLink], }) export class LogoComponent { + private environment = inject(EnvironmentService); + get appInfo(): ApplicationInfo { return this.environment.getEnvironment().application; } - - constructor(private environment: EnvironmentService) {} } diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts index 40f2d4a342..cd6f087600 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/current-user.component.ts @@ -9,16 +9,17 @@ import { ToInjectorPipe, } from '@abp/ng.core'; import { AbpVisibleDirective, UserMenu, UserMenuService } from '@abp/ng.theme.shared'; -import { Component, Inject, TrackByFunction } from '@angular/core'; +import { Component, TrackByFunction, inject } from '@angular/core'; import { Observable } from 'rxjs'; -import { CommonModule } from '@angular/common'; +import { NgComponentOutlet, AsyncPipe } from '@angular/common'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'abp-current-user', templateUrl: './current-user.component.html', imports: [ - CommonModule, + NgComponentOutlet, + AsyncPipe, NgbDropdownModule, AbpVisibleDirective, PermissionDirective, @@ -27,6 +28,12 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; ], }) export class CurrentUserComponent { + readonly navigateToManageProfile = inject(NAVIGATE_TO_MANAGE_PROFILE); + readonly userMenu = inject(UserMenuService); + private authService = inject(AuthService); + private configState = inject(ConfigStateService); + private sessionState = inject(SessionStateService); + currentUser$: Observable = this.configState.getOne$('currentUser'); selectedTenant$ = this.sessionState.getTenant$(); @@ -36,14 +43,6 @@ export class CurrentUserComponent { return window.innerWidth < 992; } - constructor( - @Inject(NAVIGATE_TO_MANAGE_PROFILE) public readonly navigateToManageProfile: () => void, - public readonly userMenu: UserMenuService, - private authService: AuthService, - private configState: ConfigStateService, - private sessionState: SessionStateService, - ) {} - navigateToLogin() { this.authService.navigateToLogin(); } diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts index 67d09d300b..eaf645c4a0 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/languages.component.ts @@ -1,8 +1,8 @@ import { ConfigStateService, LanguageInfo, SessionStateService } from '@abp/ng.core'; -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { CommonModule } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; @Component({ @@ -39,9 +39,12 @@ import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; } `, - imports: [CommonModule, NgbDropdownModule], + imports: [AsyncPipe, NgbDropdownModule], }) export class LanguagesComponent { + private sessionState = inject(SessionStateService); + private configState = inject(ConfigStateService); + get smallScreen(): boolean { return window.innerWidth < 992; } @@ -69,11 +72,6 @@ export class LanguagesComponent { return this.sessionState.getLanguage(); } - constructor( - private sessionState: SessionStateService, - private configState: ConfigStateService, - ) {} - onChangeLang(cultureName: string) { this.sessionState.setLanguage(cultureName); } diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts index d2e4a516b4..623c586aa7 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/nav-items/nav-items.component.ts @@ -1,15 +1,15 @@ import { AbpVisibleDirective, NavItem, NavItemsService } from '@abp/ng.theme.shared'; -import { Component, TrackByFunction } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { Component, TrackByFunction, inject } from '@angular/core'; +import { NgComponentOutlet, AsyncPipe } from '@angular/common'; import { PermissionDirective, ToInjectorPipe } from '@abp/ng.core'; @Component({ selector: 'abp-nav-items', templateUrl: 'nav-items.component.html', - imports: [CommonModule, AbpVisibleDirective, PermissionDirective, ToInjectorPipe], + imports: [NgComponentOutlet, AsyncPipe, AbpVisibleDirective, PermissionDirective, ToInjectorPipe], }) export class NavItemsComponent { - trackByFn: TrackByFunction = (_, element) => element.id; + readonly navItems = inject(NavItemsService); - constructor(public readonly navItems: NavItemsService) {} + trackByFn: TrackByFunction = (_, element) => element.id; } diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.ts index b28cd6c81b..f40ba2cd91 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/page-alert-container/page-alert-container.component.ts @@ -1,14 +1,14 @@ -import { Component, ViewEncapsulation } from '@angular/core'; +import { Component, ViewEncapsulation, inject } from '@angular/core'; import { PageAlertService } from '@abp/ng.theme.shared'; -import { CommonModule } from '@angular/common'; +import { NgClass, AsyncPipe } from '@angular/common'; import { LocalizationPipe, SafeHtmlPipe } from '@abp/ng.core'; @Component({ selector: 'abp-page-alert-container', templateUrl: './page-alert-container.component.html', encapsulation: ViewEncapsulation.None, - imports: [CommonModule, LocalizationPipe, SafeHtmlPipe], + imports: [NgClass, AsyncPipe, LocalizationPipe, SafeHtmlPipe], }) export class PageAlertContainerComponent { - constructor(public service: PageAlertService) {} + service = inject(PageAlertService); } diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts index 5d91c660da..5bc56ee765 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/routes/routes.component.ts @@ -16,17 +16,19 @@ import { TrackByFunction, ViewChildren, } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgTemplateOutlet, NgClass, AsyncPipe } from '@angular/common'; import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; -import { RouterModule } from '@angular/router'; +import { RouterLink } from '@angular/router'; import { EllipsisDirective } from '@abp/ng.theme.shared'; @Component({ selector: 'abp-routes', templateUrl: 'routes.component.html', imports: [ - CommonModule, - RouterModule, + NgTemplateOutlet, + NgClass, + AsyncPipe, + RouterLink, NgbDropdownModule, LazyLocalizationPipe, PermissionDirective, diff --git a/npm/ng-packs/packages/theme-basic/src/lib/components/validation-error/validation-error.component.ts b/npm/ng-packs/packages/theme-basic/src/lib/components/validation-error/validation-error.component.ts index bfa9d6cab2..213ebb6306 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/components/validation-error/validation-error.component.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/components/validation-error/validation-error.component.ts @@ -1,4 +1,3 @@ -import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, ViewEncapsulation } from '@angular/core'; import { Validation, ValidationErrorComponent as ErrorComponent } from '@ngx-validate/core'; import { LocalizationPipe } from '@abp/ng.core'; @@ -14,7 +13,7 @@ import { LocalizationPipe } from '@abp/ng.core'; `, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, - imports: [CommonModule, LocalizationPipe], + imports: [LocalizationPipe], }) export class ValidationErrorComponent extends ErrorComponent { get abpErrors(): (Validation.Error & { interpoliteParams?: string[] })[] { diff --git a/npm/ng-packs/packages/theme-basic/src/lib/constants/styles.ts b/npm/ng-packs/packages/theme-basic/src/lib/constants/styles.ts index d19776424f..5d4e745ce9 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/constants/styles.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/constants/styles.ts @@ -151,4 +151,15 @@ background-color: rgba(0, 0, 0, 0.6); .abp-md-form { max-width: 540px; } + +.ngx-datatable.material:has(.datatable-body-row) .datatable-footer { + border-top: none; +} + +.ngx-datatable.material:not(:has(.datatable-body-row)) .datatable-footer { + border-top: 1px solid #dee2e6; +} + `; + + diff --git a/npm/ng-packs/packages/theme-basic/src/lib/handlers/lazy-style.handler.ts b/npm/ng-packs/packages/theme-basic/src/lib/handlers/lazy-style.handler.ts index 02d9f69817..1064a143fb 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/handlers/lazy-style.handler.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/handlers/lazy-style.handler.ts @@ -1,6 +1,6 @@ import { LazyLoadService, LOADING_STRATEGY } from '@abp/ng.core'; import { DocumentDirHandlerService, LocaleDirection } from '@abp/ng.theme.shared'; -import { Injectable, Injector } from '@angular/core'; +import { Injectable, Injector, inject } from '@angular/core'; import { LAZY_STYLES } from '../tokens/lazy-styles.token'; export const BOOTSTRAP = 'bootstrap-{{dir}}.min.css'; @@ -23,7 +23,9 @@ export class LazyStyleHandler { return this._dir; } - constructor(injector: Injector) { + constructor() { + const injector = inject(Injector); + this.setStyles(injector); this.setLazyLoad(injector); this.listenToDirectionChanges(injector); @@ -88,8 +90,8 @@ export function createLazyStyleHref(style: string, dir: string): string { return style.replace(/{{\s*dir\s*}}/g, dir); } -export function initLazyStyleHandler(injector: Injector) { - return () => new LazyStyleHandler(injector); +export function initLazyStyleHandler() { + return () => new LazyStyleHandler(); } interface LoadedStyle { diff --git a/npm/ng-packs/packages/theme-basic/src/lib/services/layout.service.ts b/npm/ng-packs/packages/theme-basic/src/lib/services/layout.service.ts index a990173218..0f8a535fe5 100644 --- a/npm/ng-packs/packages/theme-basic/src/lib/services/layout.service.ts +++ b/npm/ng-packs/packages/theme-basic/src/lib/services/layout.service.ts @@ -1,11 +1,14 @@ import {RouterEvents, SubscriptionService} from '@abp/ng.core'; -import { ChangeDetectorRef, Injectable } from '@angular/core'; +import { ChangeDetectorRef, Injectable, inject } from '@angular/core'; import { fromEvent } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { eThemeBasicComponents } from '../enums'; @Injectable() export class LayoutService { + private subscription = inject(SubscriptionService); + private cdRef = inject(ChangeDetectorRef); + isCollapsed = true; smallScreen!: boolean; // do not set true or false @@ -16,9 +19,10 @@ export class LayoutService { navItemsComponentKey = eThemeBasicComponents.NavItems; - constructor(private subscription: SubscriptionService, - private cdRef: ChangeDetectorRef, - routerEvents:RouterEvents) { + constructor() { + const subscription = this.subscription; + const routerEvents = inject(RouterEvents); + subscription.addOne(routerEvents.getNavigationEvents("End"),() => { this.isCollapsed = true; }) diff --git a/npm/ng-packs/packages/theme-basic/tsconfig.lib.json b/npm/ng-packs/packages/theme-basic/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/theme-basic/tsconfig.lib.json +++ b/npm/ng-packs/packages/theme-basic/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts index 0c68151d36..6e60a77669 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb-items/breadcrumb-items.component.ts @@ -1,12 +1,12 @@ import { Component, Input } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { CommonModule } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; +import { RouterLink } from '@angular/router'; import { ABP, LocalizationPipe } from '@abp/ng.core'; @Component({ selector: 'abp-breadcrumb-items', templateUrl: './breadcrumb-items.component.html', - imports: [CommonModule, RouterModule, LocalizationPipe], + imports: [ NgTemplateOutlet, RouterLink, LocalizationPipe], }) export class BreadcrumbItemsComponent { @Input() items: Partial[] = []; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts index 4c7bd3cf30..f269be8d1e 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/breadcrumb/breadcrumb.component.ts @@ -6,7 +6,7 @@ import { SubscriptionService, TreeNode, } from '@abp/ng.core'; -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, inject } from '@angular/core'; import { Router } from '@angular/router'; import { map, startWith } from 'rxjs/operators'; import { eThemeSharedRouteNames } from '../../enums/route-names'; @@ -20,15 +20,13 @@ import { BreadcrumbItemsComponent } from '../breadcrumb-items/breadcrumb-items.c imports: [BreadcrumbItemsComponent], }) export class BreadcrumbComponent implements OnInit { - segments: Partial[] = []; + readonly cdRef = inject(ChangeDetectorRef); + private router = inject(Router); + private routes = inject(RoutesService); + private subscription = inject(SubscriptionService); + private routerEvents = inject(RouterEvents); - constructor( - public readonly cdRef: ChangeDetectorRef, - private router: Router, - private routes: RoutesService, - private subscription: SubscriptionService, - private routerEvents: RouterEvents, - ) {} + segments: Partial[] = []; ngOnInit(): void { this.subscription.addOne( diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts index fdebb542a7..3298b6f82c 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts @@ -1,15 +1,16 @@ /* eslint-disable @angular-eslint/no-output-native */ -import { - Component, - ElementRef, - EventEmitter, - Input, - OnInit, - Output, - Renderer2, - ViewChild, +import { + Component, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + Renderer2, + ViewChild, + inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgClass } from '@angular/common'; import { ABP } from '@abp/ng.core'; @Component({ @@ -29,9 +30,11 @@ import { ABP } from '@abp/ng.core'; `, - imports: [CommonModule], + imports: [NgClass], }) export class ButtonComponent implements OnInit { + private renderer = inject(Renderer2); + @Input() buttonId = ''; @@ -75,8 +78,6 @@ export class ButtonComponent implements OnInit { return `${this.loading ? 'fa fa-spinner fa-spin' : this.iconClass || 'd-none'}`; } - constructor(private renderer: Renderer2) {} - ngOnInit() { if (this.attributes) { Object.keys(this.attributes).forEach(key => { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts index 2376cef02e..35790054b5 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-body.component.ts @@ -1,12 +1,12 @@ import { Component, HostBinding, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; @Component({ selector: 'abp-card-body', template: `
`, - imports: [CommonModule], + imports: [NgClass, NgStyle], }) export class CardBodyComponent { @HostBinding('class') componentClass = 'card-body'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts index 0e09e1fda6..38a798e146 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-footer.component.ts @@ -1,5 +1,5 @@ import { Component, HostBinding, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; @Component({ selector: 'abp-card-footer', @@ -9,7 +9,7 @@ import { CommonModule } from '@angular/common'; `, styles: [], - imports: [CommonModule], + imports: [NgClass, NgStyle], }) export class CardFooterComponent { @HostBinding('class') componentClass = 'card-footer'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts index 03e1fa8af4..4857913d77 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card-header.component.ts @@ -1,5 +1,5 @@ import { Component, HostBinding, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; @Component({ selector: 'abp-card-header', @@ -9,7 +9,7 @@ import { CommonModule } from '@angular/common'; `, styles: [], - imports: [CommonModule], + imports: [NgClass, NgStyle], }) export class CardHeaderComponent { @HostBinding('class') componentClass = 'card-header'; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts index 5c2269b8aa..42973fdcb0 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/card/card.component.ts @@ -1,12 +1,12 @@ import { Component, Input } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; @Component({ selector: 'abp-card', template: `
`, - imports: [CommonModule], + imports: [NgClass, NgStyle], }) export class CardComponent { @Input() cardClass: string; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts index 9ee8cb1af5..8cadf828d1 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/checkbox/checkbox.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; import { NG_VALUE_ACCESSOR, FormsModule } from '@angular/forms'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core'; @Component({ @@ -31,7 +31,7 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core'; multi: true, }, ], - imports: [CommonModule, FormsModule, LocalizationPipe], + imports: [NgClass, NgStyle, FormsModule, LocalizationPipe], }) export class FormCheckboxComponent extends AbstractNgModelComponent { @Input() label?: string; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts index f211b758e3..3ca2158c11 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/confirmation/confirmation.component.ts @@ -1,5 +1,5 @@ -import { Component, Inject } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgClass, AsyncPipe } from '@angular/common'; +import { Component, inject } from '@angular/core'; import { ReplaySubject } from 'rxjs'; import { Confirmation } from '../../models/confirmation'; import { CONFIRMATION_ICONS, ConfirmationIcons } from '../../tokens/confirmation-icons.token'; @@ -9,10 +9,11 @@ import { LocalizationPipe } from '@abp/ng.core'; selector: 'abp-confirmation', templateUrl: './confirmation.component.html', styleUrls: ['./confirmation.component.scss'], - imports: [CommonModule, LocalizationPipe], + imports: [NgClass, AsyncPipe, LocalizationPipe], }) export class ConfirmationComponent { - constructor(@Inject(CONFIRMATION_ICONS) private icons: ConfirmationIcons) {} + private icons = inject(CONFIRMATION_ICONS); + confirm = Confirmation.Status.confirm; reject = Confirmation.Status.reject; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts index 73ad3306b1..2d10ab5709 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/form-input/form-input.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, forwardRef, Input, Output } from '@angular/core'; import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core'; @Component({ @@ -32,7 +32,7 @@ import { AbstractNgModelComponent, LocalizationPipe } from '@abp/ng.core'; multi: true, }, ], - imports: [CommonModule, LocalizationPipe, FormsModule], + imports: [NgClass, NgStyle, LocalizationPipe, FormsModule], }) export class FormInputComponent extends AbstractNgModelComponent { @Input() inputId!: string; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/internet-connection-status/internet-connection-status.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/internet-connection-status/internet-connection-status.component.ts index 04f5a9a5ab..9963868ed1 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/internet-connection-status/internet-connection-status.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/internet-connection-status/internet-connection-status.component.ts @@ -1,10 +1,10 @@ import { Component, inject } from '@angular/core'; -import { InternetConnectionService, LocalizationModule } from '@abp/ng.core'; +import { InternetConnectionService, LocalizationPipe } from '@abp/ng.core'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'abp-internet-status', - imports: [LocalizationModule, NgbTooltip], + imports: [LocalizationPipe, NgbTooltip], template: ` @if (!isOnline()) {
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts index 501fbbb82a..244b055322 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/loader-bar/loader-bar.component.ts @@ -1,6 +1,5 @@ -import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; -import { CommonModule } from '@angular/common'; +import { NgClass, NgStyle } from '@angular/common'; +import { ChangeDetectorRef, Component, Input, OnDestroy, OnInit, inject } from '@angular/core'; import { combineLatest, Subscription, timer } from 'rxjs'; import { HttpWaitService, RouterWaitService, SubscriptionService } from '@abp/ng.core'; @@ -21,9 +20,14 @@ import { HttpWaitService, RouterWaitService, SubscriptionService } from '@abp/ng `, styleUrls: ['./loader-bar.component.scss'], providers: [SubscriptionService], - imports: [CommonModule], + imports: [NgClass, NgStyle], }) export class LoaderBarComponent implements OnDestroy, OnInit { + private cdRef = inject(ChangeDetectorRef); + private subscription = inject(SubscriptionService); + private httpWaitService = inject(HttpWaitService); + private routerWaitService = inject(RouterWaitService); + protected _isLoading!: boolean; @Input() @@ -73,14 +77,6 @@ export class LoaderBarComponent implements OnDestroy, OnInit { return `0 0 10px rgba(${this.color}, 0.5)`; } - constructor( - private router: Router, - private cdRef: ChangeDetectorRef, - private subscription: SubscriptionService, - private httpWaitService: HttpWaitService, - private routerWaitService: RouterWaitService, - ) {} - ngOnInit() { this.subscribeLoading(); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal-close.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal-close.directive.ts index 96455d1151..e15422d31b 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal-close.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal-close.directive.ts @@ -1,11 +1,15 @@ -import { Directive, HostListener, Optional } from '@angular/core'; +import { Directive, HostListener, inject } from '@angular/core'; import { ModalComponent } from './modal.component'; @Directive({ selector: '[abpClose]', }) export class ModalCloseDirective { - constructor(@Optional() private modal: ModalComponent) { + private modal = inject(ModalComponent, { optional: true })!; + + constructor() { + const modal = this.modal; + if (!modal) { console.error('Please use abpClose within an abp-modal'); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts index 3ac175ae6c..5e03aa5e75 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/modal/modal.component.ts @@ -12,7 +12,7 @@ import { output, viewChild, } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { SubscriptionService, uuid } from '@abp/ng.core'; import { NgbModal, NgbModalOptions, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; @@ -31,7 +31,7 @@ export type ModalSize = 'sm' | 'md' | 'lg' | 'xl'; templateUrl: './modal.component.html', styleUrls: ['./modal.component.scss'], providers: [SubscriptionService], - imports: [CommonModule], + imports: [NgTemplateOutlet], }) export class ModalComponent implements OnInit, OnDestroy, DismissableModal { protected readonly confirmationService = inject(ConfirmationService); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.ts index a6e670db0b..eeefb3ac01 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/password/password.component.ts @@ -1,6 +1,6 @@ import { Component, forwardRef, Input } from '@angular/core'; -import { FormsModule, NG_VALUE_ACCESSOR, ReactiveFormsModule } from '@angular/forms'; -import { CommonModule } from '@angular/common'; +import { FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { NgClass } from '@angular/common'; import { AbstractNgModelComponent } from '@abp/ng.core'; import { NgxValidateCoreModule } from '@ngx-validate/core'; @@ -10,7 +10,7 @@ import { NgxValidateCoreModule } from '@ngx-validate/core'; */ @Component({ selector: 'abp-password', - imports: [CommonModule, FormsModule, ReactiveFormsModule, NgxValidateCoreModule], + imports: [NgClass, FormsModule, NgxValidateCoreModule], templateUrl: `./password.component.html`, providers: [ { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts index 8bb1a7126e..7834a87c05 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/components/toast/toast.component.ts @@ -1,5 +1,5 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; -import { CommonModule } from '@angular/common'; +import { NgClass } from '@angular/common'; import { Toaster } from '../../models/toaster'; import { LocalizationPipe } from '@abp/ng.core'; @@ -7,7 +7,7 @@ import { LocalizationPipe } from '@abp/ng.core'; selector: 'abp-toast', templateUrl: './toast.component.html', styleUrls: ['./toast.component.scss'], - imports: [CommonModule, LocalizationPipe], + imports: [NgClass, LocalizationPipe], }) export class ToastComponent implements OnInit { @Input() diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/disabled.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/disabled.directive.ts index c0a1a74995..e00d827ba7 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/disabled.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/disabled.directive.ts @@ -1,15 +1,15 @@ -import { Directive, Host, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { Directive, Input, OnChanges, SimpleChanges, inject } from '@angular/core'; import { NgControl } from '@angular/forms'; @Directive({ selector: '[abpDisabled]', }) export class DisabledDirective implements OnChanges { + private ngControl = inject(NgControl, { host: true }); + @Input() abpDisabled = false; - constructor(@Host() private ngControl: NgControl) {} - // Related issue: https://github.com/angular/angular/issues/35330 ngOnChanges({ abpDisabled }: SimpleChanges) { if (this.ngControl.control && abpDisabled) { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/ellipsis.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/ellipsis.directive.ts index 94d963ce2e..b26be01a34 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/ellipsis.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/ellipsis.directive.ts @@ -1,17 +1,20 @@ -import { - AfterViewInit, - ChangeDetectorRef, - Directive, - ElementRef, - HostBinding, - Input, - NgModule, +import { + AfterViewInit, + ChangeDetectorRef, + Directive, + ElementRef, + HostBinding, + Input, + inject } from '@angular/core'; @Directive({ selector: '[abpEllipsis]', }) export class EllipsisDirective implements AfterViewInit { + private cdRef = inject(ChangeDetectorRef); + private elRef = inject(ElementRef); + @Input('abpEllipsis') width?: string; @@ -37,11 +40,6 @@ export class EllipsisDirective implements AfterViewInit { return this.enabled && this.width ? this.width || '170px' : undefined; } - constructor( - private cdRef: ChangeDetectorRef, - private elRef: ElementRef, - ) {} - ngAfterViewInit() { this.title = this.title || (this.elRef.nativeElement as HTMLElement).innerText; this.cdRef.detectChanges(); diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts index 53b8e331f1..2ac03dcc19 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/loading.directive.ts @@ -1,16 +1,16 @@ -import { - ComponentFactoryResolver, - ComponentRef, - Directive, - ElementRef, - EmbeddedViewRef, - HostBinding, - Injector, - Input, - OnDestroy, - OnInit, - Renderer2, - ViewContainerRef, +import { + ComponentFactoryResolver, + ComponentRef, + Directive, + ElementRef, + EmbeddedViewRef, + HostBinding, + Injector, + Input, + OnDestroy, + OnInit, + Renderer2, + inject } from '@angular/core'; import { Subscription, timer } from 'rxjs'; import { take } from 'rxjs/operators'; @@ -20,6 +20,11 @@ import { LoadingComponent } from '../components'; selector: '[abpLoading]', }) export class LoadingDirective implements OnInit, OnDestroy { + private elRef = inject>(ElementRef); + private cdRes = inject(ComponentFactoryResolver); + private injector = inject(Injector); + private renderer = inject(Renderer2); + private _loading!: boolean; @HostBinding('style.position') @@ -77,14 +82,6 @@ export class LoadingDirective implements OnInit, OnDestroy { rootNode: HTMLDivElement | null = null; timerSubscription: Subscription | null = null; - constructor( - private elRef: ElementRef, - private vcRef: ViewContainerRef, - private cdRes: ComponentFactoryResolver, - private injector: Injector, - private renderer: Renderer2, - ) {} - ngOnInit() { if (!this.targetElement) { const { offsetHeight, offsetWidth } = this.elRef.nativeElement; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts index c7dc96f3c9..23ae8736f8 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/ngx-datatable-default.directive.ts @@ -1,5 +1,5 @@ import { DOCUMENT } from '@angular/common'; -import { AfterViewInit, Directive, HostBinding, Inject, Input, OnDestroy } from '@angular/core'; +import { AfterViewInit, Directive, HostBinding, Input, OnDestroy, inject } from '@angular/core'; import { ColumnMode, DatatableComponent, ScrollerComponent } from '@swimlane/ngx-datatable'; import { fromEvent, Subscription } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; @@ -10,6 +10,9 @@ import { debounceTime } from 'rxjs/operators'; exportAs: 'ngxDatatableDefault', }) export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy { + private table = inject(DatatableComponent); + private document = inject(DOCUMENT); + private subscription = new Subscription(); private resizeDiff = 0; @@ -20,10 +23,7 @@ export class NgxDatatableDefaultDirective implements AfterViewInit, OnDestroy { return `ngx-datatable ${this.class}`; } - constructor( - private table: DatatableComponent, - @Inject(DOCUMENT) private document: MockDocument, - ) { + constructor() { this.table.columnMode = ColumnMode.force; this.table.footerHeight = 50; this.table.headerHeight = 50; diff --git a/npm/ng-packs/packages/theme-shared/src/lib/directives/visible.directive.ts b/npm/ng-packs/packages/theme-shared/src/lib/directives/visible.directive.ts index 9613779472..7532041462 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/directives/visible.directive.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/directives/visible.directive.ts @@ -1,10 +1,13 @@ -import { OnInit, Directive, OnDestroy, Input, ViewContainerRef, TemplateRef } from '@angular/core'; +import { OnInit, Directive, OnDestroy, Input, ViewContainerRef, TemplateRef, inject } from '@angular/core'; import { EMPTY, from, Observable, of, Subscription } from 'rxjs'; @Directive({ selector: '[abpVisible]', }) export class AbpVisibleDirective implements OnDestroy, OnInit { + private viewContainerRef = inject(ViewContainerRef); + private templateRef = inject>(TemplateRef); + conditionSubscription: Subscription | undefined; isVisible: boolean | undefined; @@ -16,11 +19,6 @@ export class AbpVisibleDirective implements OnDestroy, OnInit { } private condition$: Observable = of(false); - - constructor( - private viewContainerRef: ViewContainerRef, - private templateRef: TemplateRef, - ) {} ngOnInit(): void { this.updateVisibility(); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/document-dir.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/document-dir.handler.ts index 8260a39c27..7ca9426076 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/document-dir.handler.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/document-dir.handler.ts @@ -1,14 +1,16 @@ import { getLocaleDirection, LocalizationService } from '@abp/ng.core'; -import { Injectable, Injector } from '@angular/core'; +import { Injectable, Injector, inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; import { LocaleDirection } from '../models/common'; @Injectable() export class DocumentDirHandlerService { + protected injector = inject(Injector); + private dir = new BehaviorSubject('ltr'); dir$ = this.dir.asObservable(); - constructor(protected injector: Injector) { + constructor() { this.listenToLanguageChanges(); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts index 07c5721625..f4a799ba1b 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/handlers/error.handler.ts @@ -17,6 +17,8 @@ import { RouterErrorHandlerService } from '../services/router-error-handler.serv @Injectable({ providedIn: 'root' }) export class ErrorHandler { + protected injector = inject(Injector); + protected readonly httpErrorReporter = inject(HttpErrorReporterService); protected readonly confirmationService = inject(ConfirmationService); protected readonly routerErrorHandlerService = inject(RouterErrorHandlerService); @@ -24,7 +26,7 @@ export class ErrorHandler { protected readonly customErrorHandlers = inject(CUSTOM_ERROR_HANDLERS); protected readonly httpErrorHandler = inject(HTTP_ERROR_HANDLER, { optional: true }); - constructor(protected injector: Injector) { + constructor() { this.listenToRestError(); this.listenToRouterError(); } diff --git a/npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts b/npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts index 5eeadb0ab6..ead1f0c91a 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/services/confirmation.service.ts @@ -1,5 +1,5 @@ import { ContentProjectionService, LocalizationParam, PROJECTION_STRATEGY } from '@abp/ng.core'; -import { ComponentRef, Injectable } from '@angular/core'; +import { ComponentRef, Injectable, inject } from '@angular/core'; import { fromEvent, Observable, ReplaySubject, Subject } from 'rxjs'; import { debounceTime, filter, takeUntil } from 'rxjs/operators'; import { ConfirmationComponent } from '../components/confirmation/confirmation.component'; @@ -7,6 +7,8 @@ import { Confirmation } from '../models/confirmation'; @Injectable({ providedIn: 'root' }) export class ConfirmationService { + private contentProjectionService = inject(ContentProjectionService); + status$!: Subject; confirmation$ = new ReplaySubject(1); @@ -17,8 +19,6 @@ export class ConfirmationService { this.status$.next(status); }; - constructor(private contentProjectionService: ContentProjectionService) {} - private setContainer() { this.containerComponentRef = this.contentProjectionService.projectContent( PROJECTION_STRATEGY.AppendComponentToBody(ConfirmationComponent, { diff --git a/npm/ng-packs/packages/theme-shared/src/lib/utils/date-parser-formatter.ts b/npm/ng-packs/packages/theme-shared/src/lib/utils/date-parser-formatter.ts index 4a85a4dab6..40a99d98da 100644 --- a/npm/ng-packs/packages/theme-shared/src/lib/utils/date-parser-formatter.ts +++ b/npm/ng-packs/packages/theme-shared/src/lib/utils/date-parser-formatter.ts @@ -1,6 +1,6 @@ import { ApplicationLocalizationConfigurationDto, ConfigStateService } from '@abp/ng.core'; import { formatDate } from '@angular/common'; -import { Inject, Injectable, LOCALE_ID } from '@angular/core'; +import { Injectable, LOCALE_ID, inject } from '@angular/core'; import { NgbDateParserFormatter, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; function isNumber(value: any): boolean { @@ -13,10 +13,14 @@ function toInteger(value: any): number { @Injectable() export class DateParserFormatter extends NgbDateParserFormatter { - constructor(private configState: ConfigStateService, @Inject(LOCALE_ID) private locale: string) { + private configState = inject(ConfigStateService); + private locale = inject(LOCALE_ID); + + constructor() { super(); } + parse(value: string): NgbDateStruct | null { if (value) { const dateParts = value.trim().split('-'); diff --git a/npm/ng-packs/packages/theme-shared/tsconfig.lib.json b/npm/ng-packs/packages/theme-shared/tsconfig.lib.json index 58b88ce56c..22d2695db8 100644 --- a/npm/ng-packs/packages/theme-shared/tsconfig.lib.json +++ b/npm/ng-packs/packages/theme-shared/tsconfig.lib.json @@ -7,7 +7,7 @@ "declarationMap": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"], + "lib": ["dom", "es2020"], "useDefineForClassFields": false }, "exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"], diff --git a/npm/ng-packs/source-code-requirements/tsconfig.lib.json b/npm/ng-packs/source-code-requirements/tsconfig.lib.json index 884bdb3be7..df57fcc946 100644 --- a/npm/ng-packs/source-code-requirements/tsconfig.lib.json +++ b/npm/ng-packs/source-code-requirements/tsconfig.lib.json @@ -6,7 +6,7 @@ "declaration": true, "inlineSources": true, "types": [], - "lib": ["dom", "es2018"] + "lib": ["dom", "es2020"] }, "angularCompilerOptions": { "enableIvy": true, diff --git a/npm/ng-packs/tsconfig.base.json b/npm/ng-packs/tsconfig.base.json index d24de94706..f863f18520 100644 --- a/npm/ng-packs/tsconfig.base.json +++ b/npm/ng-packs/tsconfig.base.json @@ -10,7 +10,8 @@ "importHelpers": true, "target": "es2020", "module": "esnext", - "lib": ["es2017", "dom"], + "lib": ["es2020", "dom"], + "esModuleInterop": true, "baseUrl": "./", "allowSyntheticDefaultImports": true, "paths": { diff --git a/npm/packs/jquery-validation/abp.resourcemapping.js b/npm/packs/jquery-validation/abp.resourcemapping.js index 91575842d3..a0ce7addcc 100644 --- a/npm/packs/jquery-validation/abp.resourcemapping.js +++ b/npm/packs/jquery-validation/abp.resourcemapping.js @@ -1,6 +1,7 @@ module.exports = { mappings: { "@node_modules/jquery-validation/dist/jquery.validate.js": "@libs/jquery-validation/", - "@node_modules/jquery-validation/dist/localization/*.*": "@libs/jquery-validation/localization/" + "@node_modules/jquery-validation/dist/localization/*.*": "@libs/jquery-validation/localization/", + "@node_modules/@abp/jquery-validation/src/*.*": "@libs/jquery-validation/" } } \ No newline at end of file diff --git a/npm/packs/jquery-validation/src/abp.jquery.validate.js b/npm/packs/jquery-validation/src/abp.jquery.validate.js new file mode 100644 index 0000000000..62b32d1d22 --- /dev/null +++ b/npm/packs/jquery-validation/src/abp.jquery.validate.js @@ -0,0 +1,27 @@ +$.validator.methods.range = function (value, element, param) { + if (this.optional(element)) { + return true; + } + + // Remove thousands separators and convert decimal separators + var cleanValue = value.replace(/[.,](?=.*[.,])/g, '').replace(/[.,](?!.*[.,])/g, '.'); + var numericValue = parseFloat(cleanValue); + + // Check if conversion was successful and value is within range + return !isNaN(numericValue) && (numericValue >= param[0] && numericValue <= param[1]); +}; + +$.validator.methods.number = function (value, element) { + if (this.optional(element)) { + return true; + } + + // 1. Period as decimal separator, comma as thousands separator (e.g., 1,234.56 or -1,234.56) + // 2. Comma as decimal separator, period as thousands separator (e.g., 1.234,56 or -1.234,56) + // 3. Pure numbers (integers) + var pattern1 = /^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:-?\.\d+)?$/; + var pattern2 = /^(?:-?\d+|-?\d{1,3}(?:\.\d{3})+)?(?:-?,\d+)?$/; + var pattern3 = /^-?\d+$/; + + return pattern1.test(value) || pattern2.test(value) || pattern3.test(value); +} diff --git a/nupkg/common.ps1 b/nupkg/common.ps1 index 9dbce186ee..d6f3bab715 100644 --- a/nupkg/common.ps1 +++ b/nupkg/common.ps1 @@ -221,6 +221,7 @@ $projects = ( "framework/src/Volo.Abp.Ldap", "framework/src/Volo.Abp.Localization.Abstractions", "framework/src/Volo.Abp.MailKit", + "framework/src/Volo.Abp.Mapperly", "framework/src/Volo.Abp.Maui.Client", "framework/src/Volo.Abp.Localization", "framework/src/Volo.Abp.MemoryDb", diff --git a/templates/app-nolayers/angular/angular.json b/templates/app-nolayers/angular/angular.json index bfdb10c374..6ed0811fb5 100644 --- a/templates/app-nolayers/angular/angular.json +++ b/templates/app-nolayers/angular/angular.json @@ -19,12 +19,12 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/MyProjectName", "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/main.ts", + "polyfills": ["src/polyfills.ts"], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "allowedCommonJsDependencies": ["chart.js", "js-sha256"], @@ -137,12 +137,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" @@ -168,8 +165,8 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/test.ts", + "polyfills": ["src/polyfills.ts"], "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", diff --git a/templates/app-nolayers/angular/src/app/home/home.component.ts b/templates/app-nolayers/angular/src/app/home/home.component.ts index 4fa3761541..3a5856c7c2 100644 --- a/templates/app-nolayers/angular/src/app/home/home.component.ts +++ b/templates/app-nolayers/angular/src/app/home/home.component.ts @@ -1,12 +1,12 @@ import {AuthService, LocalizationPipe} from '@abp/ng.core'; import { Component, inject } from '@angular/core'; -import {CommonModule} from "@angular/common"; +import {NgTemplateOutlet} from "@angular/common"; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], - imports: [CommonModule, LocalizationPipe], + imports: [NgTemplateOutlet, LocalizationPipe], }) export class HomeComponent { private authService = inject(AuthService); diff --git a/templates/app-nolayers/angular/tsconfig.json b/templates/app-nolayers/angular/tsconfig.json index 29df0877f5..eeb0e33782 100644 --- a/templates/app-nolayers/angular/tsconfig.json +++ b/templates/app-nolayers/angular/tsconfig.json @@ -11,10 +11,11 @@ "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", - "module": "es2020", + "module": "esnext", "skipLibCheck": true, + "esModuleInterop": true, "lib": [ - "es2018", + "es2020", "dom" ], "paths": { diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj index ea549a8661..1f1593187e 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj @@ -18,7 +18,6 @@ - @@ -29,7 +28,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyProjectNameModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyProjectNameModule.cs index 988608d631..86c8803963 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyProjectNameModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyProjectNameModule.cs @@ -24,7 +24,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.MongoDB; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.MongoDB; using Volo.Abp.Emailing; using Volo.Abp.FeatureManagement; @@ -63,7 +63,7 @@ namespace MyCompanyName.MyProjectName; // ABP Framework packages typeof(AbpAspNetCoreMvcModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpSwashbuckleModule), typeof(AbpAspNetCoreSerilogModule), typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), @@ -176,7 +176,6 @@ public class MyProjectNameModule : AbpModule ConfigureAuthentication(context); ConfigureUrls(configuration); ConfigureBundles(); - ConfigureAutoMapper(context); ConfigureVirtualFiles(hostingEnvironment); ConfigureLocalizationServices(); ConfigureSwaggerServices(context.Services); @@ -185,6 +184,8 @@ public class MyProjectNameModule : AbpModule ConfigureBlazorise(context); ConfigureRouter(context); ConfigureMongoDB(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureAuthentication(ServiceConfigurationContext context) @@ -326,15 +327,6 @@ public class MyProjectNameModule : AbpModule }); } - private void ConfigureAutoMapper(ServiceConfigurationContext context) - { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddMaps(); - }); - } - private void ConfigureMongoDB(ServiceConfigurationContext context) { context.Services.AddMongoDbContext(options => diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs deleted file mode 100644 index 6871cd2257..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.ObjectMapping; - -public class MyProjectNameAutoMapperProfile : Profile -{ - public MyProjectNameAutoMapperProfile() - { - /* Create your AutoMapper object mappings here */ - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/ObjectMapping/MyProjectNameMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/ObjectMapping/MyProjectNameMappers.cs new file mode 100644 index 0000000000..d857855a80 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/ObjectMapping/MyProjectNameMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.ObjectMapping; + +// This file is a placeholder for Mapperly mappers. +// Add your mapper classes here following the pattern: +// [Mapper] +// public partial class SourceToDestinationMapper : MapperBase +// { +// public override partial Destination Map(Source source); +// public override partial void Map(Source source, Destination destination); +// } + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250611122251_Initial.Designer.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250717081711_Initial.Designer.cs similarity index 99% rename from templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250611122251_Initial.Designer.cs rename to templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250717081711_Initial.Designer.cs index baa25c45a2..db85e2f5d5 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250611122251_Initial.Designer.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250717081711_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Blazor.Server.Migrations { [DbContext(typeof(MyProjectNameDbContext))] - [Migration("20250611122251_Initial")] + [Migration("20250717081711_Initial")] partial class Initial { /// @@ -1229,6 +1229,9 @@ namespace MyCompanyName.MyProjectName.Blazor.Server.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1452,8 +1455,8 @@ namespace MyCompanyName.MyProjectName.Blazor.Server.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250611122251_Initial.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250717081711_Initial.cs similarity index 99% rename from templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250611122251_Initial.cs rename to templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250717081711_Initial.cs index dd86268274..ca5e31e7c4 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250611122251_Initial.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/20250717081711_Initial.cs @@ -427,6 +427,7 @@ namespace MyCompanyName.MyProjectName.Blazor.Server.Migrations RedirectUris = table.Column(type: "nvarchar(max)", nullable: true), Requirements = table.Column(type: "nvarchar(max)", nullable: true), Settings = table.Column(type: "nvarchar(max)", nullable: true), + FrontChannelLogoutUri = table.Column(type: "nvarchar(max)", nullable: true), ClientUri = table.Column(type: "nvarchar(max)", nullable: true), LogoUri = table.Column(type: "nvarchar(max)", nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), @@ -766,7 +767,7 @@ namespace MyCompanyName.MyProjectName.Blazor.Server.Migrations ReferenceId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), - Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Type = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/MyProjectNameDbContextModelSnapshot.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/MyProjectNameDbContextModelSnapshot.cs index 2aa577257f..aa06ad1bbe 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/MyProjectNameDbContextModelSnapshot.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Migrations/MyProjectNameDbContextModelSnapshot.cs @@ -1226,6 +1226,9 @@ namespace MyCompanyName.MyProjectName.Blazor.Server.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1449,8 +1452,8 @@ namespace MyCompanyName.MyProjectName.Blazor.Server.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index ca6b3478c2..313f51d004 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -18,7 +18,6 @@ - @@ -29,7 +28,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameModule.cs index 8923f6189b..762ca3b7b1 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameModule.cs @@ -25,7 +25,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Emailing; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.SqlServer; @@ -64,7 +64,7 @@ namespace MyCompanyName.MyProjectName; // ABP Framework packages typeof(AbpAspNetCoreMvcModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpSwashbuckleModule), typeof(AbpAspNetCoreSerilogModule), @@ -179,7 +179,6 @@ public class MyProjectNameModule : AbpModule ConfigureAuthentication(context); ConfigureUrls(configuration); ConfigureBundles(); - ConfigureAutoMapper(context); ConfigureVirtualFiles(hostingEnvironment); ConfigureLocalizationServices(); ConfigureSwaggerServices(context.Services); @@ -188,6 +187,8 @@ public class MyProjectNameModule : AbpModule ConfigureBlazorise(context); ConfigureRouter(context); ConfigureEfCore(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureAuthentication(ServiceConfigurationContext context) @@ -329,15 +330,6 @@ public class MyProjectNameModule : AbpModule }); } - private void ConfigureAutoMapper(ServiceConfigurationContext context) - { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddMaps(); - }); - } - private void ConfigureEfCore(ServiceConfigurationContext context) { context.Services.AddAbpDbContext(options => diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/ObjectMapping/MyProjectNameAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/ObjectMapping/MyProjectNameAutoMapperProfile.cs deleted file mode 100644 index 6871cd2257..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/ObjectMapping/MyProjectNameAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.ObjectMapping; - -public class MyProjectNameAutoMapperProfile : Profile -{ - public MyProjectNameAutoMapperProfile() - { - /* Create your AutoMapper object mappings here */ - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/ObjectMapping/MyProjectNameMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/ObjectMapping/MyProjectNameMappers.cs new file mode 100644 index 0000000000..d857855a80 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/ObjectMapping/MyProjectNameMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.ObjectMapping; + +// This file is a placeholder for Mapperly mappers. +// Add your mapper classes here following the pattern: +// [Mapper] +// public partial class SourceToDestinationMapper : MapperBase +// { +// public override partial Destination Map(Source source); +// public override partial void Map(Source source, Destination destination); +// } + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorAutoMapperProfile.cs deleted file mode 100644 index 623f6fb33e..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName; - -public class MyProjectNameBlazorAutoMapperProfile : Profile -{ - public MyProjectNameBlazorAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Blazor project. - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorMappers.cs new file mode 100644 index 0000000000..aeb87b3440 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Blazor.WebAssembly.Client; + +[Mapper] +public partial class MyProjectNameBlazorMappers +{ + //Define your Mapperly configuration here for the Blazor project. +} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorModule.cs index 46afc0d1b0..aeeec93107 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyProjectNameBlazorModule.cs @@ -10,7 +10,7 @@ using Volo.Abp.AspNetCore.Components.Web.LeptonXLiteTheme.Themes.LeptonXLite; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; using Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme; using Volo.Abp.Autofac.WebAssembly; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.FeatureManagement; using Volo.Abp.Identity; using Volo.Abp.Identity.Blazor.WebAssembly; @@ -68,8 +68,9 @@ public class MyProjectNameBlazorModule : AbpModule ConfigureBlazorise(context); ConfigureRouter(context); ConfigureMenu(context); - ConfigureAutoMapper(context); ConfigureHttpClientProxies(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureRouter(ServiceConfigurationContext context) @@ -131,12 +132,4 @@ public class MyProjectNameBlazorModule : AbpModule BaseAddress = new Uri(environment.BaseAddress) }); } - - private void ConfigureAutoMapper(ServiceConfigurationContext context) - { - Configure(options => - { - options.AddMaps(); - }); - } } diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj index dd64f3032f..8393fda5bc 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj @@ -19,7 +19,7 @@ - + @@ -72,7 +72,6 @@ - diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyProjectNameHostModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyProjectNameHostModule.cs index 4d8c9f7f51..42ad99abd1 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyProjectNameHostModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyProjectNameHostModule.cs @@ -23,7 +23,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.MongoDB; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Emailing; using Volo.Abp.FeatureManagement; using Volo.Abp.FeatureManagement.MongoDB; @@ -58,7 +58,7 @@ namespace MyCompanyName.MyProjectName; typeof(AbpAspNetCoreMvcModule), typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule), typeof(AbpSwashbuckleModule), @@ -156,7 +156,7 @@ public class MyProjectNameHostModule : AbpModule ConfigureBundles(); ConfigureMultiTenancy(); ConfigureUrls(configuration); - ConfigureAutoMapper(context); + ConfigureMapperly(context); ConfigureSwagger(context.Services, configuration); ConfigureAutoApiControllers(); ConfigureVirtualFiles(hostingEnvironment); @@ -239,17 +239,9 @@ public class MyProjectNameHostModule : AbpModule }); } - private void ConfigureAutoMapper(ServiceConfigurationContext context) + private void ConfigureMapperly(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - /* Uncomment `validate: true` if you want to enable the Configuration Validation feature. - * See AutoMapper's documentation to learn what it is: - * https://docs.automapper.org/en/stable/Configuration-validation.html - */ - options.AddMaps(/* validate: true */); - }); + context.Services.AddMapperlyObjectMapper(); } private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration) diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs deleted file mode 100644 index 6871cd2257..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.ObjectMapping; - -public class MyProjectNameAutoMapperProfile : Profile -{ - public MyProjectNameAutoMapperProfile() - { - /* Create your AutoMapper object mappings here */ - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/ObjectMapping/MyProjectNameMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/ObjectMapping/MyProjectNameMappers.cs new file mode 100644 index 0000000000..d857855a80 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/ObjectMapping/MyProjectNameMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.ObjectMapping; + +// This file is a placeholder for Mapperly mappers. +// Add your mapper classes here following the pattern: +// [Mapper] +// public partial class SourceToDestinationMapper : MapperBase +// { +// public override partial Destination Map(Source source); +// public override partial void Map(Source source, Destination destination); +// } + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250611122409_Initial.Designer.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250717081855_Initial.Designer.cs similarity index 99% rename from templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250611122409_Initial.Designer.cs rename to templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250717081855_Initial.Designer.cs index daa422bbcc..961580c3c6 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250611122409_Initial.Designer.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250717081855_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Migrations { [DbContext(typeof(MyProjectNameDbContext))] - [Migration("20250611122409_Initial")] + [Migration("20250717081855_Initial")] partial class Initial { /// @@ -1229,6 +1229,9 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1452,8 +1455,8 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250611122321_Initial.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250717081855_Initial.cs similarity index 99% rename from templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250611122321_Initial.cs rename to templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250717081855_Initial.cs index 12a56494ee..d4990d59a7 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250611122321_Initial.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250717081855_Initial.cs @@ -427,6 +427,7 @@ namespace MyCompanyName.MyProjectName.Migrations RedirectUris = table.Column(type: "nvarchar(max)", nullable: true), Requirements = table.Column(type: "nvarchar(max)", nullable: true), Settings = table.Column(type: "nvarchar(max)", nullable: true), + FrontChannelLogoutUri = table.Column(type: "nvarchar(max)", nullable: true), ClientUri = table.Column(type: "nvarchar(max)", nullable: true), LogoUri = table.Column(type: "nvarchar(max)", nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), @@ -766,7 +767,7 @@ namespace MyCompanyName.MyProjectName.Migrations ReferenceId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), - Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Type = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/MyProjectNameDbContextModelSnapshot.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/MyProjectNameDbContextModelSnapshot.cs index 617cde4a48..d216c5d155 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/MyProjectNameDbContextModelSnapshot.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/MyProjectNameDbContextModelSnapshot.cs @@ -1226,6 +1226,9 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1449,8 +1452,8 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj index f378584d5c..e15b5ecc30 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj @@ -19,7 +19,7 @@ - + @@ -73,7 +73,6 @@ - diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyProjectNameHostModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyProjectNameHostModule.cs index 454120e5ef..f77a4069e1 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyProjectNameHostModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyProjectNameHostModule.cs @@ -23,7 +23,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Emailing; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.SqlServer; @@ -60,7 +60,7 @@ namespace MyCompanyName.MyProjectName; typeof(AbpAspNetCoreMvcModule), typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), typeof(AbpAspNetCoreComponentsWebAssemblyLeptonXLiteThemeBundlingModule), @@ -161,7 +161,7 @@ public class MyProjectNameHostModule : AbpModule ConfigureBundles(); ConfigureMultiTenancy(); ConfigureUrls(configuration); - ConfigureAutoMapper(context); + ConfigureMapperly(context); ConfigureSwagger(context.Services, configuration); ConfigureAutoApiControllers(); ConfigureVirtualFiles(hostingEnvironment); @@ -244,17 +244,9 @@ public class MyProjectNameHostModule : AbpModule }); } - private void ConfigureAutoMapper(ServiceConfigurationContext context) + private void ConfigureMapperly(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - /* Uncomment `validate: true` if you want to enable the Configuration Validation feature. - * See AutoMapper's documentation to learn what it is: - * https://docs.automapper.org/en/stable/Configuration-validation.html - */ - options.AddMaps(/* validate: true */); - }); + context.Services.AddMapperlyObjectMapper(); } private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration) diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/ObjectMapping/MyProjectNameAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/ObjectMapping/MyProjectNameAutoMapperProfile.cs deleted file mode 100644 index 6871cd2257..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/ObjectMapping/MyProjectNameAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.ObjectMapping; - -public class MyProjectNameAutoMapperProfile : Profile -{ - public MyProjectNameAutoMapperProfile() - { - /* Create your AutoMapper object mappings here */ - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/ObjectMapping/MyProjectNameMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/ObjectMapping/MyProjectNameMappers.cs new file mode 100644 index 0000000000..d857855a80 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/ObjectMapping/MyProjectNameMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.ObjectMapping; + +// This file is a placeholder for Mapperly mappers. +// Add your mapper classes here following the pattern: +// [Mapper] +// public partial class SourceToDestinationMapper : MapperBase +// { +// public override partial Destination Map(Source source); +// public override partial void Map(Source source, Destination destination); +// } + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj index 2cfb6401e0..f645f27eeb 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj @@ -15,7 +15,7 @@ - + @@ -68,7 +68,6 @@ - diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyProjectNameModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyProjectNameModule.cs index 0a1942912c..93a9bd89b6 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyProjectNameModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyProjectNameModule.cs @@ -19,7 +19,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.MongoDB; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Emailing; using Volo.Abp.FeatureManagement; using Volo.Abp.FeatureManagement.MongoDB; @@ -54,7 +54,7 @@ namespace MyCompanyName.MyProjectName; typeof(AbpAspNetCoreMvcModule), typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), typeof(AbpSwashbuckleModule), typeof(AbpAspNetCoreSerilogModule), @@ -153,7 +153,7 @@ public class MyProjectNameModule : AbpModule ConfigureBundles(); ConfigureMultiTenancy(); ConfigureUrls(configuration); - ConfigureAutoMapper(context); + ConfigureMapperly(context); ConfigureSwagger(context.Services, configuration); ConfigureAutoApiControllers(); ConfigureVirtualFiles(hostingEnvironment); @@ -279,17 +279,9 @@ public class MyProjectNameModule : AbpModule }); } - private void ConfigureAutoMapper(ServiceConfigurationContext context) + private void ConfigureMapperly(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - /* Uncomment `validate: true` if you want to enable the Configuration Validation feature. - * See AutoMapper's documentation to learn what it is: - * https://docs.automapper.org/en/stable/Configuration-validation.html - */ - options.AddMaps(/* validate: true */); - }); + context.Services.AddMapperlyObjectMapper(); } private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration) diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs deleted file mode 100644 index 6871cd2257..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.ObjectMapping; - -public class MyProjectNameAutoMapperProfile : Profile -{ - public MyProjectNameAutoMapperProfile() - { - /* Create your AutoMapper object mappings here */ - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/ObjectMapping/MyProjectNameMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/ObjectMapping/MyProjectNameMappers.cs new file mode 100644 index 0000000000..d857855a80 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/ObjectMapping/MyProjectNameMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.ObjectMapping; + +// This file is a placeholder for Mapperly mappers. +// Add your mapper classes here following the pattern: +// [Mapper] +// public partial class SourceToDestinationMapper : MapperBase +// { +// public override partial Destination Map(Source source); +// public override partial void Map(Source source, Destination destination); +// } + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250611122225_Initial.Designer.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250717081619_Initial.Designer.cs similarity index 99% rename from templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250611122225_Initial.Designer.cs rename to templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250717081619_Initial.Designer.cs index 88488681cd..c90ff0bb2e 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250611122225_Initial.Designer.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250717081619_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Host.Migrations { [DbContext(typeof(MyProjectNameDbContext))] - [Migration("20250611122225_Initial")] + [Migration("20250717081619_Initial")] partial class Initial { /// @@ -1229,6 +1229,9 @@ namespace MyCompanyName.MyProjectName.Host.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1452,8 +1455,8 @@ namespace MyCompanyName.MyProjectName.Host.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250611122225_Initial.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250717081619_Initial.cs similarity index 99% rename from templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250611122225_Initial.cs rename to templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250717081619_Initial.cs index 293ec738c4..b08f2c0b80 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250611122225_Initial.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/20250717081619_Initial.cs @@ -427,6 +427,7 @@ namespace MyCompanyName.MyProjectName.Host.Migrations RedirectUris = table.Column(type: "nvarchar(max)", nullable: true), Requirements = table.Column(type: "nvarchar(max)", nullable: true), Settings = table.Column(type: "nvarchar(max)", nullable: true), + FrontChannelLogoutUri = table.Column(type: "nvarchar(max)", nullable: true), ClientUri = table.Column(type: "nvarchar(max)", nullable: true), LogoUri = table.Column(type: "nvarchar(max)", nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), @@ -766,7 +767,7 @@ namespace MyCompanyName.MyProjectName.Host.Migrations ReferenceId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), - Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Type = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/MyProjectNameDbContextModelSnapshot.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/MyProjectNameDbContextModelSnapshot.cs index a403280dbc..fffa45164c 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/MyProjectNameDbContextModelSnapshot.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Migrations/MyProjectNameDbContextModelSnapshot.cs @@ -1226,6 +1226,9 @@ namespace MyCompanyName.MyProjectName.Host.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1449,8 +1452,8 @@ namespace MyCompanyName.MyProjectName.Host.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj index f05ec35849..67529a7e05 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj @@ -15,7 +15,7 @@ - + @@ -69,7 +69,6 @@ - diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyProjectNameModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyProjectNameModule.cs index a4a4410663..d0bc2944dc 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyProjectNameModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyProjectNameModule.cs @@ -20,7 +20,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Emailing; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.SqlServer; @@ -56,7 +56,7 @@ namespace MyCompanyName.MyProjectName; typeof(AbpAspNetCoreMvcModule), typeof(AbpAspNetCoreMultiTenancyModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), typeof(AbpSwashbuckleModule), @@ -157,7 +157,7 @@ public class MyProjectNameModule : AbpModule ConfigureBundles(); ConfigureMultiTenancy(); ConfigureUrls(configuration); - ConfigureAutoMapper(context); + ConfigureMapperly(context); ConfigureSwagger(context.Services, configuration); ConfigureAutoApiControllers(); ConfigureVirtualFiles(hostingEnvironment); @@ -283,17 +283,9 @@ public class MyProjectNameModule : AbpModule }); } - private void ConfigureAutoMapper(ServiceConfigurationContext context) + private void ConfigureMapperly(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - /* Uncomment `validate: true` if you want to enable the Configuration Validation feature. - * See AutoMapper's documentation to learn what it is: - * https://docs.automapper.org/en/stable/Configuration-validation.html - */ - options.AddMaps(/* validate: true */); - }); + context.Services.AddMapperlyObjectMapper(); } private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration) diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/ObjectMapping/MyProjectNameAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/ObjectMapping/MyProjectNameAutoMapperProfile.cs deleted file mode 100644 index 6871cd2257..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/ObjectMapping/MyProjectNameAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.ObjectMapping; - -public class MyProjectNameAutoMapperProfile : Profile -{ - public MyProjectNameAutoMapperProfile() - { - /* Create your AutoMapper object mappings here */ - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/ObjectMapping/MyProjectNameMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/ObjectMapping/MyProjectNameMappers.cs new file mode 100644 index 0000000000..d857855a80 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/ObjectMapping/MyProjectNameMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.ObjectMapping; + +// This file is a placeholder for Mapperly mappers. +// Add your mapper classes here following the pattern: +// [Mapper] +// public partial class SourceToDestinationMapper : MapperBase +// { +// public override partial Destination Map(Source source); +// public override partial void Map(Source source, Destination destination); +// } + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj index ab1cad55cd..383081b517 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj @@ -16,7 +16,6 @@ - @@ -24,7 +23,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyProjectNameModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyProjectNameModule.cs index 543c60f13f..b0331d6144 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyProjectNameModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyProjectNameModule.cs @@ -17,7 +17,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.MongoDB; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.MongoDB; using Volo.Abp.Emailing; using Volo.Abp.FeatureManagement; @@ -56,7 +56,7 @@ namespace MyCompanyName.MyProjectName; // ABP Framework packages typeof(AbpAspNetCoreMvcModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpSwashbuckleModule), typeof(AbpAspNetCoreSerilogModule), typeof(AbpAspNetCoreMvcUiLeptonXLiteThemeModule), @@ -159,7 +159,7 @@ public class MyProjectNameModule : AbpModule ConfigureMultiTenancy(); ConfigureUrls(configuration); ConfigureBundles(); - ConfigureAutoMapper(context); + ConfigureMapperly(context); ConfigureSwagger(context.Services); ConfigureNavigationServices(); ConfigureAutoApiControllers(); @@ -287,17 +287,9 @@ public class MyProjectNameModule : AbpModule ); } - private void ConfigureAutoMapper(ServiceConfigurationContext context) + private void ConfigureMapperly(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - /* Uncomment `validate: true` if you want to enable the Configuration Validation feature. - * See AutoMapper's documentation to learn what it is: - * https://docs.automapper.org/en/stable/Configuration-validation.html - */ - options.AddMaps(/* validate: true */); - }); + context.Services.AddMapperlyObjectMapper(); } private void ConfigureMongoDB(ServiceConfigurationContext context) diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs deleted file mode 100644 index 6871cd2257..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/ObjectMapping/MyProjectNameAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.ObjectMapping; - -public class MyProjectNameAutoMapperProfile : Profile -{ - public MyProjectNameAutoMapperProfile() - { - /* Create your AutoMapper object mappings here */ - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/ObjectMapping/MyProjectNameMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/ObjectMapping/MyProjectNameMappers.cs new file mode 100644 index 0000000000..d857855a80 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/ObjectMapping/MyProjectNameMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.ObjectMapping; + +// This file is a placeholder for Mapperly mappers. +// Add your mapper classes here following the pattern: +// [Mapper] +// public partial class SourceToDestinationMapper : MapperBase +// { +// public override partial Destination Map(Source source); +// public override partial void Map(Source source, Destination destination); +// } + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250611122237_Initial.Designer.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250717081642_Initial.Designer.cs similarity index 99% rename from templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250611122237_Initial.Designer.cs rename to templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250717081642_Initial.Designer.cs index c55ee59fc5..5e5d98d72e 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250611122237_Initial.Designer.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250717081642_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Mvc.Migrations { [DbContext(typeof(MyProjectNameDbContext))] - [Migration("20250611122237_Initial")] + [Migration("20250717081642_Initial")] partial class Initial { /// @@ -1229,6 +1229,9 @@ namespace MyCompanyName.MyProjectName.Mvc.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1452,8 +1455,8 @@ namespace MyCompanyName.MyProjectName.Mvc.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250611122237_Initial.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250717081642_Initial.cs similarity index 99% rename from templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250611122237_Initial.cs rename to templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250717081642_Initial.cs index 32de876108..fa3bbe7aed 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250611122237_Initial.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/20250717081642_Initial.cs @@ -427,6 +427,7 @@ namespace MyCompanyName.MyProjectName.Mvc.Migrations RedirectUris = table.Column(type: "nvarchar(max)", nullable: true), Requirements = table.Column(type: "nvarchar(max)", nullable: true), Settings = table.Column(type: "nvarchar(max)", nullable: true), + FrontChannelLogoutUri = table.Column(type: "nvarchar(max)", nullable: true), ClientUri = table.Column(type: "nvarchar(max)", nullable: true), LogoUri = table.Column(type: "nvarchar(max)", nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), @@ -766,7 +767,7 @@ namespace MyCompanyName.MyProjectName.Mvc.Migrations ReferenceId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), - Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Type = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/MyProjectNameDbContextModelSnapshot.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/MyProjectNameDbContextModelSnapshot.cs index 77eaf6b52c..5df74a87ff 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/MyProjectNameDbContextModelSnapshot.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Migrations/MyProjectNameDbContextModelSnapshot.cs @@ -1226,6 +1226,9 @@ namespace MyCompanyName.MyProjectName.Mvc.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1449,8 +1452,8 @@ namespace MyCompanyName.MyProjectName.Mvc.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj index c771ce0cf9..148f1afd74 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj @@ -16,7 +16,6 @@ - @@ -24,7 +23,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyProjectNameModule.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyProjectNameModule.cs index e8c99fc852..4869ab492e 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyProjectNameModule.cs +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyProjectNameModule.cs @@ -18,7 +18,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.AuditLogging.EntityFrameworkCore; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Emailing; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.SqlServer; @@ -57,7 +57,7 @@ namespace MyCompanyName.MyProjectName; // ABP Framework packages typeof(AbpAspNetCoreMvcModule), typeof(AbpAutofacModule), - typeof(AbpAutoMapperModule), + typeof(AbpMapperlyModule), typeof(AbpEntityFrameworkCoreSqlServerModule), typeof(AbpSwashbuckleModule), typeof(AbpAspNetCoreSerilogModule), @@ -162,7 +162,7 @@ public class MyProjectNameModule : AbpModule ConfigureMultiTenancy(); ConfigureUrls(configuration); ConfigureBundles(); - ConfigureAutoMapper(context); + ConfigureMapperly(context); ConfigureSwagger(context.Services); ConfigureNavigationServices(); ConfigureAutoApiControllers(); @@ -291,17 +291,9 @@ public class MyProjectNameModule : AbpModule ); } - private void ConfigureAutoMapper(ServiceConfigurationContext context) + private void ConfigureMapperly(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - /* Uncomment `validate: true` if you want to enable the Configuration Validation feature. - * See AutoMapper's documentation to learn what it is: - * https://docs.automapper.org/en/stable/Configuration-validation.html - */ - options.AddMaps(/* validate: true */); - }); + context.Services.AddMapperlyObjectMapper(); } private void ConfigureEfCore(ServiceConfigurationContext context) diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/ObjectMapping/MyProjectNameAutoMapperProfile.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/ObjectMapping/MyProjectNameAutoMapperProfile.cs deleted file mode 100644 index 6871cd2257..0000000000 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/ObjectMapping/MyProjectNameAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.ObjectMapping; - -public class MyProjectNameAutoMapperProfile : Profile -{ - public MyProjectNameAutoMapperProfile() - { - /* Create your AutoMapper object mappings here */ - } -} diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/ObjectMapping/MyProjectNameMappers.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/ObjectMapping/MyProjectNameMappers.cs new file mode 100644 index 0000000000..d857855a80 --- /dev/null +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/ObjectMapping/MyProjectNameMappers.cs @@ -0,0 +1,14 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.ObjectMapping; + +// This file is a placeholder for Mapperly mappers. +// Add your mapper classes here following the pattern: +// [Mapper] +// public partial class SourceToDestinationMapper : MapperBase +// { +// public override partial Destination Map(Source source); +// public override partial void Map(Source source, Destination destination); +// } + diff --git a/templates/app/angular/angular.json b/templates/app/angular/angular.json index bfdb10c374..6ed0811fb5 100644 --- a/templates/app/angular/angular.json +++ b/templates/app/angular/angular.json @@ -19,12 +19,12 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/MyProjectName", "index": "src/index.html", - "main": "src/main.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/main.ts", + "polyfills": ["src/polyfills.ts"], "tsConfig": "tsconfig.app.json", "inlineStyleLanguage": "scss", "allowedCommonJsDependencies": ["chart.js", "js-sha256"], @@ -137,12 +137,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" @@ -168,8 +165,8 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "src/test.ts", - "polyfills": "src/polyfills.ts", + "browser": "src/test.ts", + "polyfills": ["src/polyfills.ts"], "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", "inlineStyleLanguage": "scss", diff --git a/templates/app/angular/src/app/home/home.component.ts b/templates/app/angular/src/app/home/home.component.ts index cf42b0f606..420edd4724 100644 --- a/templates/app/angular/src/app/home/home.component.ts +++ b/templates/app/angular/src/app/home/home.component.ts @@ -1,12 +1,12 @@ import {AuthService, LocalizationPipe} from '@abp/ng.core'; import { Component, inject } from '@angular/core'; -import {CommonModule} from "@angular/common"; +import {NgTemplateOutlet} from "@angular/common"; @Component({ selector: 'app-home', templateUrl: './home.component.html', styleUrls: ['./home.component.scss'], - imports: [CommonModule, LocalizationPipe] + imports: [NgTemplateOutlet, LocalizationPipe] }) export class HomeComponent { private authService = inject(AuthService); diff --git a/templates/app/angular/tsconfig.json b/templates/app/angular/tsconfig.json index 29df0877f5..eeb0e33782 100644 --- a/templates/app/angular/tsconfig.json +++ b/templates/app/angular/tsconfig.json @@ -11,10 +11,11 @@ "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", - "module": "es2020", + "module": "esnext", "skipLibCheck": true, + "esModuleInterop": true, "lib": [ - "es2018", + "es2020", "dom" ], "paths": { diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationAutoMapperProfile.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationAutoMapperProfile.cs deleted file mode 100644 index f3f7d14052..0000000000 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName; - -public class MyProjectNameApplicationAutoMapperProfile : Profile -{ - public MyProjectNameApplicationAutoMapperProfile() - { - /* You can configure your AutoMapper mapping configuration here. - * Alternatively, you can split your mapping configurations - * into multiple profile classes for a better organization. */ - } -} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationMappers.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationMappers.cs new file mode 100644 index 0000000000..fa87ca82bd --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationMappers.cs @@ -0,0 +1,12 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName; + +[Mapper] +public partial class MyProjectNameApplicationMappers +{ + /* You can configure your Mapperly mapping configuration here. + * Alternatively, you can split your mapping configurations + * into multiple mapper classes for a better organization. */ +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs index 12ba3bdc25..081d167b6d 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs @@ -1,11 +1,12 @@ using Volo.Abp.Account; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.FeatureManagement; using Volo.Abp.Identity; using Volo.Abp.Modularity; using Volo.Abp.PermissionManagement; using Volo.Abp.SettingManagement; using Volo.Abp.TenantManagement; +using Microsoft.Extensions.DependencyInjection; namespace MyCompanyName.MyProjectName; @@ -23,9 +24,6 @@ public class MyProjectNameApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => - { - options.AddMaps(); - }); + context.Services.AddMapperlyObjectMapper(); } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj index 1f70aa544e..add85fe9f0 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj @@ -49,7 +49,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorAutoMapperProfile.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorAutoMapperProfile.cs deleted file mode 100644 index 9e5092bfe7..0000000000 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.Blazor.Client; - -public class MyProjectNameBlazorAutoMapperProfile : Profile -{ - public MyProjectNameBlazorAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Blazor project. - } -} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorClientModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorClientModule.cs index 035c9ef835..13a9d2694b 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorClientModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorClientModule.cs @@ -10,7 +10,7 @@ using OpenIddict.Abstractions; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; using Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme; using Volo.Abp.Autofac.WebAssembly; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Identity.Blazor.WebAssembly; using Volo.Abp.Modularity; using Volo.Abp.SettingManagement.Blazor.WebAssembly; @@ -39,7 +39,8 @@ public class MyProjectNameBlazorClientModule : AbpModule ConfigureBlazorise(context); ConfigureRouter(context); ConfigureMenu(context); - ConfigureAutoMapper(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureRouter(ServiceConfigurationContext context) @@ -87,12 +88,4 @@ public class MyProjectNameBlazorClientModule : AbpModule BaseAddress = new Uri(environment.BaseAddress) }); } - - private void ConfigureAutoMapper(ServiceConfigurationContext context) - { - Configure(options => - { - options.AddMaps(); - }); - } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorMappers.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorMappers.cs new file mode 100644 index 0000000000..81e4271bc8 --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyProjectNameBlazorMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Blazor.Client; + +[Mapper] +public partial class MyProjectNameBlazorMappers +{ + //Define your Mapperly configuration here for the Blazor project. +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj index a8494c994e..586ba24a4d 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj @@ -26,7 +26,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs index 3414c945a3..4d94c3bde9 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyProjectNameBlazorModule.cs @@ -38,7 +38,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.DistributedLocking; @@ -108,7 +108,6 @@ public class MyProjectNameBlazorModule : AbpModule ConfigureBundles(); ConfigureMultiTenancy(); ConfigureAuthentication(context, configuration); - ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureBlazorise(context); ConfigureRouter(context); @@ -116,6 +115,8 @@ public class MyProjectNameBlazorModule : AbpModule ConfigureDataProtection(context, configuration, hostingEnvironment); ConfigureDistributedLocking(context, configuration); ConfigureSwaggerServices(context.Services); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureUrls(IConfiguration configuration) @@ -298,15 +299,6 @@ public class MyProjectNameBlazorModule : AbpModule options.AppAssembly = typeof(MyProjectNameBlazorModule).Assembly; }); } - - private void ConfigureAutoMapper() - { - Configure(options => - { - options.AddMaps(); - }); - } - private void ConfigureSwaggerServices(IServiceCollection services) { services.AddAbpSwaggerGen( diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index 72835cb34d..a16dfa637a 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -25,7 +25,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameBlazorModule.cs index 6fb111d98d..5d27bb122f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameBlazorModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyProjectNameBlazorModule.cs @@ -32,7 +32,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.MultiTenancy; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Identity.Blazor.Server; using Volo.Abp.Modularity; using Volo.Abp.Security.Claims; @@ -121,13 +121,14 @@ public class MyProjectNameBlazorModule : AbpModule ConfigureAuthentication(context); ConfigureUrls(configuration); ConfigureBundles(); - ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureSwaggerServices(context.Services); ConfigureAutoApiControllers(); ConfigureBlazorise(context); ConfigureRouter(context); ConfigureMenu(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureAuthentication(ServiceConfigurationContext context) @@ -244,14 +245,6 @@ public class MyProjectNameBlazorModule : AbpModule }); } - private void ConfigureAutoMapper() - { - Configure(options => - { - options.AddMaps(); - }); - } - public override void OnApplicationInitialization(ApplicationInitializationContext context) { var env = context.GetEnvironment(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorAutoMapperProfile.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorAutoMapperProfile.cs deleted file mode 100644 index 4ec4dfb7bb..0000000000 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.Blazor.WebApp.Client; - -public class MyProjectNameBlazorAutoMapperProfile : Profile -{ - public MyProjectNameBlazorAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Blazor project. - } -} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorClientModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorClientModule.cs index e2de628db7..d3e28b8e23 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorClientModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorClientModule.cs @@ -11,7 +11,7 @@ using Volo.Abp.AspNetCore.Components.Web; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; using Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme; using Volo.Abp.Autofac.WebAssembly; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Identity.Blazor.WebAssembly; using Volo.Abp.Modularity; using Volo.Abp.SettingManagement.Blazor.WebAssembly; @@ -48,7 +48,8 @@ public class MyProjectNameBlazorClientModule : AbpModule ConfigureBlazorise(context); ConfigureRouter(context); ConfigureMenu(context); - ConfigureAutoMapper(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureRouter(ServiceConfigurationContext context) @@ -89,12 +90,4 @@ public class MyProjectNameBlazorClientModule : AbpModule BaseAddress = new Uri(environment.BaseAddress) }); } - - private void ConfigureAutoMapper(ServiceConfigurationContext context) - { - Configure(options => - { - options.AddMaps(); - }); - } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorMappers.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorMappers.cs new file mode 100644 index 0000000000..2edcf5e58f --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyProjectNameBlazorMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Blazor.WebApp.Client; + +[Mapper] +public partial class MyProjectNameBlazorMappers +{ + //Define your Mapperly configuration here for the Blazor project. +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorAutoMapperProfile.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorAutoMapperProfile.cs deleted file mode 100644 index efd9d3cb61..0000000000 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client; - -public class MyProjectNameBlazorAutoMapperProfile : Profile -{ - public MyProjectNameBlazorAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Blazor project. - } -} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorClientModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorClientModule.cs index b0e4e4f67f..7f1f464aac 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorClientModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorClientModule.cs @@ -11,7 +11,7 @@ using Volo.Abp.AspNetCore.Components.Web; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; using Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXLiteTheme; using Volo.Abp.Autofac.WebAssembly; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Identity.Blazor.WebAssembly; using Volo.Abp.Modularity; using Volo.Abp.SettingManagement.Blazor.WebAssembly; @@ -48,7 +48,8 @@ public class MyProjectNameBlazorClientModule : AbpModule ConfigureBlazorise(context); ConfigureRouter(context); ConfigureMenu(context); - ConfigureAutoMapper(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureRouter(ServiceConfigurationContext context) @@ -88,12 +89,4 @@ public class MyProjectNameBlazorClientModule : AbpModule BaseAddress = new Uri(environment.BaseAddress) }); } - - private void ConfigureAutoMapper(ServiceConfigurationContext context) - { - Configure(options => - { - options.AddMaps(); - }); - } } diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorMappers.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorMappers.cs new file mode 100644 index 0000000000..fc64d2f482 --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyProjectNameBlazorMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client; + +[Mapper] +public partial class MyProjectNameBlazorMappers +{ + //Define your Mapperly configuration here for the Blazor project. +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj index fc22aa3126..321e09eecf 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj @@ -28,7 +28,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyProjectNameBlazorModule.cs index aa439fe0ff..0849b09d6d 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyProjectNameBlazorModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyProjectNameBlazorModule.cs @@ -41,7 +41,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.DistributedLocking; @@ -112,7 +112,6 @@ public class MyProjectNameBlazorModule : AbpModule ConfigureBundles(); ConfigureMultiTenancy(); ConfigureAuthentication(context, configuration); - ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureBlazorise(context); ConfigureRouter(context); @@ -307,14 +306,6 @@ public class MyProjectNameBlazorModule : AbpModule }); } - private void ConfigureAutoMapper() - { - Configure(options => - { - options.AddMaps(); - }); - } - private void ConfigureSwaggerServices(IServiceCollection services) { services.AddAbpSwaggerGen( diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj index 76e388e05a..661b04fd57 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj @@ -26,7 +26,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyProjectNameBlazorModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyProjectNameBlazorModule.cs index a47a16406f..6a75e85ae0 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyProjectNameBlazorModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyProjectNameBlazorModule.cs @@ -34,7 +34,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite; using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite.Bundling; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Identity.Blazor.Server; using Volo.Abp.Modularity; using Volo.Abp.OpenIddict; @@ -125,13 +125,14 @@ public class MyProjectNameBlazorModule : AbpModule ConfigureAuthentication(context); ConfigureUrls(configuration); ConfigureBundles(); - ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureSwaggerServices(context.Services); ConfigureAutoApiControllers(); ConfigureBlazorise(context); ConfigureRouter(context); ConfigureMenu(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureAuthentication(ServiceConfigurationContext context) @@ -252,14 +253,6 @@ public class MyProjectNameBlazorModule : AbpModule }); } - private void ConfigureAutoMapper() - { - Configure(options => - { - options.AddMaps(); - }); - } - public override void OnApplicationInitialization(ApplicationInitializationContext context) { var env = context.GetEnvironment(); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250611122258_Initial.Designer.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250717081721_Initial.Designer.cs similarity index 99% rename from templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250611122258_Initial.Designer.cs rename to templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250717081721_Initial.Designer.cs index 5a7190317f..4ee8dc1ff3 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250611122258_Initial.Designer.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250717081721_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Migrations { [DbContext(typeof(MyProjectNameDbContext))] - [Migration("20250611122258_Initial")] + [Migration("20250717081721_Initial")] partial class Initial { /// @@ -1285,6 +1285,9 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1508,8 +1511,8 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250611122258_Initial.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250717081721_Initial.cs similarity index 99% rename from templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250611122258_Initial.cs rename to templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250717081721_Initial.cs index ec2bf59db0..579fe19395 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250611122258_Initial.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/20250717081721_Initial.cs @@ -449,6 +449,7 @@ namespace MyCompanyName.MyProjectName.Migrations RedirectUris = table.Column(type: "nvarchar(max)", nullable: true), Requirements = table.Column(type: "nvarchar(max)", nullable: true), Settings = table.Column(type: "nvarchar(max)", nullable: true), + FrontChannelLogoutUri = table.Column(type: "nvarchar(max)", nullable: true), ClientUri = table.Column(type: "nvarchar(max)", nullable: true), LogoUri = table.Column(type: "nvarchar(max)", nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), @@ -788,7 +789,7 @@ namespace MyCompanyName.MyProjectName.Migrations ReferenceId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), - Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Type = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/MyProjectNameDbContextModelSnapshot.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/MyProjectNameDbContextModelSnapshot.cs index 8fb9ea0266..51c2b336bc 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/MyProjectNameDbContextModelSnapshot.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/Migrations/MyProjectNameDbContextModelSnapshot.cs @@ -1282,6 +1282,9 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1505,8 +1508,8 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyCompanyName.MyProjectName.HttpApi.HostWithIds.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyCompanyName.MyProjectName.HttpApi.HostWithIds.csproj index f1f76659b6..d9c527fe34 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyCompanyName.MyProjectName.HttpApi.HostWithIds.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyCompanyName.MyProjectName.HttpApi.HostWithIds.csproj @@ -24,7 +24,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj index 4a6970ac19..c802138f7b 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj @@ -26,7 +26,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebAutoMapperProfile.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebAutoMapperProfile.cs deleted file mode 100644 index eea8d4cb05..0000000000 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.Web; - -public class MyProjectNameWebAutoMapperProfile : Profile -{ - public MyProjectNameWebAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Web project. - } -} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebMappers.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebMappers.cs new file mode 100644 index 0000000000..4ef965f932 --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Web.Host; + +[Mapper] +public partial class MyProjectNameWebMappers +{ + //Define your Mapperly configuration here for the Web project. +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs index 9c2db7971c..152076d235 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebModule.cs @@ -28,7 +28,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared.Toolbars; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.DistributedLocking; @@ -92,11 +92,12 @@ public class MyProjectNameWebModule : AbpModule ConfigureDistributedLocking(context, configuration); ConfigureUrls(configuration); ConfigureAuthentication(context, configuration); - ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureNavigationServices(configuration); ConfigureMultiTenancy(); ConfigureSwaggerServices(context.Services); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureBundles() @@ -217,14 +218,6 @@ public class MyProjectNameWebModule : AbpModule }); } - private void ConfigureAutoMapper() - { - Configure(options => - { - options.AddMaps(); - }); - } - private void ConfigureVirtualFileSystem(IWebHostEnvironment hostingEnvironment) { if (hostingEnvironment.IsDevelopment()) diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj index b97fd05460..19dfb6cf70 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj @@ -47,7 +47,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebAutoMapperProfile.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebAutoMapperProfile.cs deleted file mode 100644 index eea8d4cb05..0000000000 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.Web; - -public class MyProjectNameWebAutoMapperProfile : Profile -{ - public MyProjectNameWebAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Web project. - } -} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebMappers.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebMappers.cs new file mode 100644 index 0000000000..b7b6450bb3 --- /dev/null +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Web; + +[Mapper] +public partial class MyProjectNameWebMappers +{ + //Define your Mapperly configuration here for the Web project. +} diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs index bec50e72eb..27f8f93367 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs @@ -25,7 +25,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.LeptonXLite.Bundling; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.FeatureManagement; using Volo.Abp.Identity.Web; using Volo.Abp.Localization; @@ -107,11 +107,12 @@ public class MyProjectNameWebModule : AbpModule ConfigureAuthentication(context); ConfigureUrls(configuration); ConfigureBundles(); - ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureNavigationServices(); ConfigureAutoApiControllers(); ConfigureSwaggerServices(context.Services); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureAuthentication(ServiceConfigurationContext context) @@ -145,14 +146,6 @@ public class MyProjectNameWebModule : AbpModule }); } - private void ConfigureAutoMapper() - { - Configure(options => - { - options.AddMaps(); - }); - } - private void ConfigureVirtualFileSystem(IWebHostEnvironment hostingEnvironment) { if (hostingEnvironment.IsDevelopment()) diff --git a/templates/module/angular/angular.json b/templates/module/angular/angular.json index b6145d12f1..02fd552ffe 100644 --- a/templates/module/angular/angular.json +++ b/templates/module/angular/angular.json @@ -58,12 +58,12 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:browser", + "builder": "@angular-devkit/build-angular:application", "options": { "outputPath": "dist/dev-app", "index": "projects/dev-app/src/index.html", - "main": "projects/dev-app/src/main.ts", - "polyfills": "projects/dev-app/src/polyfills.ts", + "browser": "projects/dev-app/src/main.ts", + "polyfills": ["projects/dev-app/src/polyfills.ts"], "tsConfig": "projects/dev-app/tsconfig.app.json", "inlineStyleLanguage": "scss", "allowedCommonJsDependencies": ["chart.js", "js-sha256"], @@ -131,12 +131,9 @@ "outputHashing": "all" }, "development": { - "buildOptimizer": false, "optimization": false, - "vendorChunk": true, "extractLicenses": false, - "sourceMap": true, - "namedChunks": true + "sourceMap": true } }, "defaultConfiguration": "production" @@ -162,8 +159,8 @@ "test": { "builder": "@angular-devkit/build-angular:karma", "options": { - "main": "projects/dev-app/src/test.ts", - "polyfills": "projects/dev-app/src/polyfills.ts", + "browser": "projects/dev-app/src/test.ts", + "polyfills": ["projects/dev-app/src/polyfills.ts"], "tsConfig": "projects/dev-app/tsconfig.spec.json", "karmaConfig": "projects/dev-app/karma.conf.js", "inlineStyleLanguage": "scss", diff --git a/templates/module/angular/tsconfig.prod.json b/templates/module/angular/tsconfig.prod.json index bd2235775b..28b0a75f51 100644 --- a/templates/module/angular/tsconfig.prod.json +++ b/templates/module/angular/tsconfig.prod.json @@ -9,10 +9,11 @@ "experimentalDecorators": true, "moduleResolution": "node", "importHelpers": true, - "target": "es2017", - "module": "es2020", + "target": "es2020", + "module": "esnext", + "esModuleInterop": true, "lib": [ - "es2018", + "es2020", "dom" ], }, diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250611122321_Initial.Designer.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250717081803_Initial.Designer.cs similarity index 99% rename from templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250611122321_Initial.Designer.cs rename to templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250717081803_Initial.Designer.cs index 2566ba0bb8..0d0867486b 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250611122321_Initial.Designer.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250717081803_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Migrations { [DbContext(typeof(AuthServerDbContext))] - [Migration("20250611122321_Initial")] + [Migration("20250717081803_Initial")] partial class Initial { /// @@ -1229,6 +1229,9 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1452,8 +1455,8 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250611122409_Initial.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250717081803_Initial.cs similarity index 99% rename from templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250611122409_Initial.cs rename to templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250717081803_Initial.cs index 12a56494ee..d4990d59a7 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/Migrations/20250611122409_Initial.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/20250717081803_Initial.cs @@ -427,6 +427,7 @@ namespace MyCompanyName.MyProjectName.Migrations RedirectUris = table.Column(type: "nvarchar(max)", nullable: true), Requirements = table.Column(type: "nvarchar(max)", nullable: true), Settings = table.Column(type: "nvarchar(max)", nullable: true), + FrontChannelLogoutUri = table.Column(type: "nvarchar(max)", nullable: true), ClientUri = table.Column(type: "nvarchar(max)", nullable: true), LogoUri = table.Column(type: "nvarchar(max)", nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), @@ -766,7 +767,7 @@ namespace MyCompanyName.MyProjectName.Migrations ReferenceId = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: true), Status = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), Subject = table.Column(type: "nvarchar(400)", maxLength: 400, nullable: true), - Type = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: true), + Type = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: true), ExtraProperties = table.Column(type: "nvarchar(max)", nullable: false), ConcurrencyStamp = table.Column(type: "nvarchar(40)", maxLength: 40, nullable: false) }, diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/AuthServerDbContextModelSnapshot.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/AuthServerDbContextModelSnapshot.cs index 8c0f1e1e4c..a9dc5b7efa 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/AuthServerDbContextModelSnapshot.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/Migrations/AuthServerDbContextModelSnapshot.cs @@ -1226,6 +1226,9 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(max)") .HasColumnName("ExtraProperties"); + b.Property("FrontChannelLogoutUri") + .HasColumnType("nvarchar(max)"); + b.Property("IsDeleted") .ValueGeneratedOnAdd() .HasColumnType("bit") @@ -1449,8 +1452,8 @@ namespace MyCompanyName.MyProjectName.Migrations .HasColumnType("nvarchar(400)"); b.Property("Type") - .HasMaxLength(50) - .HasColumnType("nvarchar(50)"); + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); b.HasKey("Id"); diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostAutoMapperProfile.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostAutoMapperProfile.cs deleted file mode 100644 index 1552e364ff..0000000000 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.Blazor.Host.Client; - -public class MyProjectNameBlazorHostAutoMapperProfile : Profile -{ - public MyProjectNameBlazorHostAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Blazor project. - } -} diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostClientModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostClientModule.cs index 69bfb87723..f8f1805ea1 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostClientModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostClientModule.cs @@ -11,7 +11,7 @@ using Volo.Abp.Account; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; using Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme; using Volo.Abp.Autofac.WebAssembly; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Identity.Blazor.WebAssembly; using Volo.Abp.Modularity; using Volo.Abp.SettingManagement.Blazor.WebAssembly; @@ -41,7 +41,8 @@ public class MyProjectNameBlazorHostClientModule : AbpModule ConfigureBlazorise(context); ConfigureRouter(context); ConfigureMenu(context); - ConfigureAutoMapper(context); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureRouter(ServiceConfigurationContext context) @@ -83,12 +84,4 @@ public class MyProjectNameBlazorHostClientModule : AbpModule BaseAddress = new Uri(environment.BaseAddress) }); } - - private void ConfigureAutoMapper(ServiceConfigurationContext context) - { - Configure(options => - { - options.AddMaps(); - }); - } } diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostMappers.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostMappers.cs new file mode 100644 index 0000000000..71b072cd93 --- /dev/null +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyProjectNameBlazorHostMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Blazor.Host.Client; + +[Mapper] +public partial class MyProjectNameBlazorHostMappers +{ + //Define your Mapperly configuration here for the Blazor project. +} diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250611122311_Initial.Designer.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250717081744_Initial.Designer.cs similarity index 99% rename from templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250611122311_Initial.Designer.cs rename to templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250717081744_Initial.Designer.cs index 9b8b4d619a..956e6e89d1 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250611122311_Initial.Designer.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250717081744_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Blazor.Server.Host.Migrations { [DbContext(typeof(UnifiedDbContext))] - [Migration("20250611122311_Initial")] + [Migration("20250717081744_Initial")] partial class Initial { /// diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250611122311_Initial.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250717081744_Initial.cs similarity index 100% rename from templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250611122311_Initial.cs rename to templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/Migrations/20250717081744_Initial.cs diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250611122327_Initial.Designer.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250717081812_Initial.Designer.cs similarity index 96% rename from templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250611122327_Initial.Designer.cs rename to templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250717081812_Initial.Designer.cs index 754d85b5a2..3cd1db524a 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250611122327_Initial.Designer.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250717081812_Initial.Designer.cs @@ -12,7 +12,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Migrations { [DbContext(typeof(MyProjectNameHttpApiHostMigrationsDbContext))] - [Migration("20250611122327_Initial")] + [Migration("20250717081812_Initial")] partial class Initial { /// diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250611122327_Initial.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250717081812_Initial.cs similarity index 100% rename from templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250611122327_Initial.cs rename to templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/Migrations/20250717081812_Initial.cs diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj index eb3d5c8d53..3ffec54e2c 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs index d53b3d90bc..61534b55c8 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyProjectNameHttpApiHostModule.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using IdentityModel; +using Duende.IdentityModel; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Cors; diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebAutoMapperProfile.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebAutoMapperProfile.cs deleted file mode 100644 index c6255b4bd1..0000000000 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebAutoMapperProfile.cs +++ /dev/null @@ -1,11 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName; - -public class MyProjectNameWebAutoMapperProfile : Profile -{ - public MyProjectNameWebAutoMapperProfile() - { - //Define your AutoMapper configuration here for the Web project. - } -} diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs index e90eddd122..8d912bcad4 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebHostModule.cs @@ -29,7 +29,7 @@ using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; using Volo.Abp.AspNetCore.Serilog; using Volo.Abp.Autofac; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Caching; using Volo.Abp.Caching.StackExchangeRedis; using Volo.Abp.FeatureManagement; @@ -101,11 +101,12 @@ public class MyProjectNameWebHostModule : AbpModule ConfigureCache(configuration); ConfigureUrls(configuration); ConfigureAuthentication(context, configuration); - ConfigureAutoMapper(); ConfigureVirtualFileSystem(hostingEnvironment); ConfigureSwaggerServices(context.Services); ConfigureMultiTenancy(); ConfigureDataProtection(context, configuration, hostingEnvironment); + + context.Services.AddMapperlyObjectMapper(); } private void ConfigureMenu(IConfiguration configuration) @@ -169,14 +170,6 @@ public class MyProjectNameWebHostModule : AbpModule }); } - private void ConfigureAutoMapper() - { - Configure(options => - { - options.AddMaps(); - }); - } - private void ConfigureVirtualFileSystem(IWebHostEnvironment hostingEnvironment) { if (hostingEnvironment.IsDevelopment()) diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebMappers.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebMappers.cs new file mode 100644 index 0000000000..4ef965f932 --- /dev/null +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyProjectNameWebMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Web.Host; + +[Mapper] +public partial class MyProjectNameWebMappers +{ + //Define your Mapperly configuration here for the Web project. +} diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250611122337_Initial.Designer.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250717081828_Initial.Designer.cs similarity index 99% rename from templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250611122337_Initial.Designer.cs rename to templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250717081828_Initial.Designer.cs index 2756075b5f..0746c415d6 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250611122337_Initial.Designer.cs +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250717081828_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace MyCompanyName.MyProjectName.Migrations { [DbContext(typeof(UnifiedDbContext))] - [Migration("20250611122337_Initial")] + [Migration("20250717081828_Initial")] partial class Initial { /// diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250611122337_Initial.cs b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250717081828_Initial.cs similarity index 100% rename from templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250611122337_Initial.cs rename to templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/Migrations/20250717081828_Initial.cs diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyCompanyName.MyProjectName.Application.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyCompanyName.MyProjectName.Application.csproj index df92d80ceb..bbb637edb3 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyCompanyName.MyProjectName.Application.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyCompanyName.MyProjectName.Application.csproj @@ -9,7 +9,7 @@ - + diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationAutoMapperProfile.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationAutoMapperProfile.cs deleted file mode 100644 index f3f7d14052..0000000000 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationAutoMapperProfile.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName; - -public class MyProjectNameApplicationAutoMapperProfile : Profile -{ - public MyProjectNameApplicationAutoMapperProfile() - { - /* You can configure your AutoMapper mapping configuration here. - * Alternatively, you can split your mapping configurations - * into multiple profile classes for a better organization. */ - } -} diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationMappers.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationMappers.cs new file mode 100644 index 0000000000..fa87ca82bd --- /dev/null +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationMappers.cs @@ -0,0 +1,12 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName; + +[Mapper] +public partial class MyProjectNameApplicationMappers +{ + /* You can configure your Mapperly mapping configuration here. + * Alternatively, you can split your mapping configurations + * into multiple mapper classes for a better organization. */ +} diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs index d8fefafb98..93ca04edf6 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Application/MyProjectNameApplicationModule.cs @@ -1,5 +1,5 @@ using Microsoft.Extensions.DependencyInjection; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.Application; @@ -9,16 +9,12 @@ namespace MyCompanyName.MyProjectName; typeof(MyProjectNameDomainModule), typeof(MyProjectNameApplicationContractsModule), typeof(AbpDddApplicationModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class MyProjectNameApplicationModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddMaps(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); } } diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj index 870ca204dd..5ab7cf5e98 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj @@ -8,7 +8,7 @@ - + diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorAutoMapperProfile.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorAutoMapperProfile.cs deleted file mode 100644 index 13deb63b1d..0000000000 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorAutoMapperProfile.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.Blazor; - -public class MyProjectNameBlazorAutoMapperProfile : Profile -{ - public MyProjectNameBlazorAutoMapperProfile() - { - /* You can configure your AutoMapper mapping configuration here. - * Alternatively, you can split your mapping configurations - * into multiple profile classes for a better organization. */ - } -} diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorMappers.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorMappers.cs new file mode 100644 index 0000000000..a37e5da245 --- /dev/null +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorMappers.cs @@ -0,0 +1,12 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Blazor; + +[Mapper] +public partial class MyProjectNameBlazorMappers +{ + /* You can configure your Mapperly mapping configuration here. + * Alternatively, you can split your mapping configurations + * into multiple mapper classes for a better organization. */ +} diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorModule.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorModule.cs index dd7bf654ee..1dcb64b5c5 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorModule.cs +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyProjectNameBlazorModule.cs @@ -2,7 +2,7 @@ using MyCompanyName.MyProjectName.Blazor.Menus; using Volo.Abp.AspNetCore.Components.Web.Theming; using Volo.Abp.AspNetCore.Components.Web.Theming.Routing; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.UI.Navigation; @@ -11,18 +11,13 @@ namespace MyCompanyName.MyProjectName.Blazor; [DependsOn( typeof(MyProjectNameApplicationContractsModule), typeof(AbpAspNetCoreComponentsWebThemingModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class MyProjectNameBlazorModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - context.Services.AddAutoMapperObjectMapper(); - - Configure(options => - { - options.AddProfile(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj index c11df1a518..7102f1935f 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj @@ -13,7 +13,7 @@ - + diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebAutoMapperProfile.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebAutoMapperProfile.cs deleted file mode 100644 index 08b573de75..0000000000 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebAutoMapperProfile.cs +++ /dev/null @@ -1,13 +0,0 @@ -using AutoMapper; - -namespace MyCompanyName.MyProjectName.Web; - -public class MyProjectNameWebAutoMapperProfile : Profile -{ - public MyProjectNameWebAutoMapperProfile() - { - /* You can configure your AutoMapper mapping configuration here. - * Alternatively, you can split your mapping configurations - * into multiple profile classes for a better organization. */ - } -} diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebMappers.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebMappers.cs new file mode 100644 index 0000000000..b7b6450bb3 --- /dev/null +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebMappers.cs @@ -0,0 +1,10 @@ +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace MyCompanyName.MyProjectName.Web; + +[Mapper] +public partial class MyProjectNameWebMappers +{ + //Define your Mapperly configuration here for the Web project. +} diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs index 9e139a0515..4b1876a61d 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyProjectNameWebModule.cs @@ -4,7 +4,7 @@ using MyCompanyName.MyProjectName.Localization; using MyCompanyName.MyProjectName.Web.Menus; using Volo.Abp.AspNetCore.Mvc.Localization; using Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared; -using Volo.Abp.AutoMapper; +using Volo.Abp.Mapperly; using Volo.Abp.Modularity; using Volo.Abp.UI.Navigation; using Volo.Abp.VirtualFileSystem; @@ -15,7 +15,7 @@ namespace MyCompanyName.MyProjectName.Web; [DependsOn( typeof(MyProjectNameApplicationContractsModule), typeof(AbpAspNetCoreMvcUiThemeSharedModule), - typeof(AbpAutoMapperModule) + typeof(AbpMapperlyModule) )] public class MyProjectNameWebModule : AbpModule { @@ -44,11 +44,7 @@ public class MyProjectNameWebModule : AbpModule options.FileSets.AddEmbedded(); }); - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddMaps(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); Configure(options => { diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/ClientDemoService.cs b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/ClientDemoService.cs index 8290b2df36..95b0fd717d 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/ClientDemoService.cs +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/ClientDemoService.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Microsoft.Extensions.Configuration; using MyCompanyName.MyProjectName.Samples; using Volo.Abp.DependencyInjection;