diff --git a/.github/ISSUE_TEMPLATE/02_feature_request.yml b/.github/ISSUE_TEMPLATE/02_feature_request.yml index 21189e3a30..029830c7c5 100644 --- a/.github/ISSUE_TEMPLATE/02_feature_request.yml +++ b/.github/ISSUE_TEMPLATE/02_feature_request.yml @@ -1,6 +1,6 @@ name: 💡 Feature request description: Suggest an idea for this project -labels: [feature] +labels: [feature-request] body: - type: checkboxes attributes: diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..43e9cba5de --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,14 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 7.x.x | :white_check_mark: | +| < 7.0.0 | :x: | + +## Reporting a Vulnerability + +Please don not share vulnerabilities publicly in GitHub or other platforms. +You can report security issues by sending a email to `security@abp.io` +Your report is immediately evaluated. We publish patch versions for critical vulnerabilities in a week at most. diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/ar.json index 4a7e64125f..d3537d3742 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/ar.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/ar.json @@ -167,7 +167,7 @@ "ABPDiscordServer": "ABP سيرفر الدسكورد", "ABPCommunityTalks": "برامج منتدى ABP الحوارية", "ABPCommunityPosts": "منشورات منتدى ABP", - "BuyAndGetMonths": "شراء 12 شهر، احصل على 14 شهرا!", + "BuyAndGetMonths": "شراء 12 شهر، احصل على 14 شهرا!", "GetYourDeal": "احصل على صفقتك", "BuyOrRenewLicense": "اشترِ أو جدد الرخصة الآن واحصل على شهرين إضافيين!", "BuyOrRenewLicenseToGetExtra2Months": "اشترِ أو جدد الرخصة الآن واحصل على شهرين إضافيين! اسرع! ⏰ آخر يوم: {0}", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json index 9ef432a8bd..63f78103d9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json @@ -171,7 +171,7 @@ "ABPDiscordServer": "ABP Discord Server", "ABPCommunityTalks": "ABP Community Talks", "ABPCommunityPosts": "ABP Community Posts", - "BuyAndGetMonths": "BUY 12 MONTHS, GET 14 MONTHS!", + "BuyAndGetMonths": "BUY 12 MONTHS, GET 14 MONTHS!", "GetYourDeal": "Get Your Deal", "BuyOrRenewLicense": "Buy or Renew License Now and Get 2 Extra Months!", "BuyOrRenewLicenseToGetExtra2Months": "Buy or Renew License Now and Get 2 Extra Months! HURRY UP! ⏰ Last Day: {0}", @@ -187,7 +187,7 @@ "GiveAwayForNewPurchases": "Application Development Classroom Training will be given away for the new purchases!", "BlackFriday": "BLACK FRIDAY", "ValidForExistingCustomers": "Also valid for the
existing customers!", - "CampaignBetweenDates": "From {0}
to {1}", + "CampaignBetweenDates": "From {0}
To {1}", "SaveUpTo": "SAVE UP TO${0}K", "ImplementingDDD": "Implementing Domain Driven Design", "ExploreTheEBook": "Explore the E-Book", @@ -220,6 +220,7 @@ "NoContent": "No content", "More": "More", "WhyABPIOPlatform": "Why ABP.IO Platform?", - "AbpStudio": "ABP Studio" + "AbpStudio": "ABP Studio", + "ExtraMonths": "{0}EXTRA MONTHS" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/fi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/fi.json index 16437480ef..80f351043f 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/fi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/fi.json @@ -170,7 +170,7 @@ "ABPDiscordServer": "ABP Discord-palvelin", "ABPCommunityTalks": "ABP Community Talks", "ABPCommunityPosts": "ABP-yhteisön viestit", - "BuyAndGetMonths": "OSTA 12 KUUKAUTA, SAAT 14 KUUKAUTA!", + "BuyAndGetMonths": "OSTA 12 KUUKAUTA, SAAT 14 KUUKAUTA!", "GetYourDeal": "Hanki tarjouksesi", "BuyOrRenewLicense": "Osta tai uusi lisenssi nyt ja saat 2 lisäkuukautta!", "BuyOrRenewLicenseToGetExtra2Months": "Osta tai uusi lisenssi nyt ja saat 2 lisäkuukautta! KIIREHDI! ⏰ Viimeinen päivä: {0}", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/hu.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/hu.json index b9ccddecf4..d785f418aa 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/hu.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/hu.json @@ -168,7 +168,7 @@ "ABPDiscordServer": "ABP Discord szerver", "ABPCommunityTalks": "ABP közösségi beszélgetések", "ABPCommunityPosts": "ABP közösségi bejegyzések", - "BuyAndGetMonths": "VÁSÁROLJON 12 HÓNAPOT, 14 HÓNAPOT KAP!", + "BuyAndGetMonths": "VÁSÁROLJON 12 HÓNAPOT, 14 HÓNAPOT KAP!", "GetYourDeal": "Szerezze meg az ajánlatát", "BuyOrRenewLicense": "Vásároljon vagy újítson meg licencet most, és 2 további hónapot kap!", "BuyOrRenewLicenseToGetExtra2Months": "Vásároljon vagy újítson meg licencet most, és 2 további hónapot kap! SIESS! ⏰ Utolsó nap: {0}", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json index d1a0db7176..545a909abf 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/zh-Hans.json @@ -170,7 +170,7 @@ "ABPDiscordServer": "ABP Discord 服务器", "ABPCommunityTalks": "ABP社区讲话", "ABPCommunityPosts": "ABP社区文章", - "BuyAndGetMonths": "购买 12 个月,获得 14 个月!", + "BuyAndGetMonths": "购买 12 个月,获得 14 个月!", "GetYourDeal": "得到你的交易", "BuyOrRenewLicense": "立即购买或续订许可证并额外获得 2 个月!", "BuyOrRenewLicenseToGetExtra2Months": "立即购买或续订 ABP 商业许可证(适用于所有版本)并额外获得 2 个月!", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 1b8c972d5c..d9794a06e4 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -567,7 +567,7 @@ "TotalPrice": "Total Price", "ThereIsNoInvoice": "There is no invoice", "MyOrganizations_Detail_PaymentProviderInfo": "If you have purchased your license through {0} gateway, it sends the PDF invoice to your email address, see {0} invoicing.", - "MyOrganizations_Detail_PayUInfo": "If you have purchased through the PayU gateway, click the \"Request Invoice\" button and fill in the billing information.", + "MyOrganizations_Detail_PayUInfo": "If you have purchased through the Iyzico gateway, click the \"Request Invoice\" button and fill in the billing information.", "MyOrganizations_Detail_ConclusionInfo": "Your invoice request will be concluded within {0} business days.", "ExtendYourLicense": "Extend your {0} license", "Continue": "Continue", @@ -1001,19 +1001,19 @@ "ABPSOLUTION": "ABP SOLUTION", "CreatingAnEmptySolution_ABPSOLUTION_Description": "ABP provides a well-architected, layered and production-ready startup solution based on the Domain Driven Design principles. The solution also includes a pre-configured unit and integration test projects for each layer.", "CommonLibraries": "Common Libraries", - "CommonLibraries_THEPROBLEM_Description": "Which libraries should you use to implement common requirements? The software development ecosystem is highly dynamic and it is hard to follow the latest tools, libraries, trends and approaches.", - "CommonLibraries_ABPSOLUTION_Description": "ABP pre-integrates the popular, mature and up-to-date libraries into the solution. You don't spend time integrating them and talking to each other. They properly work out of the box.", + "CommonLibraries_THEPROBLEM_Description": "Which libraries should you use to implement common requirements? The software development ecosystem is highly dynamic, making it challenging to keep up with the latest tools, libraries, trends, and approaches.", + "CommonLibraries_ABPSOLUTION_Description": "ABP pre-integrates popular, mature, and up-to-date libraries into the solution. You don't need to spend time integrating them or making them communicate with each other. They work properly out of the box.", "UITheme&Layout": "UI Theme & Layout", - "UITheme&Layout_THEPROBLEM_Description": "When it comes to the UI, there are a lot of challenges, including preparing a foundation to create a responsive, modern and flexible UI kit with a consistent look & feel and tons of features (like left/top navigation menu, header, toolbar, footer, widgets and so.).", - "UITheme&Layout_THEPROBLEM_Description2": "Even if you buy a pre-built theme, integrating it into your solution may take days of development. Upgrading such a theme is another problem. Most of the time, the theme's HTML/CSS structure is mixed with your UI code, and it is not easy to upgrade or change the theme later.", - "UITheme&Layout_ABPSOLUTION_Description": "ABP Framework provides a theming system that makes your UI code independent from the theme. Themes are isolated, and they are NuGet/NPM packages. Installing or upgrading a theme is just a minute. While you can build your theme (or integrate an existing theme), ABP Commercial offers professional and modern themes.", - "UITheme&Layout_ABPSOLUTION_Description2": "There are also UI component providers (like Telerik and DevExpress). But they only provide individual components. You are responsible for creating your own layout system. You can use such libraries in your ABP-based solutions just like in any other project.", + "UITheme&Layout_THEPROBLEM_Description": "When addressing UI concerns, a range of challenges surfaces. These include establishing the groundwork for a responsive, contemporary, and adaptable UI kit with a consistent appearance and a host of features like navigation menus, headers, toolbars, footers, widgets, and more.", + "UITheme&Layout_THEPROBLEM_Description2": "Even if you opt for a pre-designed theme, seamlessly integrating it into your project could demand days of development. An additional hurdle lies in upgrading such themes. Frequently, the theme's HTML/CSS structure becomes intertwined with your UI code, rendering future theme changes or upgrades intricate tasks. This interweaving of code and design complicates the flexibility of making adjustments down the line.", + "UITheme&Layout_ABPSOLUTION_Description": "ABP Framework offers a distinctive theming system that liberates your UI code from theme constraints. Themes exist in isolation, packaged as NuGet or NPM packages, making theme installation or upgrades a matter of minutes. While you retain the option to develop your custom theme or integrate an existing one, ABP Commercial presents a collection of polished and contemporary themes.", + "UITheme&Layout_ABPSOLUTION_Description2": "Additionally, there are UI component providers like Telerik and DevExpress. However, these providers primarily furnish individual components, placing the onus on you to establish your layout system. When working within ABP-based projects, you can seamlessly incorporate these libraries, similar to how you would in any other project.", "TestInfrastructure": "Test Infrastructure", - "TestInfrastructure_THEPROBLEM_Description": "Preparing a robust test environment takes time. You need to setup test projects in your solution, select the tools, mock the services and database, create the required base classes and utility services to reduce repeating code in the tests and so on.", - "TestInfrastructure_ABPSOLUTION_Description": "ABP Startup Templates comes with the test projects already configured for you, and you can immediately write your first unit or integration test code on day 1.", + "TestInfrastructure_THEPROBLEM_Description": "Establishing a robust testing environment is a time-consuming endeavor. It involves setting up dedicated test projects within your solution, carefully selecting the necessary tools, creating service and database mocks, crafting essential base classes and utility services to minimize redundant code across tests, and addressing various related tasks.", + "TestInfrastructure_ABPSOLUTION_Description": "ABP Startup Templates arrive pre-equipped with configured test projects, streamlining the process for you. This means that from day one, you can readily commence writing your initial unit or integration test code without delay.", "CodingStandards&Training": "Coding Standards & Training", - "CodingStandards&Training_THEPROBLEM_Description": "Once you create the development-ready solution, you typically need to train the developers to explain the system and develop it with the same conventions in a standard and consistent way. Even if you train the developers, it is hard to prepare and maintain your documentation. Over time, every developer will write the code differently, and coding standards will begin to diverge.", - "CodingStandards&Training_ABPSOLUTION_Description": "ABP solution is already well-defined and well-documented. Tutorials and best practice guides clearly explain how to make development on an ABP project.", + "CodingStandards&Training_THEPROBLEM_Description": "After you've set up the solution for development, you usually have to teach the developers how the system works and how to build it using the same agreed-upon methods. Even if you give them training, keeping the documentation up-to-date can be difficult. As time goes on, each developer might write code in their own way, causing the rules for writing code to become different from each other.", + "CodingStandards&Training_ABPSOLUTION_Description": "The ABP solution is already neatly organized and has clear explanations. Step-by-step tutorials and guides show you exactly how to work on an ABP project.", "KeepingYourSolutionUpToDate": "Keeping Your Solution Up to Date", "KeepingYourSolutionUpToDate_THEPROBLEM_Description": "After you start your development, you must keep track of the new versions of the libraries you use for upgrades & patches.", "KeepingYourSolutionUpToDate_ABPSOLUTION_Description": "We regularly update all packages to the latest versions and test them before the stable release. When you update the ABP Framework, all its dependencies are upgraded to edge technology.", @@ -1073,6 +1073,8 @@ "ABPCommunity_Description2": "It is easy to share code or even re-usable libraries between ABP developers. A code snippet that works for you will also work for others. There are a lot of samples and tutorials that you can directly implement for your application.", "ABPCommunity_Description3": "When you hire a developer who worked before with the ABP architecture will immediately understand your solution and start development in a very short time.", "WhyAbpIo_Page_Title": "Why ABP.IO Platform?", - "AbpStudio_Page_Title": "ABP Studio" + "AbpStudio_Page_Title": "ABP Studio", + "CampaignInfo": "Buy a new license or renew your existing license and get an additional 2 months at no additional cost! This offer is valid for all license plans. Ensure you take advantage of this limited-time promotion to expand your access to premium features and upgrades.", + "HurryUpLastDay": "Hurry Up! Last Day: {0}" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json index e6b3cf46f7..0377689bfc 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -189,6 +189,7 @@ "SeeMoreVideos": "See more videos", "DiscordPageTitle": "ABP Discord Community", "ViewVideo": "View Video", - "AbpCommunityTitleContent": "ABP Community - Open Source ABP Framework" + "AbpCommunityTitleContent": "ABP Community - Open Source ABP Framework", + "CommunitySlogan": "A unique community platform for ABP Lovers" } } diff --git a/common.props b/common.props index 4d75a2c459..e956b17835 100644 --- a/common.props +++ b/common.props @@ -1,7 +1,7 @@ latest - 7.4.0-rc.2 + 8.0.0 $(NoWarn);CS1591;CS0436 https://abp.io/assets/abp_nupkg.png https://abp.io/ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/POST.md b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/POST.md new file mode 100644 index 0000000000..c7d8411886 --- /dev/null +++ b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/POST.md @@ -0,0 +1,287 @@ +# ABP.IO Platform 7.4 RC Has Been Released + +Today, we are happy to release the [ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) version **7.4 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 v7.4! Thanks to all of you. + +## Get Started with the 7.4 RC + +Follow the steps below to try version 7.4.0 RC today: + +1) **Upgrade** the ABP CLI to version `7.4.0-rc.1` using a command line terminal: + +````bash +dotnet tool update Volo.Abp.Cli -g --version 7.4.0-rc.1 +```` + +**or install** it if you haven't before: + +````bash +dotnet tool install Volo.Abp.Cli -g --version 7.4.0-rc.1 +```` + +2) Create a **new application** with the `--preview` option: + +````bash +abp new BookStore --preview +```` + +See the [ABP CLI documentation](https://docs.abp.io/en/abp/latest/CLI) for all the available options. + +> You can also use the [Get Started](https://abp.io/get-started) page to generate a CLI command to create a new application. + +You can use any IDE that supports .NET 7.x, like [Visual Studio 2022](https://visualstudio.microsoft.com/downloads/). + +## Migration Guides + +There are a few breaking changes in this version that may affect your application. +Please see the following migration documents, if you are upgrading from v7.3 or earlier: + +* [ABP Framework 7.3 to 7.4 Migration Guide](https://docs.abp.io/en/abp/7.4/Migration-Guides/Abp-7_4) + +## What's New with ABP Framework 7.4? + +In this section, I will introduce some major features released in this version. Here is a brief list of the titles that will be explained in the next sections: + +* Dynamic Setting Store +* Introducing the `AdditionalAssemblyAttribute` +* `CorrelationId` Support on Distributed Events +* Database Migration System for EF Core +* Other News + +### Dynamic Setting Store + +Prior to this version, it was hard to define settings in different microservices and centrally manage all setting definitions in a single admin application. To make that possible, we used to add project references for all the microservices' service contract packages from a single microservice, so it can know all the setting definitions and manage them. + +In this version, ABP Framework introduces the Dynamic Setting Store, which is an important feature that allows you to collect and get all setting definitions from a single point and overcome the setting management problems on microservices. + +> *Note*: If you are upgrading from an earlier version and using the Setting Management module, you need to create a new migration and apply it to your database because a new database table has been added for this feature. + +### Introducing the `AdditionalAssemblyAttribute` + +In this version, we have introduced the `AdditionalAssemblyAttribute` to define additional assemblies to be part of a module. ABP Framework automatically registers all the services of your module to the [Dependency Injection System](https://docs.abp.io/en/abp/latest/Dependency-Injection). It finds the service types by scanning types in the assembly that define your module class. Typically, every assembly contains a separate module class definition and modules depend on each other using the `DependsOn` attribute. + +In some rare cases, your module may consist of multiple assemblies and only one of them defines a module class, and you want to make the other assemblies parts of your module. This is especially useful if you can't define a module class in the target assembly or you don't want to depend on that module's dependencies. + +In that case, you can use the `AdditionalAssembly` attribute as shown below: + +```csharp +[DependsOn(...)] // Your module dependencies as you normally would do +[AdditionalAssembly(typeof(IdentityServiceModule))] // A type in the target assembly (in another assembly) +public class IdentityServiceTestModule : AbpModule +{ + ... +} +``` + +With the `AdditionalAssembly` attribute definition, ABP loads the assembly containing the `IdentityServiceModule` class as a part of the identity service module. Notice that in this case, none of the module dependencies of the `IdentityServiceModule` are loaded. Because we are not depending on the `IdentityServiceModule`, instead we are just adding its assembly as a part of the `IdentityServiceTestModule`. + +> You can check the [Module Development Basics](https://docs.abp.io/en/abp/7.4/Module-Development-Basics) documentation to learn more. + +### `CorrelationId` Support on Distributed Events + +In this version, `CorrelationId` (a unique key that is used in distributed applications to trace requests across multiple services/operations) is attached to the distributed events, so you can relate events with HTTP requests and can trace all the related activities. + +ABP Framework generates a `correlationId` for the first time when an operation is started and then attaches the current `correlationId` to distributed events as an additional property. For example, if you are using the [transactional outbox or inbox pattern provided by ABP Framework](https://docs.abp.io/en/abp/latest/Distributed-Event-Bus#outbox-inbox-for-transactional-events), you can see the `correlationId` in the extra properties of the `IncomingEventInfo` or `OutgoingEventInfo` classes with the standard `X-Correlation-Id` key. + +> You can check [this issue](https://github.com/abpframework/abp/issues/16773) for more information. + +### Database Migration System for EF Core + +In this version, ABP Framework provides base classes and events to migrate the database schema and seed the database on application startup. This system works compatibly with multi-tenancy and whenever a new tenant is created or a tenant's database connection string has been updated, it checks and applies database migrations for the new tenant state. + +This system is especially useful to migrate databases for microservices. In this way, when you deploy a new version of a microservice, you don't need to manually migrate its database. + +You need to take the following actions to use the database migration system: + +* Create a class that derives from `EfCoreRuntimeDatabaseMigratorBase` class, override and implement its `SeedAsync` method. And lastly, execute the `CheckAndApplyDatabaseMigrationsAsync` method of your class in the `OnPostApplicationInitializationAsync` method of your module class. +* Create a class that derives from `DatabaseMigrationEventHandlerBase` class, override and implement its `SeedAsync` method. Then, whenever a new tenant is created or a tenant's connection string is changed then the `SeedAsync` method will be executed. + +### Other News + +* [OpenIddict](https://github.com/openiddict/openiddict-core/tree/4.7.0) library has been upgraded to **v4.7.0**. See [#17334](https://github.com/abpframework/abp/pull/17334) for more info. +* ABP v7.4 introduces the `Volo.Abp.Maui.Client` package, which is used by the MAUI mobile application in ABP Commercial. See [#17201](https://github.com/abpframework/abp/pull/17201) for more info. +* In this version, the `AbpAspNetCoreIntegratedTestBase` class gets a generic type parameter, which expects either a startup class or an ABP module class. This allows us to use configurations from an ABP module or old-style ASP.NET Core Startup class in a test application class and this simplifies the test application project. See [#17039](https://github.com/abpframework/abp/pull/17039) for more info. + +## What's New with ABP Commercial 7.4? + +We've also worked on [ABP Commercial](https://commercial.abp.io/) to align the features and changes made in the ABP Framework. The following sections introduce new features coming with ABP Commercial 7.4. + +### Dynamic Text Template Store + +Prior to this version, it was hard to create text templates in different microservices and centrally manage them in a single admin application. For example, if you would define a text template in your ordering microservice, then those text templates could not be seen on the administration microservice because the administration microservice would not have any knowledge about that text template (because it's hard-coded in the ordering microservice). + +For this reason, in this version, the Dynamic Text Template Store has been introduced to make the [Text Template Management module](https://docs.abp.io/en/commercial/latest/modules/text-template-management) compatible with microservices and distributed systems. It allows you to store and get all text templates from a single point. Thanks to that, you can centrally manage the text templates in your admin application. + +> *Note*: If you are upgrading from an earlier version and are using the Text Template Management module, you need to create a new migration and apply it to your database. + +To enable the dynamic template store, you just need to configure the `TextTemplateManagementOptions` and set the `IsDynamicTemplateStoreEnabled` as true in your module class: + +```csharp +Configure(options => +{ + options.IsDynamicTemplateStoreEnabled = true; +}); +``` + +Notice this is only needed in the microservice where you centrally manage your text template contents. So, typically you would use the configuration above in your administration microservice. Other microservices automatically save their text template contents to the central database. + +### Suite: Custom Code Support + +In this version, we have implemented the custom code support in Suite. This allows you to customize the generated code-blocks and preserve your custom code changes in the next CRUD Page Generation in Suite. ABP Suite specifies hook-points to allow adding custom code blocks. Then, the code that you wrote to these hook points will be respected and will not be overridden in the next entity generation. + +![](suite-custom-code.png) + +To enable custom code support, you should check the *Customizable code* option in the crud page generation page. When you enable the custom code support, you will be seeing some hook-points in your application. + +For example, on the C# side, you'll be seeing some abstract classes and classes that derive from them (for entities, application services, interfaces, domain services, and so on...). You can write your custom code in those classes (`*.Extended.cs`) and the next time when you need to re-generate the entity, your custom code will not be overridden (only the base abstract classes will be re-generated and your changes on Suite will be respected): + +Folder structure | Book.Extended.cs +:-------------------------:|:-------------------------: +![](suite-custom-code-backend.png) | ![](book-extended-cs.png) + +> *Note*: If you want to override the entity and add custom code, please do not touch the code between `...` placeholders, because the constructor of the entity should be always re-generated in case of a new property added. + +On the UI side, you can see the *comment placeholders* on the pages for MVC & Blazor applications. These are hook-points provided by ABP Suite and you can write your custom code between these comment sections: + +Folder structure | Books/Index.cshtml +:-------------------------:|:-------------------------: +![](suite-custom-code-ui.png) | ![](book-extended-cshtml.png) + +### MAUI & React Native UI Revisions + +In this version, we have revised MAUI & React Native mobile applications and added new pages, functionalities and made improvements on the UI side. + +![](maui.png) + +For example, in the MAUI application, we have implemented the following functionalities and changed the UI completely: + +* **User Management Page**: Management page for your application users. You can search, add, update, or delete users of your application. +* **Tenants**: Management page for your tenants. +* **Settings**: Management page for your application settings. On this page, you can change **the current language**, **the profile picture**, **the current password**, or/and **the current theme**. + +Also, we have aligned the features on both of these mobile options (MAUI & React Native) and showed them in the ["ABP Community Talks 2023.5: Exploring the Options for Mobile Development with the ABP Framework"](https://community.abp.io/events/mobile-development-with-the-abp-framework-ogtwaz5l). + +> If you have missed the event, you can watch from 👉 [here](https://www.youtube.com/watch?v=-wrdngeKgZw). + +### New LeptonX Theme Features + +In the new version of LeptonX Theme, which is v2.4.0-rc.1, there are some new features that we want to mention. + +#### Mobile Toolbars + +The [Toolbar System](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Toolbars) is used to define *toolbars* on the user interface. Modules (or your application) can add items to a toolbar, then the UI themes can render the toolbar on the layout. + +LeptonX Theme extends this system even further and introduces mobile toolbars with this version. You can create a component and add it as a mobile toolbar as below: + +```csharp +public class MyToolbarContributor : IToolbarContributor +{ + public Task ConfigureToolbarAsync(IToolbarConfigurationContext context) + { + if (context.Toolbar.Name == LeptonXToolbars.MainMobile) + { + context.Toolbar.Items.Add(new ToolbarItem(typeof(ShoppingCardToolbarComponent))); + + //other mobile toolbars... + } + + return Task.CompletedTask; + } +} +``` + +Then, the LeptonX Theme will render these mobile toolbars like in the figure below: + +![](mobile-toolbars.png) + +> **Note**: The Angular UI hasn't been completed yet. We aim to complete it as soon as possible and include it in the next release. + +#### New Error Page Designs + +In this version, we have implemented new error pages. Encounter a fresh look during error situations with the 'New Error Page Designs,' providing informative and visually appealing error displays that enhance user experience: + +![](error-page.png) + +#### Fluid Layout + +In this version, LeptonX Theme introduces the fresh-looking **Fluid Layout**, which is a layout that lets you align elements so that they automatically adjust their alignment and proportions for different page sizes and orientations. + +![](fluid-layout.png) + +> You can visit [the live demo of LeptonX Theme](https://x.leptontheme.com/side-menu) and try the Fluid Layout now! + +### Check & Move Related Entities on Deletion/Demand + +In application modules, there are some entities that have complete relationships with each other such as role-user relations. In such cases, it's a typical requirement to check & move related entities that have a relation with the other entity that is about to be deleted. + +For example, if you need to delete an edition from your system, you would typically want to move the tenant that is associated with that edition. For this purpose, in this version, ABP Commercial allows you to move related entities on deletion/demand. + +![](editions.png) + +Currently, this feature is implemented for SaaS and Identity Pro modules and for the following relations: + +* Edition - Tenant +* Role - User +* Organization Unit - User + +Also, it's possible to move the related associated-records before deleting the record. For example, you can move all tenants from an edition as shown in the figure below: + +"Move all tenants" action | "Move all tenants" modal +:-------------------------:|:-------------------------: +![](move-all-tenants.png) | ![](move-tenants.png) + +### CMS Kit Pro: Page Feedback + +In this version, the **Page Feedback** feature has been added to the [CMS Kit Pro](https://docs.abp.io/en/commercial/latest/modules/cms-kit/index) module. This feature allows you to get feedback from a page in your application. + +This is especially useful if you have content that needs feedback from users. For example, if you have documentation or a blog website, it's a common requirement to assess the quality of the articles and get feedback from users. In that case, you can use this feature: + +![](page-feedback.png) + +### Chat Module: Deleting Messages & Conversations + +In this version, the [Chat Module](https://docs.abp.io/en/commercial/latest/modules/chat) allows you to delete individual messages or a complete conversation. + +You can enable or disable the message/conversation deletion globally on your application: + +![](settings.png) + +> **Note**: The Angular UI hasn't been completed yet. We aim to complete it as soon as possible and include it in the next release. + +### Password Complexity Indicators + +In this version, ABP Framework introduces an innovative ["Password Complexity Indicator"](https://docs.abp.io/en/commercial/7.4/ui/angular/password-complexity-indicator-component) feature, designed to enhance security and user experience. This feature dynamically evaluates and rates the strength of user-generated passwords, providing real-time feedback to users as they create or update their passwords. By visually indicating the complexity level, users are guided toward crafting stronger passwords that meet modern security standards. + +![](password-complexity.png) + +You can check the [Password Complexity Indicator Angular documentation](https://docs.abp.io/en/commercial/7.4/ui/angular/password-complexity-indicator-component) to learn more. + +> **Note**: Currently, this feature is only available for the Angular UI, but we will be implemented for other UIs in the next version. + +## Community News + +### DevNot Developer Summit 2023 + +![](developersummit.jpg) + +We are thrilled to announce that the co-founder of [Volosoft](https://volosoft.com/) and Lead Developer of the ABP Framework, Halil Ibrahim Kalkan will give a speech about "Building a Kubernetes Integrated Local Development Environment" in the [Developer Summit 2023 event](https://summit.devnot.com/) on the 7th of October. + +### New ABP Community Posts + +There are exciting articles contributed by the ABP community as always. I will highlight some of them here: + +* [ABP Commercial - GDPR Module Overview](https://community.abp.io/posts/abp-commercial-gdpr-module-overview-kvmsm3ku) by [Engincan Veske](https://twitter.com/EngincanVeske) +* [Video: ABP Framework Data Transfer Objects](https://community.abp.io/videos/abp-framework-data-transfer-objects-qwebfqz5) by [Hamza Albreem](https://github.com/braim23) +* [Video: ABP Framework Essentials: MongoDB](https://community.abp.io/videos/abp-framework-essentials-mongodb-gwlblh5x) by [Hamza Albreem](https://github.com/braim23) +* [ABP Modules and Entity Dependencies](https://community.abp.io/posts/abp-modules-and-entity-dependencies-hn7wr093) by [Jack Fistelmann](https://github.com/nebula2) +* [How to add dark mode support to the Basic Theme in 3 steps?](https://community.abp.io/posts/how-to-add-dark-mode-support-to-the-basic-theme-in-3-steps-ge9c0f85) by [Enis Necipoğlu](https://twitter.com/EnisNecipoglu) +* [Deploying docker image to Azure with yml and bicep through Github Actions](https://community.abp.io/posts/deploying-docker-image-to-azure-with-yml-and-bicep-through-github-actions-cjiuh55m) by [Sturla](https://community.abp.io/members/Sturla) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP-related (text or video) content](https://community.abp.io/articles/submit) 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://docs.abp.io/en/abp/7.4/Road-Map) documentation to learn about the release schedule and planned features for the next releases. Please try ABP v7.4 RC and provide feedback to help us release a more stable version. + +Thanks for being a part of this community! diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/book-extended-cs.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/book-extended-cs.png new file mode 100644 index 0000000000..1179118621 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/book-extended-cs.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/book-extended-cshtml.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/book-extended-cshtml.png new file mode 100644 index 0000000000..0ed90a33de Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/book-extended-cshtml.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/cover-image.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/cover-image.png new file mode 100644 index 0000000000..06ced79397 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/cover-image.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/developersummit.jpg b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/developersummit.jpg new file mode 100644 index 0000000000..65bae90315 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/developersummit.jpg differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/editions.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/editions.png new file mode 100644 index 0000000000..331706daa7 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/editions.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/error-page.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/error-page.png new file mode 100644 index 0000000000..8ece260ec3 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/error-page.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/fluid-layout.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/fluid-layout.png new file mode 100644 index 0000000000..6150f3e2bd Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/fluid-layout.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/maui.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/maui.png new file mode 100644 index 0000000000..4205001d80 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/maui.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/mobile-toolbars.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/mobile-toolbars.png new file mode 100644 index 0000000000..6dcde5da60 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/mobile-toolbars.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/move-all-tenants.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/move-all-tenants.png new file mode 100644 index 0000000000..b893360447 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/move-all-tenants.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/move-tenants.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/move-tenants.png new file mode 100644 index 0000000000..fb6b8364fb Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/move-tenants.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/page-feedback.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/page-feedback.png new file mode 100644 index 0000000000..dbdd9fe199 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/page-feedback.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/password-complexity.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/password-complexity.png new file mode 100644 index 0000000000..55bc8a8f43 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/password-complexity.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/settings.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/settings.png new file mode 100644 index 0000000000..1e835661a0 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/settings.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code-backend.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code-backend.png new file mode 100644 index 0000000000..cb14dfc0f1 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code-backend.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code-ui.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code-ui.png new file mode 100644 index 0000000000..132988a873 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code-ui.png differ diff --git a/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code.png b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code.png new file mode 100644 index 0000000000..2e510d0768 Binary files /dev/null and b/docs/en/Blog-Posts/2023-08-15 v7_4_Preview/suite-custom-code.png differ diff --git a/docs/en/Emailing.md b/docs/en/Emailing.md index bfb3058862..fd9e696b3e 100644 --- a/docs/en/Emailing.md +++ b/docs/en/Emailing.md @@ -58,7 +58,12 @@ namespace MyProject `SendAsync` method has overloads to supply more parameters like; * **from**: You can set this as the first argument to set a sender email address. If not provided, the default sender address is used (see the email settings below). +* **to**: You can set the target email address. +* **subject**: You can set the email subject. +* **body**: You can set the email body. * **isBodyHtml**: Indicates whether the email body may contain HTML tags. **Default: true**. +* **attachments**: You can pass a list of `EmailAttachment` to add attachments to the email. +* **extraProperties**: You can pass extra properties and use these properties in your own `IEmailSender` implementation to implement more mail sending features. > `IEmailSender` is the suggested way to send emails, since it makes your code provider independent. diff --git a/docs/en/Entity-Framework-Core.md b/docs/en/Entity-Framework-Core.md index e2cfd21a05..1f8b9e22f0 100644 --- a/docs/en/Entity-Framework-Core.md +++ b/docs/en/Entity-Framework-Core.md @@ -594,6 +594,14 @@ Whenever you access to a property/collection, EF Core automatically performs an See also [lazy loading document](https://docs.microsoft.com/en-us/ef/core/querying/related-data/lazy) of the EF Core. +## Read-Only Repositories + +ABP Framework provides read-only [repository](Repositories.md) interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`) to explicitly indicate that your purpose is to query data, but not change it. If so, you can inject these interfaces into your services. + +Entity Framework Core read-only repository implementation uses [EF Core's No-Tracking feature](https://learn.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries). That means the entities returned from the repository will not be tracked by the EF Core [change tracker](https://learn.microsoft.com/en-us/ef/core/change-tracking/), because it is expected that you won't update entities queried from a read-only repository. + +> This behavior works only if the repository object is injected with one of the read-only repository interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`). It won't work if you have injected a standard repository (e.g. `IRepository<...>`) then casted it to a read-only repository interface. + ## Access to the EF Core API In most cases, you want to hide EF Core APIs behind a repository (this is the main purpose of the repository pattern). However, if you want to access the `DbContext` instance over the repository, you can use `GetDbContext()` or `GetDbSet()` extension methods. Example: diff --git a/docs/en/Object-To-Object-Mapping.md b/docs/en/Object-To-Object-Mapping.md index c12b260921..40efdd7ee6 100644 --- a/docs/en/Object-To-Object-Mapping.md +++ b/docs/en/Object-To-Object-Mapping.md @@ -320,9 +320,22 @@ public class MyCustomUserMapper : IObjectMapper, ITransientDepend } ```` -ABP automatically discovers and registers the `MyCustomUserMapper` and it is automatically used whenever you use the `IObjectMapper` to map `User` to `UserDto`. - -A single class may implement more than one `IObjectMapper` each for a different object pairs. +ABP automatically discovers and registers the `MyCustomUserMapper` and it is automatically used whenever you use the `IObjectMapper` to map `User` to `UserDto`. A single class may implement more than one `IObjectMapper` each for a different object pairs. > This approach is powerful since `MyCustomUserMapper` can inject any other service and use in the `Map` methods. +Once you implement `IObjectMapper`, ABP can automatically convert a collection of `User` objects to a collection of `UserDto` objects. The following generic collection types are supported: + +* `IEnumerable` +* `ICollection` +* `Collection` +* `IList` +* `List` +* `T[]` (array) + +**Example:** + +````csharp +var users = await _userRepository.GetListAsync(); // returns List +var dtos = ObjectMapper.Map, List>(users); // creates List +```` diff --git a/docs/en/Repositories.md b/docs/en/Repositories.md index 95f908d004..a93460bbdd 100644 --- a/docs/en/Repositories.md +++ b/docs/en/Repositories.md @@ -205,8 +205,6 @@ Methods: - `WithDetails()` 1 overload - `WithDetailsAsync()` 1 overload - - Where as the `IReadOnlyBasicRepository` provides the following methods: - `GetCountAsync()` @@ -217,6 +215,12 @@ They can all be seen as below: ![generic-repositories](images/generic-repositories.png) +#### Read Only Repositories behavior in Entity Framework Core + +Entity Framework Core read-only repository implementation uses [EF Core's No-Tracking feature](https://learn.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries). That means the entities returned from the repository will not be tracked by the EF Core [change tracker](https://learn.microsoft.com/en-us/ef/core/change-tracking/), because it is expected that you won't update entities queried from a read-only repository. If you need to track the entities, you can still uses [AsTracking()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) extension method. + +> This behavior works only if the repository object is injected with one of the read-only repository interfaces (`IReadOnlyRepository<...>` or `IReadOnlyBasicRepository<...>`). It won't work if you have injected a standard repository (e.g. `IRepository<...>`) then casted it to a read-only repository interface. + ### Generic Repository without a Primary Key If your entity does not have an Id primary key (it may have a composite primary key for instance) then you cannot use the `IRepository` (or basic/readonly versions) defined above. In that case, you can inject and use `IRepository` for your entity. diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/Metadata/AbpModelMetadataProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/Metadata/AbpModelMetadataProvider.cs index 8d5f2d4c4a..1301a5e823 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/Metadata/AbpModelMetadataProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/Metadata/AbpModelMetadataProvider.cs @@ -40,11 +40,11 @@ public class AbpModelMetadataProvider : DefaultModelMetadataProvider { foreach (var validationAttribute in detail.ModelAttributes.Attributes.OfType()) { - NormalizeValidationAttrbute(validationAttribute); + NormalizeValidationAttribute(validationAttribute); } } - protected virtual void NormalizeValidationAttrbute(ValidationAttribute validationAttribute) + protected virtual void NormalizeValidationAttribute(ValidationAttribute validationAttribute) { if (validationAttribute.ErrorMessage == null) { diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/ValidationAttributeHelper.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/ValidationAttributeHelper.cs index 505c07a3e1..cd87c55ea4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/ValidationAttributeHelper.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Validation/ValidationAttributeHelper.cs @@ -1,14 +1,15 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.Reflection; namespace Volo.Abp.AspNetCore.Mvc.Validation; public static class ValidationAttributeHelper { - private static readonly PropertyInfo ValidationAttributeErrorMessageStringProperty = typeof(ValidationAttribute) + private readonly static PropertyInfo ValidationAttributeErrorMessageStringProperty = typeof(ValidationAttribute) .GetProperty("ErrorMessageString", BindingFlags.Instance | BindingFlags.NonPublic)!; - private static readonly PropertyInfo ValidationAttributeCustomErrorMessageSetProperty = typeof(ValidationAttribute) + private readonly static PropertyInfo ValidationAttributeCustomErrorMessageSetProperty = typeof(ValidationAttribute) .GetProperty("CustomErrorMessageSet", BindingFlags.Instance | BindingFlags.NonPublic)!; public static void SetDefaultErrorMessage(ValidationAttribute validationAttribute) @@ -24,7 +25,14 @@ public static class ValidationAttributeHelper } } - validationAttribute.ErrorMessage = - ValidationAttributeErrorMessageStringProperty.GetValue(validationAttribute) as string; + try + { + var errorMessageString = ValidationAttributeErrorMessageStringProperty.GetValue(validationAttribute) as string; + validationAttribute.ErrorMessage = errorMessageString; + } + catch (Exception e) + { + // ignored + } } } diff --git a/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs new file mode 100644 index 0000000000..6c648fd022 --- /dev/null +++ b/framework/src/Volo.Abp.Data/Volo/Abp/Data/AbpRepositoryIsReadOnlyException.cs @@ -0,0 +1,22 @@ +namespace Volo.Abp.Data; + +public class AbpRepositoryIsReadOnlyException : AbpException +{ + /// + /// Creates a new object. + /// + public AbpRepositoryIsReadOnlyException() + { + + } + + /// + /// Creates a new object. + /// + /// Exception message + public AbpRepositoryIsReadOnlyException(string message) + : base(message) + { + + } +} diff --git a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs index 8602792701..3791c4202f 100644 --- a/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs +++ b/framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs @@ -132,7 +132,7 @@ public abstract class AbstractKeyReadOnlyAppService ((IHasCreationTime)e).CreationTime); } - throw new AbpException("No sorting specified but this query requires sorting. Override the ApplyDefaultSorting method for your application service derived from AbstractKeyReadOnlyAppService!"); + throw new AbpException("No sorting specified but this query requires sorting. Override the ApplySorting or the ApplyDefaultSorting method for your application service derived from AbstractKeyReadOnlyAppService!"); } /// diff --git a/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs index c63a5442c7..0a2fa66b77 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Microsoft/Extensions/DependencyInjection/ServiceCollectionRepositoryExtensions.cs @@ -1,5 +1,6 @@ using System; using Microsoft.Extensions.DependencyInjection.Extensions; +using Volo.Abp; using Volo.Abp.Domain.Entities; using Volo.Abp.Domain.Repositories; @@ -17,13 +18,13 @@ public static class ServiceCollectionRepositoryExtensions var readOnlyBasicRepositoryInterface = typeof(IReadOnlyBasicRepository<>).MakeGenericType(entityType); if (readOnlyBasicRepositoryInterface.IsAssignableFrom(repositoryImplementationType)) { - RegisterService(services, readOnlyBasicRepositoryInterface, repositoryImplementationType, replaceExisting); + RegisterService(services, readOnlyBasicRepositoryInterface, repositoryImplementationType, replaceExisting, true); //IReadOnlyRepository var readOnlyRepositoryInterface = typeof(IReadOnlyRepository<>).MakeGenericType(entityType); if (readOnlyRepositoryInterface.IsAssignableFrom(repositoryImplementationType)) { - RegisterService(services, readOnlyRepositoryInterface, repositoryImplementationType, replaceExisting); + RegisterService(services, readOnlyRepositoryInterface, repositoryImplementationType, replaceExisting, true); } //IBasicRepository @@ -48,13 +49,13 @@ public static class ServiceCollectionRepositoryExtensions var readOnlyBasicRepositoryInterfaceWithPk = typeof(IReadOnlyBasicRepository<,>).MakeGenericType(entityType, primaryKeyType); if (readOnlyBasicRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType)) { - RegisterService(services, readOnlyBasicRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting); + RegisterService(services, readOnlyBasicRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting, true); //IReadOnlyRepository var readOnlyRepositoryInterfaceWithPk = typeof(IReadOnlyRepository<,>).MakeGenericType(entityType, primaryKeyType); if (readOnlyRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType)) { - RegisterService(services, readOnlyRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting); + RegisterService(services, readOnlyRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting, true); } //IBasicRepository @@ -80,15 +81,33 @@ public static class ServiceCollectionRepositoryExtensions IServiceCollection services, Type serviceType, Type implementationType, - bool replaceExisting) + bool replaceExisting, + bool isReadOnlyRepository = false) { + ServiceDescriptor descriptor; + + if (isReadOnlyRepository) + { + services.TryAddTransient(implementationType); + descriptor = ServiceDescriptor.Transient(serviceType, provider => + { + var repository = provider.GetRequiredService(implementationType); + ObjectHelper.TrySetProperty(repository.As(), x => x.IsReadOnly, _ => true); + return repository; + }); + } + else + { + descriptor = ServiceDescriptor.Transient(serviceType, implementationType); + } + if (replaceExisting) { - services.Replace(ServiceDescriptor.Transient(serviceType, implementationType)); + services.Replace(descriptor); } else { - services.TryAddTransient(serviceType, implementationType); + services.TryAdd(descriptor); } } } 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 0d86045eea..6de2aa3208 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 @@ -34,6 +34,8 @@ public abstract class BasicRepositoryBase : public ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService(NullCancellationTokenProvider.Instance); + public bool IsReadOnly { get; protected set; } + protected BasicRepositoryBase() { 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 3c7ae81875..d50a881c28 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 @@ -12,7 +12,7 @@ namespace Volo.Abp.Domain.Repositories; /// public interface IRepository { - + bool IsReadOnly { get; } } public interface IRepository : IReadOnlyRepository, IBasicRepository diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/BackgroundEmailSendingJob.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/BackgroundEmailSendingJob.cs index 07b4ca9bcd..f1d2812ae9 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/BackgroundEmailSendingJob.cs +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/BackgroundEmailSendingJob.cs @@ -15,15 +15,15 @@ public class BackgroundEmailSendingJob : AsyncBackgroundJob public bool IsBodyHtml { get; set; } = true; - //TODO: Add other properties and attachments + public List? Attachments { get; set; } + + public ExtraPropertyDictionary? ExtraProperties { get; set; } } diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailAttachment.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailAttachment.cs new file mode 100644 index 0000000000..e2fe3a3a6b --- /dev/null +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailAttachment.cs @@ -0,0 +1,11 @@ +using System; + +namespace Volo.Abp.Emailing; + +[Serializable] +public class EmailAttachment +{ + public string? Name { get; set; } + + public byte[]? File { get; set; } +} diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailSenderBase.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailSenderBase.cs index d8a46ca4b9..8de9f4c5a1 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailSenderBase.cs +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/EmailSenderBase.cs @@ -1,8 +1,12 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Net.Mail; using System.Text; using System.Threading.Tasks; using Volo.Abp.BackgroundJobs; +using Volo.Abp.Data; namespace Volo.Abp.Emailing; @@ -24,20 +28,33 @@ public abstract class EmailSenderBase : IEmailSender BackgroundJobManager = backgroundJobManager; } - public virtual async Task SendAsync(string to, string? subject, string? body, bool isBodyHtml = true) + public virtual async Task SendAsync(string to, string? subject, string? body, bool isBodyHtml = true, List? attachments = null, ExtraPropertyDictionary? extraProperties = null) { - await SendAsync(new MailMessage - { - To = { to }, - Subject = subject, - Body = body, - IsBodyHtml = isBodyHtml - }); + await SendAsync(BuildMailMessage(null, to, subject, body, isBodyHtml, attachments, extraProperties)); } - public virtual async Task SendAsync(string from, string to, string? subject, string? body, bool isBodyHtml = true) + public virtual async Task SendAsync(string from, string to, string? subject, string? body, bool isBodyHtml = true, List? attachments = null, ExtraPropertyDictionary? extraProperties = null) { - await SendAsync(new MailMessage(from, to, subject, body) { IsBodyHtml = isBodyHtml }); + await SendAsync(BuildMailMessage(from, to, subject, body, isBodyHtml, attachments, extraProperties)); + } + + protected virtual MailMessage BuildMailMessage(string? from, string to, string? subject, string? body, bool isBodyHtml = true, List? attachments = null, ExtraPropertyDictionary? extraProperties = null) + { + var message = from == null + ? new MailMessage { To = { to }, Subject = subject, Body = body, IsBodyHtml = isBodyHtml } + : new MailMessage(from, to, subject, body) { IsBodyHtml = isBodyHtml }; + + if (attachments != null) + { + foreach (var attachment in attachments.Where(x => x.File != null)) + { + var fileStream = new MemoryStream(attachment.File!); + fileStream.Seek(0, SeekOrigin.Begin); + message.Attachments.Add(new Attachment(fileStream, attachment.Name)); + } + } + + return message; } public virtual async Task SendAsync(MailMessage mail, bool normalize = true) @@ -50,11 +67,11 @@ public abstract class EmailSenderBase : IEmailSender await SendEmailAsync(mail); } - public virtual async Task QueueAsync(string to, string subject, string body, bool isBodyHtml = true) + public virtual async Task QueueAsync(string to, string subject, string body, bool isBodyHtml = true, List? attachments = null, ExtraPropertyDictionary? extraProperties = null) { if (!BackgroundJobManager.IsAvailable()) { - await SendAsync(to, subject, body, isBodyHtml); + await SendAsync(to, subject, body, isBodyHtml, attachments, extraProperties); return; } @@ -64,16 +81,18 @@ public abstract class EmailSenderBase : IEmailSender To = to, Subject = subject, Body = body, - IsBodyHtml = isBodyHtml + IsBodyHtml = isBodyHtml, + Attachments = attachments, + ExtraProperties = extraProperties } ); } - public virtual async Task QueueAsync(string from, string to, string subject, string body, bool isBodyHtml = true) + public virtual async Task QueueAsync(string from, string to, string subject, string body, bool isBodyHtml = true, List? attachments = null, ExtraPropertyDictionary? extraProperties = null) { if (!BackgroundJobManager.IsAvailable()) { - await SendAsync(from, to, subject, body, isBodyHtml); + await SendAsync(from, to, subject, body, isBodyHtml, attachments, extraProperties); return; } @@ -84,7 +103,9 @@ public abstract class EmailSenderBase : IEmailSender To = to, Subject = subject, Body = body, - IsBodyHtml = isBodyHtml + IsBodyHtml = isBodyHtml, + Attachments = attachments, + ExtraProperties = extraProperties } ); } diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/IEmailSender.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/IEmailSender.cs index 55456783c4..d2952c2d47 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/IEmailSender.cs +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/IEmailSender.cs @@ -1,5 +1,7 @@ -using System.Net.Mail; +using System.Collections.Generic; +using System.Net.Mail; using System.Threading.Tasks; +using Volo.Abp.Data; namespace Volo.Abp.Emailing; @@ -15,7 +17,9 @@ public interface IEmailSender string to, string? subject, string? body, - bool isBodyHtml = true + bool isBodyHtml = true, + List? attachments = null, + ExtraPropertyDictionary? extraProperties = null ); /// @@ -26,7 +30,9 @@ public interface IEmailSender string to, string? subject, string? body, - bool isBodyHtml = true + bool isBodyHtml = true, + List? attachments = null, + ExtraPropertyDictionary? extraProperties = null ); /// @@ -49,7 +55,9 @@ public interface IEmailSender string to, string subject, string body, - bool isBodyHtml = true + bool isBodyHtml = true, + List? attachments = null, + ExtraPropertyDictionary? extraProperties = null ); /// @@ -60,8 +68,8 @@ public interface IEmailSender string to, string subject, string body, - bool isBodyHtml = true + bool isBodyHtml = true, + List? attachments = null, + ExtraPropertyDictionary? extraProperties = null ); - - //TODO: Add other Queue methods too. Problem: MailMessage is not serializable so can not be used in background jobs. } diff --git a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Smtp/SmtpEmailSender.cs b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Smtp/SmtpEmailSender.cs index 99a8f2e1e7..4bf7e18158 100644 --- a/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Smtp/SmtpEmailSender.cs +++ b/framework/src/Volo.Abp.Emailing/Volo/Abp/Emailing/Smtp/SmtpEmailSender.cs @@ -67,7 +67,7 @@ public class SmtpEmailSender : EmailSenderBase, ISmtpEmailSender, ITransientDepe } } - protected override async Task SendEmailAsync(MailMessage mail) + protected async override Task SendEmailAsync(MailMessage mail) { using (var smtpClient = await BuildClientAsync()) { diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs index 94dba628d7..614ba135ff 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Volo.Abp.Domain.Entities; @@ -44,4 +45,10 @@ public static class EfCoreRepositoryExtensions throw new ArgumentException("Given repository does not implement " + typeof(IEfCoreRepository).AssemblyQualifiedName, nameof(repository)); } + + public static IQueryable AsNoTrackingIf(this IQueryable queryable, bool condition) + where TEntity : class, IEntity + { + return condition ? queryable.AsNoTracking() : queryable; + } } 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 96d7b4111e..b881c07245 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,4 +1,3 @@ -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -6,16 +5,15 @@ using System; using System.Collections.Generic; using System.Data; using System.Linq; -using System.Linq.Dynamic.Core; using System.Linq.Expressions; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore.Storage; +using Volo.Abp.Data; using Volo.Abp.Domain.Entities; using Volo.Abp.EntityFrameworkCore; using Volo.Abp.EntityFrameworkCore.DependencyInjection; using Volo.Abp.Guids; -using Volo.Abp.MultiTenancy; namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore; @@ -75,7 +73,7 @@ public class EfCoreRepository : RepositoryBase, IE { return (await GetDbContextAsync()).Set(); } - + protected async Task GetDbConnectionAsync() { return (await GetDbContextAsync()).Database.GetDbConnection(); @@ -107,8 +105,9 @@ public class EfCoreRepository : RepositoryBase, IE ); } - public override async Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); CheckAndSetId(entity); var dbContext = await GetDbContextAsync(); @@ -123,8 +122,9 @@ public class EfCoreRepository : RepositoryBase, IE return savedEntity; } - public override async Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task InsertManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); var entityArray = entities.ToArray(); var dbContext = await GetDbContextAsync(); cancellationToken = GetCancellationToken(cancellationToken); @@ -153,8 +153,9 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); var dbContext = await GetDbContextAsync(); dbContext.Attach(entity); @@ -169,8 +170,9 @@ public class EfCoreRepository : RepositoryBase, IE return updatedEntity; } - public override async Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task UpdateManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); cancellationToken = GetCancellationToken(cancellationToken); if (BulkOperationProvider != null) @@ -195,8 +197,9 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); var dbContext = await GetDbContextAsync(); dbContext.Set().Remove(entity); @@ -207,8 +210,9 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task DeleteManyAsync(IEnumerable entities, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); cancellationToken = GetCancellationToken(cancellationToken); if (BulkOperationProvider != null) @@ -233,26 +237,26 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default) + public async override Task> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default) { return includeDetails ? await (await WithDetailsAsync()).ToListAsync(GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()).ToListAsync(GetCancellationToken(cancellationToken)); + : await (await GetQueryableAsync()).ToListAsync(GetCancellationToken(cancellationToken)); } - public override async Task> GetListAsync(Expression> predicate, bool includeDetails = false, CancellationToken cancellationToken = default) + public async override Task> GetListAsync(Expression> predicate, bool includeDetails = false, CancellationToken cancellationToken = default) { return includeDetails ? await (await WithDetailsAsync()).Where(predicate).ToListAsync(GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()).Where(predicate).ToListAsync(GetCancellationToken(cancellationToken)); + : await (await GetQueryableAsync()).Where(predicate).ToListAsync(GetCancellationToken(cancellationToken)); } - public override async Task GetCountAsync(CancellationToken cancellationToken = default) + public async override Task GetCountAsync(CancellationToken cancellationToken = default) { - return await (await GetDbSetAsync()).LongCountAsync(GetCancellationToken(cancellationToken)); + return await (await GetQueryableAsync()).LongCountAsync(GetCancellationToken(cancellationToken)); } - public override async Task> GetPagedListAsync( + public async override Task> GetPagedListAsync( int skipCount, int maxResultCount, string sorting, @@ -261,7 +265,7 @@ public class EfCoreRepository : RepositoryBase, IE { var queryable = includeDetails ? await WithDetailsAsync() - : await GetDbSetAsync(); + : await GetQueryableAsync(); return await queryable .OrderByIf>(!sorting.IsNullOrWhiteSpace(), sorting) @@ -272,20 +276,20 @@ public class EfCoreRepository : RepositoryBase, IE [Obsolete("Use GetQueryableAsync method.")] protected override IQueryable GetQueryable() { - return DbSet.AsQueryable(); + return DbSet.AsQueryable().AsNoTrackingIf(IsReadOnly); } - public override async Task> GetQueryableAsync() + public async override Task> GetQueryableAsync() { - return (await GetDbSetAsync()).AsQueryable(); + return (await GetDbSetAsync()).AsQueryable().AsNoTrackingIf(IsReadOnly); } - protected override async Task SaveChangesAsync(CancellationToken cancellationToken) + protected async override Task SaveChangesAsync(CancellationToken cancellationToken) { await (await GetDbContextAsync()).SaveChangesAsync(cancellationToken); } - public override async Task FindAsync( + public async override Task FindAsync( Expression> predicate, bool includeDetails = true, CancellationToken cancellationToken = default) @@ -294,13 +298,14 @@ public class EfCoreRepository : RepositoryBase, IE ? await (await WithDetailsAsync()) .Where(predicate) .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()) + : await (await GetQueryableAsync()) .Where(predicate) .SingleOrDefaultAsync(GetCancellationToken(cancellationToken)); } - public override async Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) + public async override Task DeleteAsync(Expression> predicate, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); var dbContext = await GetDbContextAsync(); var dbSet = dbContext.Set(); @@ -316,8 +321,9 @@ public class EfCoreRepository : RepositoryBase, IE } } - public override async Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default) + public async override Task DeleteDirectAsync(Expression> predicate, CancellationToken cancellationToken = default) { + CheckReadOnly(); var dbContext = await GetDbContextAsync(); var dbSet = dbContext.Set(); await dbSet.Where(predicate).ExecuteDeleteAsync(GetCancellationToken(cancellationToken)); @@ -358,7 +364,7 @@ public class EfCoreRepository : RepositoryBase, IE return AbpEntityOptions.DefaultWithDetailsFunc(GetQueryable()); } - public override async Task> WithDetailsAsync() + public async override Task> WithDetailsAsync() { if (AbpEntityOptions.DefaultWithDetailsFunc == null) { @@ -377,7 +383,7 @@ public class EfCoreRepository : RepositoryBase, IE ); } - public override async Task> WithDetailsAsync(params Expression>[] propertySelectors) + public async override Task> WithDetailsAsync(params Expression>[] propertySelectors) { return IncludeDetails( await GetQueryableAsync(), @@ -421,6 +427,21 @@ public class EfCoreRepository : RepositoryBase, IE true ); } + + protected virtual void CheckReadOnly() + { + if (IsReadOnly) + { + throw new AbpRepositoryIsReadOnlyException($"Can not call " + + $"{nameof(InsertAsync)}, " + + $"{nameof(InsertManyAsync)}, " + + $"{nameof(UpdateAsync)}, " + + $"{nameof(UpdateManyAsync)}, " + + $"{nameof(DeleteAsync)}, " + + $"{nameof(DeleteManyAsync)}, " + + $"{nameof(DeleteDirectAsync)} methods on a read-only repository!"); + } + } } public class EfCoreRepository : EfCoreRepository, @@ -452,11 +473,14 @@ public class EfCoreRepository : EfCoreRepository e.Id).FirstOrDefaultAsync(e => e.Id.Equals(id), GetCancellationToken(cancellationToken)) - : await (await GetDbSetAsync()).FindAsync(new object[] { id }, GetCancellationToken(cancellationToken)); + : IsReadOnly + ? await (await GetQueryableAsync()).OrderBy(e => e.Id).FirstOrDefaultAsync(e => e.Id.Equals(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) { + CheckReadOnly(); var entity = await FindAsync(id, cancellationToken: cancellationToken); if (entity == null) { @@ -468,6 +492,7 @@ public class EfCoreRepository : EfCoreRepository ids, bool autoSave = false, CancellationToken cancellationToken = default) { + CheckReadOnly(); cancellationToken = GetCancellationToken(cancellationToken); var entities = await (await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken); diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyApiDescriptionFinder.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyApiDescriptionFinder.cs index 9c54fe840e..f7b9b838b5 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyApiDescriptionFinder.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyApiDescriptionFinder.cs @@ -1,8 +1,10 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.FileProviders.Physical; using Volo.Abp.DependencyInjection; using Volo.Abp.Http.Modeling; using Volo.Abp.Json; @@ -92,15 +94,35 @@ public class ClientProxyApiDescriptionFinder : IClientProxyApiDescriptionFinder, { if (directoryContent.IsDirectory) { - GetGenerateProxyFileInfos(fileInfoList, directoryContent.PhysicalPath); + GetGenerateProxyFileInfos(fileInfoList, GetDirectoryContentPath(path, directoryContent)); } else { if (directoryContent.Name.EndsWith("generate-proxy.json")) { - fileInfoList.Add(VirtualFileProvider.GetFileInfo(directoryContent.GetVirtualOrPhysicalPathOrNull())); + fileInfoList.Add(VirtualFileProvider.GetFileInfo(GetProxyFileInfoPath(path, directoryContent))); } } } } + + private string GetDirectoryContentPath(string rootPath, IFileInfo fileInfo) + { + if (fileInfo is PhysicalDirectoryInfo physicalDirectoryInfo) + { + return rootPath + physicalDirectoryInfo.Name.EnsureStartsWith('/'); + } + + return fileInfo.PhysicalPath; + } + + private string GetProxyFileInfoPath(string rootPath, IFileInfo fileInfo) + { + if (fileInfo is PhysicalFileInfo physicalFileInfo) + { + return rootPath + physicalFileInfo.Name.EnsureStartsWith('/'); + } + + return fileInfo.GetVirtualOrPhysicalPathOrNull(); + } } diff --git a/framework/src/Volo.Abp.MailKit/Volo/Abp/MailKit/MailKitSmtpEmailSender.cs b/framework/src/Volo.Abp.MailKit/Volo/Abp/MailKit/MailKitSmtpEmailSender.cs index c3523b4e1a..96e436760e 100644 --- a/framework/src/Volo.Abp.MailKit/Volo/Abp/MailKit/MailKitSmtpEmailSender.cs +++ b/framework/src/Volo.Abp.MailKit/Volo/Abp/MailKit/MailKitSmtpEmailSender.cs @@ -29,7 +29,7 @@ public class MailKitSmtpEmailSender : EmailSenderBase, IMailKitSmtpEmailSender SmtpConfiguration = smtpConfiguration; } - protected override async Task SendEmailAsync(MailMessage mail) + protected async override Task SendEmailAsync(MailMessage mail) { using (var client = await BuildClientAsync()) { 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 7048d183c7..6a2ab27870 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/DefaultObjectMapper.cs @@ -1,5 +1,11 @@ using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; using Volo.Abp.DependencyInjection; namespace Volo.Abp.ObjectMapping; @@ -19,6 +25,8 @@ public class DefaultObjectMapper : DefaultObjectMapper, IObjectMapper< public class DefaultObjectMapper : IObjectMapper, ITransientDependency { + protected static ConcurrentDictionary MethodInfoCache { get; } = new ConcurrentDictionary(); + public IAutoObjectMappingProvider AutoObjectMappingProvider { get; } protected IServiceProvider ServiceProvider { get; } @@ -46,6 +54,12 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency { return specificMapper.Map(source); } + + var result = TryToMapCollection(scope, source, default); + if (result != null) + { + return result; + } } if (source is IMapTo mapperSource) @@ -85,6 +99,12 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency { return specificMapper.Map(source, destination); } + + var result = TryToMapCollection(scope, source, destination); + if (result != null) + { + return result; + } } if (source is IMapTo mapperSource) @@ -102,6 +122,131 @@ public class DefaultObjectMapper : IObjectMapper, ITransientDependency return AutoMap(source, destination); } + protected virtual TDestination? TryToMapCollection(IServiceScope serviceScope, TSource source, TDestination? destination) + { + if (!IsCollectionGenericType(out var sourceArgumentType, out var destinationArgumentType, out var definitionGenericType)) + { + return default; + } + + var mapperType = typeof(IObjectMapper<,>).MakeGenericType(sourceArgumentType, destinationArgumentType); + var specificMapper = serviceScope.ServiceProvider.GetService(mapperType); + if (specificMapper == null) + { + //skip, no specific mapper + return default; + } + + var cacheKey = $"{mapperType.FullName}_{(destination == null ? "MapMethodWithSingleParameter" : "MapMethodWithDoubleParameters")}"; + var method = MethodInfoCache.GetOrAdd( + cacheKey, + _ => + { + return specificMapper + .GetType() + .GetMethods() + .First(x => + x.Name == nameof(IObjectMapper.Map) && + x.GetParameters().Length == (destination == null ? 1 : 2) + ); + } + ); + + 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 + ? method.Invoke(specificMapper, new [] { sourceList[i] })! + : method.Invoke(specificMapper, new [] { 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. + return (TDestination)result; + } + + //Return the destination if destination exists. The parameter reference equals with return object. + return destination ?? (TDestination)result; + } + + protected virtual bool IsCollectionGenericType(out Type sourceArgumentType, out Type destinationArgumentType, out Type definitionGenericType) + { + sourceArgumentType = default!; + destinationArgumentType = default!; + definitionGenericType = default!; + + if ((!typeof(TSource).IsGenericType && !typeof(TSource).IsArray) || + (!typeof(TDestination).IsGenericType && !typeof(TDestination).IsArray)) + { + return false; + } + + var supportedCollectionTypes = new[] + { + typeof(IEnumerable<>), + typeof(ICollection<>), + typeof(Collection<>), + typeof(IList<>), + typeof(List<>) + }; + + if (typeof(TSource).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TSource).GetGenericTypeDefinition())) + { + sourceArgumentType = typeof(TSource).GenericTypeArguments[0]; + } + + if (typeof(TSource).IsArray) + { + sourceArgumentType = typeof(TSource).GetElementType()!; + } + + if (sourceArgumentType == default!) + { + return false; + } + + definitionGenericType = typeof(List<>); + if (typeof(TDestination).IsGenericType && supportedCollectionTypes.Any(x => x == typeof(TDestination).GetGenericTypeDefinition())) + { + destinationArgumentType = typeof(TDestination).GenericTypeArguments[0]; + + if (typeof(TDestination).GetGenericTypeDefinition() == typeof(ICollection<>) || + typeof(TDestination).GetGenericTypeDefinition() == typeof(Collection<>)) + { + definitionGenericType = typeof(Collection<>); + } + } + + if (typeof(TDestination).IsArray) + { + destinationArgumentType = typeof(TDestination).GetElementType()!; + definitionGenericType = typeof(Array); + } + + return destinationArgumentType != default!; + } + protected virtual TDestination AutoMap(object source) { return AutoObjectMappingProvider.Map(source); diff --git a/framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs b/framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs index 6c3f8e5090..541b62f58d 100644 --- a/framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs +++ b/framework/test/AbpTestBase/Microsoft/Extensions/DependencyInjection/ServiceCollectionShouldlyExtensions.cs @@ -17,6 +17,17 @@ public static class ServiceCollectionShouldlyExtensions serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); } + public static void ShouldContainTransientImplementationFactory(this IServiceCollection services, Type serviceType) + { + var serviceDescriptor = services.FirstOrDefault(s => s.ServiceType == serviceType); + + serviceDescriptor.ShouldNotBeNull(); + serviceDescriptor.ImplementationType.ShouldBeNull(); + serviceDescriptor.ImplementationFactory.ShouldNotBeNull(); + serviceDescriptor.ImplementationInstance.ShouldBeNull(); + serviceDescriptor.Lifetime.ShouldBe(ServiceLifetime.Transient); + } + public static void ShouldContainSingleton(this IServiceCollection services, Type serviceType, Type implementationType = null) { var serviceDescriptor = services.FirstOrDefault(s => s.ServiceType == serviceType); diff --git a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs index 3b1e3553bd..46f028596d 100644 --- a/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs +++ b/framework/test/Volo.Abp.AutoMapper.Tests/Volo/Abp/AutoMapper/AbpAutoMapperModule_Specific_ObjectMapper_Tests.cs @@ -1,4 +1,7 @@ using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using Shouldly; using Volo.Abp.AutoMapper.SampleClasses; @@ -24,6 +27,109 @@ public class AbpAutoMapperModule_Specific_ObjectMapper_Tests : AbpIntegratedTest 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 AutoMapper. + destinationArray.First().Number.ShouldBe(40); + ReferenceEquals(returnArray, destinationArray).ShouldBeFalse(); + } + [Fact] public void Should_Use_Destination_Object_Constructor_If_Available() { 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 e6e17b253c..f9b59e2c94 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 @@ -31,15 +31,15 @@ public class RepositoryRegistration_Tests //Assert //MyTestAggregateRootWithoutPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); @@ -69,24 +69,24 @@ public class RepositoryRegistration_Tests //Assert //MyTestAggregateRootWithoutPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); //MyTestEntityWithInt32Pk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyBasicRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); } @@ -114,20 +114,20 @@ public class RepositoryRegistration_Tests services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); - services.ShouldContainTransient(typeof(IReadOnlyBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); //MyTestEntityWithInt32Pk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyBasicRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); } @@ -209,10 +209,10 @@ public class RepositoryRegistration_Tests services.ShouldNotContainService(typeof(IRepository)); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestDefaultRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestDefaultRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestDefaultRepository)); } @@ -234,11 +234,11 @@ public class RepositoryRegistration_Tests new MyTestRepositoryRegistrar(options).AddRepositories(); //MyTestAggregateRootWithGuidPk - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); - services.ShouldContainTransient(typeof(IReadOnlyRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); - services.ShouldContainTransient(typeof(IReadOnlyBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyRepository)); + services.ShouldContainTransientImplementationFactory(typeof(IReadOnlyBasicRepository)); services.ShouldContainTransient(typeof(IBasicRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); services.ShouldContainTransient(typeof(IRepository), typeof(MyTestAggregateRootWithDefaultPkCustomRepository)); } @@ -407,7 +407,7 @@ public class RepositoryRegistration_Tests public class MyTestAggregateRootWithDefaultPkEmptyRepository : IMyTestAggregateRootWithDefaultPkEmptyRepository { - + public bool IsReadOnly { get; set; } } public class TestDbContextRegistrationOptions : AbpCommonDbContextRegistrationOptions diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs new file mode 100644 index 0000000000..393b54dbe3 --- /dev/null +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/Repositories/ReadOnlyRepository_Tests.cs @@ -0,0 +1,112 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Data; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Domain.Repositories.EntityFrameworkCore; +using Volo.Abp.TestApp.Domain; +using Volo.Abp.TestApp.EntityFrameworkCore; +using Volo.Abp.TestApp.Testing; +using Volo.Abp.Uow; +using Xunit; + +namespace Volo.Abp.EntityFrameworkCore.Repositories; + +public class ReadOnlyRepository_Tests : TestAppTestBase +{ + [Fact] + public async Task ReadOnlyRepository_Should_NoTracking() + { + // Non-read-only repository tracking default + await WithUnitOfWorkAsync(async () => + { + var repository = GetRequiredService>(); + var db = await repository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + var list = await repository.GetListAsync(); + list.Count.ShouldBeGreaterThan(0); + db.ChangeTracker.Entries().Count().ShouldBe(list.Count); + }); + + // Read-only repository no tracking default + await WithUnitOfWorkAsync(async () => + { + var readonlyRepository = GetRequiredService>(); + var db = await readonlyRepository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + var list = await readonlyRepository.GetListAsync(); + list.Count.ShouldBeGreaterThan(0); + db.ChangeTracker.Entries().Count().ShouldBe(0); + }); + + // Read-only repository can tracking manually by AsTracking + await WithUnitOfWorkAsync(async () => + { + var readonlyRepository = GetRequiredService>(); + var db = await readonlyRepository.GetDbContextAsync(); + db.ChangeTracker.Entries().Count().ShouldBe(0); + var list = await (await readonlyRepository.ToEfCoreRepository().GetQueryableAsync()).AsTracking().ToListAsync(); + list.Count.ShouldBeGreaterThan(0); + db.ChangeTracker.Entries().Count().ShouldBe(list.Count); + }); + } + + [Fact] + public async Task ReadOnlyRepository_Should_Throw_AbpRepositoryIsReadOnlyException_When_Write_Method_Call() + { + await WithUnitOfWorkAsync(async () => + { + var repository = GetRequiredService>(); + await repository.ToEfCoreRepository().InsertAsync(new Person(Guid.NewGuid(), "test", 18)); + var person = await repository.ToEfCoreRepository().FirstOrDefaultAsync(); + person.ShouldNotBeNull(); + }); + + await WithUnitOfWorkAsync(async () => + { + await Assert.ThrowsAsync(async () => + { + var readonlyRepository = GetRequiredService>(); + await readonlyRepository.ToEfCoreRepository().As>().InsertAsync(new Person(Guid.NewGuid(), "test readonly", 18)); + }); + }); + } + + [Fact] + public async Task ReadOnlyRepository_Should_NoTracking_In_UOW() + { + var repository = GetRequiredService>(); + var readonlyRepository = GetRequiredService>(); + + await WithUnitOfWorkAsync(async () => + { + await repository.InsertAsync(new Person(Guid.NewGuid(), "people1", 18)); + await repository.InsertAsync(new Person(Guid.NewGuid(), "people2", 19)); + }); + + using (var uow = GetRequiredService().Begin()) + { + var p1 = await repository.FirstOrDefaultAsync(x => x.Name == "people1"); + p1.ShouldNotBeNull(); + p1.ChangeName("people1-updated"); + + var p2 = await readonlyRepository.FirstOrDefaultAsync(x => x.Name == "people2"); + p2.ShouldNotBeNull(); + p2.ChangeName("people2-updated"); + + await uow.CompleteAsync(); + } + + await WithUnitOfWorkAsync(async () => + { + (await repository.FirstOrDefaultAsync(x => x.Name == "people1")).ShouldBeNull(); + (await repository.FirstOrDefaultAsync(x => x.Name == "people1-updated")).ShouldNotBeNull(); + + (await readonlyRepository.FirstOrDefaultAsync(x => x.Name == "people2")).ShouldNotBeNull(); + (await readonlyRepository.FirstOrDefaultAsync(x => x.Name == "people2-updated")).ShouldBeNull(); + }); + } +} diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ar.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ar.json index cdabc68af1..4691a2268e 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ar.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ar.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "لقد تلقينا طلب استرداد الحساب! إذا بدأت هذا الطلب ، فانقر فوق الارتباط التالي لإعادة تعيين كلمة المرور الخاصة بك.", "ResetMyPassword": "إعادة تعيين كلمة المرور الخاصة بي", "AccessDenied": "تم الرفض!", - "AccessDeniedMessage": "ليس لديك حق الوصول إلى هذا المورد." + "AccessDeniedMessage": "ليس لديك حق الوصول إلى هذا المورد.", + "OrRegisterWith": "أو التسجيل بـ:", + "RegisterUsingYourProviderAccount": "قم بالتسجيل باستخدام حسابك في {0}" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/cs.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/cs.json index faec2b2a22..aeae890d54 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/cs.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/cs.json @@ -63,6 +63,8 @@ "PasswordResetInfoInEmail": "Obdrželi jsme žádost o obnovení účtu! Pokud jste tento požadavek iniciovali, klikněte na následující odkaz a obnovte své heslo.", "ResetMyPassword": "Obnovit moje heslo", "AccessDenied": "Přístup odepřen!", - "AccessDeniedMessage": "K tomuto zdroji nemáte přístup." + "AccessDeniedMessage": "K tomuto zdroji nemáte přístup.", + "OrRegisterWith": "Nebo se registrujte pomocí:", + "RegisterUsingYourProviderAccount": "Registrovat pomocí vašeho účtu {0}" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/da.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/da.json index 895638aede..24283d06c8 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/da.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/da.json @@ -63,6 +63,8 @@ "PasswordResetInfoInEmail": "Vi har modtaget en forespørgsel for gendannelse af konto! Hvis du har lavet denne forespørgsel, klik på det efterfølgende link for at nulstille dit kodeord.", "ResetMyPassword": "Nulstil mit kodeord", "AccessDenied": "Adgang nægtet!", - "AccessDeniedMessage": "Du har ikke adgang til denne ressource." + "AccessDeniedMessage": "Du har ikke adgang til denne ressource.", + "OrRegisterWith": "Eller registrér med:", + "RegisterUsingYourProviderAccount": "Registrér med din {0} konto" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/de.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/de.json index fb1f4b7153..64c86e4abe 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/de.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/de.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Wir haben eine Anfrage zur Wiederherstellung des Kontos erhalten! Wenn Sie diese Anforderung initiiert haben, klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen.", "ResetMyPassword": "Mein Passwort zurücksetzen", "AccessDenied": "Zugriff abgelehnt!", - "AccessDeniedMessage": "Sie haben keinen Zugriff auf diese Ressource." + "AccessDeniedMessage": "Sie haben keinen Zugriff auf diese Ressource.", + "OrRegisterWith": "Oder registrieren Sie sich mit:", + "RegisterUsingYourProviderAccount": "Registrieren Sie sich mit Ihrem {0} Benutzerkonto" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/el.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/el.json index 6a6788300d..a64dca5ef5 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/el.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/el.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Λάβαμε ένα αίτημα ανάκτησης λογαριασμού! Εάν υποβάλατε αυτό το αίτημα, κάντε κλικ στον παρακάτω σύνδεσμο για να επαναφέρετε τον κωδικό πρόσβασής σας.", "ResetMyPassword": "Επαναφορά του κωδικού πρόσβασής μου", "AccessDenied": "Δεν επιτρέπεται η πρόσβαση!", - "AccessDeniedMessage": "Δεν έχετε πρόσβαση σε αυτόν τον πόρο." + "AccessDeniedMessage": "Δεν έχετε πρόσβαση σε αυτόν τον πόρο.", + "OrRegisterWith": "Ή εγγραφείτε με:", + "RegisterUsingYourProviderAccount": "Εγγραφείτε χρησιμοποιώντας τον λογαριασμό σας {0}" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en-GB.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en-GB.json index 4f1486a78e..f4208335b5 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en-GB.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en-GB.json @@ -61,6 +61,8 @@ "Volo.Account:InvalidEmailAddress": "Cannot find the given email address: {0}", "PasswordReset": "Password reset", "PasswordResetInfoInEmail": "We received an account recovery request! If you initiated this request, click the following link to reset your password.", - "ResetMyPassword": "Reset my password" + "ResetMyPassword": "Reset my password", + "OrRegisterWith": "Or register with", + "RegisterUsingYourProviderAccount": "Register using your {0} account" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json index 706f92bbd5..183bb5363e 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/en.json @@ -66,6 +66,8 @@ "PasswordResetInfoInEmail": "We received an account recovery request! If you initiated this request, click the following link to reset your password.", "ResetMyPassword": "Reset my password", "AccessDenied": "Access denied!", - "AccessDeniedMessage": "You do not have access to this resource." + "AccessDeniedMessage": "You do not have access to this resource.", + "OrRegisterWith": "Or register with", + "RegisterUsingYourProviderAccount": "Register using your {0} account" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es-mx.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es-mx.json index b3bcad86f9..6793e58986 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es-mx.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es-mx.json @@ -34,6 +34,8 @@ "NewPasswordConfirmFailed": "Por favor, confirme la nueva contraseña.", "NewPasswordSameAsOld": "La nueva contraseña debe ser diferente de la contraseña actual.", "Manage": "Administrar", - "MyAccount": "Mi cuenta" + "MyAccount": "Mi cuenta", + "OrRegisterWith": "O registrarse con", + "RegisterUsingYourProviderAccount": "Registrarse con su cuenta de {0} " } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json index 0e883a1372..9fd52c8450 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/es.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Recibimos una solicitud de recuperación de cuenta. Si inició esta solicitud, haga clic en el siguiente enlace para restablecer su contraseña.", "ResetMyPassword": "Restablecer mi contraseña", "AccessDenied": "¡Acceso denegado!", - "AccessDeniedMessage": "No tienes acceso a este recurso." + "AccessDeniedMessage": "No tienes acceso a este recurso.", + "OrRegisterWith": "O registrarse con:", + "RegisterUsingYourProviderAccount": "Registrarse con su cuenta de {0} " } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fa.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fa.json index e7c5ee084e..4fc0685b79 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fa.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fa.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "ما درخواست بازیابی حساب دریافت کردیم! اگر این درخواست را آغاز کرده اید, برای بازنشانی گذرواژه خود روی پیوند زیر کلیک کنید.", "ResetMyPassword": "بازنشانی گذرواژه من", "AccessDenied": "دسترسی ممنوع!", - "AccessDeniedMessage": "شما به این منبع دسترسی ندارید." + "AccessDeniedMessage": "شما به این منبع دسترسی ندارید.", + "OrRegisterWith": "یا ثبت نام کنید با:", + "RegisterUsingYourProviderAccount": "با استفاده از حساب {0} خود ثبت نام کنید" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fi.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fi.json index 027928ead3..2f0ae2c574 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fi.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fi.json @@ -66,6 +66,8 @@ "PasswordResetInfoInEmail": "Saimme tilin palautuspyynnön! Jos aloitit tämän pyynnön, voit nollata salasanasi napsauttamalla seuraavaa linkkiä.", "ResetMyPassword": "Vaihda salasanani", "AccessDenied": "Pääsy evätty!", - "AccessDeniedMessage": "Sinulla ei ole pääsyä tähän resurssiin." + "AccessDeniedMessage": "Sinulla ei ole pääsyä tähän resurssiin.", + "OrRegisterWith": "Tai rekisteröidy:", + "RegisterUsingYourProviderAccount": "Rekisteröidy {0} -tililläsi" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fr.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fr.json index b1e547652d..aa640abf1a 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fr.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/fr.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Nous avons reçu une demande de récupération de compte! Si vous avez lancé cette demande, cliquez sur le lien suivant pour réinitialiser votre mot de passe.", "ResetMyPassword": "Réinitialiser mon mot de passe", "AccessDenied": "Accès refusé!", - "AccessDeniedMessage": "Vous n'avez pas accès à cette ressource." + "AccessDeniedMessage": "Vous n'avez pas accès à cette ressource.", + "OrRegisterWith": "Or register with", + "RegisterUsingYourProviderAccount": "Register using your {0} account" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hi.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hi.json index fd93420216..a424deae00 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hi.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hi.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "हमें एक खाता पुनर्प्राप्ति अनुरोध प्राप्त हुआ! यदि आपने यह अनुरोध किया है, तो अपना पासवर्ड रीसेट करने के लिए निम्न लिंक पर क्लिक करें।", "ResetMyPassword": "अपना पासवर्ड रीसेट करें", "AccessDenied": "पहुंच अस्वीकृत!", - "AccessDeniedMessage": "आपके पास इस संसाधन तक पहुँच नहीं है।" + "AccessDeniedMessage": "आपके पास इस संसाधन तक पहुँच नहीं है।", + "OrRegisterWith": "या इसके साथ पंजीकरण करें:", + "RegisterUsingYourProviderAccount": "अपने {0} खाते का उपयोग करके पंजीकरण करें" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hr.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hr.json index 665755a2dd..8ec48e7a09 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hr.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hr.json @@ -66,6 +66,8 @@ "PasswordResetInfoInEmail": "Primili smo zahtjev za oporavak računa! Ako ste vi pokrenuli ovaj zahtjev, kliknite na sljedeću poveznicu za ponovno postavljanje lozinke.", "ResetMyPassword": "Resetirati moju lozinku", "AccessDenied": "Pristup odbijen!", - "AccessDeniedMessage": "Nemate pristup ovom resursu." + "AccessDeniedMessage": "Nemate pristup ovom resursu.", + "OrRegisterWith": "Ili se registrirajte sa:", + "RegisterUsingYourProviderAccount": "Registrirajte se koristeći svoj {0} račun" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hu.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hu.json index 3c97fd290d..481fa2733a 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hu.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/hu.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Fiók-helyreállítási kérelmet kaptunk! Ha Ön kezdeményezte ezt a kérést, kattintson a következő hivatkozásra jelszava visszaállításához.", "ResetMyPassword": "Jelszavam visszaállítása", "AccessDenied": "Hozzáférés megtagadva!", - "AccessDeniedMessage": "Nincs hozzáférése ehhez az erőforráshoz." + "AccessDeniedMessage": "Nincs hozzáférése ehhez az erőforráshoz.", + "OrRegisterWith": "Vagy regisztráljon:", + "RegisterUsingYourProviderAccount": "Regisztráljon a(z) {0} fiókjával" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/is.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/is.json index e309962242..f33db6eadb 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/is.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/is.json @@ -63,6 +63,8 @@ "PasswordResetInfoInEmail": "Við fengum beiðni um endurheimt reiknings! Ef þú stofnaðir þessa beiðni skaltu smella á eftirfarandi krækju til að endurstilla lykilorðið þitt.", "ResetMyPassword": "Endurstilla lykilorðið mitt", "AccessDenied": "Aðgangi hafnað!", - "AccessDeniedMessage": "Þú hefur ekki aðgang að þessari auðlind." + "AccessDeniedMessage": "Þú hefur ekki aðgang að þessari auðlind.", + "OrRegisterWith": "Eða skráðu þig með:", + "RegisterUsingYourProviderAccount": "Skráðu þig með {0} aðganginum þínum" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/it.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/it.json index 6e4521c8f7..0b5f3dbcf2 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/it.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/it.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Abbiamo ricevuto una richiesta di recupero dell'account! Se hai fatto tu questa richiesta fai clic sul seguente collegamento per reimpostare la password.", "ResetMyPassword": "Reimposta la mia password", "AccessDenied": "Accesso negato!", - "AccessDeniedMessage": "Non hai accesso a questa risorsa." + "AccessDeniedMessage": "Non hai accesso a questa risorsa.", + "OrRegisterWith": "Oppure registrati con:", + "RegisterUsingYourProviderAccount": "Registrati utilizzando il tuo account {0}" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/nl.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/nl.json index b804e517a9..d5d400277f 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/nl.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/nl.json @@ -63,6 +63,8 @@ "PasswordResetInfoInEmail": "We hebben een verzoek ontvangen om uw wachtwoord opnieuw in te stellen. Als u dit verzoek heeft ingediend, klikt u op de volgende link om een nieuw wachtwoord in te stellen.", "ResetMyPassword": "Reset mijn wachtwoord", "AccessDenied": "Toegang geweigerd!", - "AccessDeniedMessage": "U heeft geen toegang tot deze bron." + "AccessDeniedMessage": "U heeft geen toegang tot deze bron.", + "OrRegisterWith": "Of registreer met:", + "RegisterUsingYourProviderAccount": "Registreer met uw {0} -account" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pl-PL.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pl-PL.json index c3596c8aa5..0fdab745de 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pl-PL.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pl-PL.json @@ -63,6 +63,8 @@ "PasswordResetInfoInEmail": "Otrzymaliśmy prośbę o odzyskanie konta! Jeśli zainicjowałeś to żądanie, kliknij poniższy link, aby zresetować hasło.", "ResetMyPassword": "Zresetować moje hasło", "AccessDenied": "Brak dostępu!", - "AccessDeniedMessage": "Nie masz dostępu do tego zasobu." + "AccessDeniedMessage": "Nie masz dostępu do tego zasobu.", + "OrRegisterWith": "Lub zarejestruj się za pomocą:", + "RegisterUsingYourProviderAccount": "Zarejestruj się za pomocą konta {0}" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt-BR.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt-BR.json index 732d804bbe..5b54bc91c5 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt-BR.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/pt-BR.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Recebemos um pedido de recuperação de conta! Se você iniciou esta solicitação, clique no link a seguir para redefinir sua senha.", "ResetMyPassword": "Resetar minha senha", "AccessDenied": "Acesso negado!", - "AccessDeniedMessage": "Você não tem acesso a este recurso." + "AccessDeniedMessage": "Você não tem acesso a este recurso.", + "OrRegisterWith": "Ou registre-se com:", + "RegisterUsingYourProviderAccount": "Registre-se utilizando sua conta {0}" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ro-RO.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ro-RO.json index c2a75c3401..732a4103ec 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ro-RO.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ro-RO.json @@ -63,6 +63,8 @@ "PasswordResetInfoInEmail": "Am primit o cerere de recuperare a contului! Dacă dumneavoastră aţi iniţiat această cerere, daţi click pe următorul link pentru a vă reseta parola.", "ResetMyPassword": "Resetează-mi parola", "AccessDenied": "Acces interzis!", - "AccessDeniedMessage": "Nu aveţi acces la această resursă." + "AccessDeniedMessage": "Nu aveţi acces la această resursă.", + "OrRegisterWith": "Sau înregistraţi-vă cu:", + "RegisterUsingYourProviderAccount": "Înregistraţi-vă folosindu-vă contul {0}" } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ru.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ru.json index 88250e8d03..8413c32831 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ru.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/ru.json @@ -63,6 +63,8 @@ "PasswordResetInfoInEmail": "Мы получили запрос на восстановление аккаунта! Если вы инициировали этот запрос, щелкните следующую ссылку, чтобы сбросить пароль.", "ResetMyPassword": "Сбросить пароль", "AccessDenied": "В доступе отказано!", - "AccessDeniedMessage": "У вас нет доступа к этому ресурсу." + "AccessDeniedMessage": "У вас нет доступа к этому ресурсу.", + "OrRegisterWith": "Или зарегистрируйтесь с помощью:", + "RegisterUsingYourProviderAccount": "Зарегистрируйтесь, используя свой {0} аккаунт" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sk.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sk.json index 6ca224455a..d69c0da03e 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sk.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sk.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Dostali sme žiadosť na obnovenie účtu! Ak ste o zmenu žiadali vy, kliknite na nasledujúci link a obnovte svoje heslo.", "ResetMyPassword": "Obnovte moje heslo", "AccessDenied": "Prístup zamietnutý!", - "AccessDeniedMessage": "K tomuto zdroju nemáte prístup." + "AccessDeniedMessage": "K tomuto zdroju nemáte prístup.", + "OrRegisterWith": "Alebo sa zaregistrujte pomocou:", + "RegisterUsingYourProviderAccount": "Zaregistrujte sa pomocou svojho {0} účtu" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sl.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sl.json index 322e04cf76..4442227ad6 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sl.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/sl.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "Prejeta je bila zahteva za obnovitev računa! V kolikor ste vi sprožili zahtevo, kliknite na sledečo povezavo, da ponastavite geslo.", "ResetMyPassword": "Ponastavi geslo", "AccessDenied": "Dostop zavrnjen!", - "AccessDeniedMessage": "Nimate dostopa do tega vira." + "AccessDeniedMessage": "Nimate dostopa do tega vira.", + "OrRegisterWith": "Ali pa se registrirajte z:", + "RegisterUsingYourProviderAccount": "Registrirajte se z uporabo vašega {0} računa" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json index c63eb86cef..0e53963f9a 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/tr.json @@ -66,6 +66,8 @@ "PasswordResetInfoInEmail": "Şifrenizi sıfırlamanız için bir talep aldık! Eğer bu talebi siz gerçekleştirmişseniz, şifrenizi sıfırlamak için bağlantıya tıklayın.", "ResetMyPassword": "Şifremi sıfırla", "AccessDenied": "Erişim reddedildi!", - "AccessDeniedMessage": "Bu kaynağa erişiminiz yok." + "AccessDeniedMessage": "Bu kaynağa erişiminiz yok.", + "OrRegisterWith": "Veya bunlarla kayıt ol:", + "RegisterUsingYourProviderAccount": "{0} hesabınızla kayıt olun." } } diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/vi.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/vi.json index e10ae3ebe1..eae396b011 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/vi.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/vi.json @@ -63,6 +63,8 @@ "PasswordResetInfoInEmail": "Chúng tôi đã nhận được yêu cầu khôi phục tài khoản! Nếu bạn bắt đầu yêu cầu này, hãy nhấp vào liên kết sau để đặt lại mật khẩu của bạn.", "ResetMyPassword": "Đặt lại mật khẩu của tôi", "AccessDenied": "Quyền truy cập bị từ chối!", - "AccessDeniedMessage": "Bạn không có quyền truy cập vào tài nguyên này." + "AccessDeniedMessage": "Bạn không có quyền truy cập vào tài nguyên này.", + "OrRegisterWith": "Hoặc đăng ký bằng:", + "RegisterUsingYourProviderAccount": "Đăng ký bằng tài khoản {0} của bạn" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json index 1cfabd4d62..635971e7db 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hans.json @@ -66,6 +66,8 @@ "PasswordResetInfoInEmail": "我们收到了帐户恢复请求!如果你发起了此请求,请单击以下链接以重置密码.", "ResetMyPassword": "重置我的密码", "AccessDenied": "拒绝访问!", - "AccessDeniedMessage": "你无权访问此资源." + "AccessDeniedMessage": "你无权访问此资源.", + "OrRegisterWith": "或注册:", + "RegisterUsingYourProviderAccount": "使用你的{0}帐户注册" } } \ No newline at end of file diff --git a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hant.json b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hant.json index 8749163c1d..a782aa42dc 100644 --- a/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hant.json +++ b/modules/account/src/Volo.Abp.Account.Application.Contracts/Volo/Abp/Account/Localization/Resources/zh-Hant.json @@ -64,6 +64,8 @@ "PasswordResetInfoInEmail": "我們收到了帳戶恢復請求!如果你發起了此請求,請點擊以下連結以重置密碼.", "ResetMyPassword": "重置我的密碼", "AccessDenied": "拒絕訪問!", - "AccessDeniedMessage": "您無權訪問此資源." + "AccessDeniedMessage": "您無權訪問此資源.", + "OrRegisterWith": "或是註冊用:", + "RegisterUsingYourProviderAccount": "使用你的{0}帳號註冊" } } diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml index 59d5ee852f..137715804a 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml @@ -12,20 +12,42 @@ @L["Login"]
- @if (!Model.IsExternalLogin) + @if (!Model.IsExternalLogin && Model.EnableLocalRegister) { } + + @if(Model.EnableLocalRegister || Model.IsExternalLogin) + { + + } - - - @if (!Model.IsExternalLogin) + @if (!Model.IsExternalLogin && Model.EnableLocalRegister) { } -
- @L["Register"] -
+ + @if(Model.EnableLocalRegister || Model.IsExternalLogin) + { +
+ @L["Register"] +
+ } + + + @if (!Model.IsExternalLogin && Model.VisibleExternalProviders.Any()) + { +
+
@L["OrRegisterWith"]
+
+ @foreach (var provider in Model.VisibleExternalProviders) + { + + } +
+
+ } + diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs index f9489f7797..7bc536ba9d 100644 --- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs +++ b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Register.cshtml.cs @@ -1,7 +1,10 @@ +using System; +using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -32,16 +35,43 @@ public class RegisterModel : AccountPageModel [BindProperty(SupportsGet = true)] public string ExternalLoginAuthSchema { get; set; } - - public RegisterModel(IAccountAppService accountAppService) + + public IEnumerable ExternalProviders { get; set; } + public IEnumerable VisibleExternalProviders => ExternalProviders.Where(x => !string.IsNullOrWhiteSpace(x.DisplayName)); + public bool EnableLocalRegister { get; set; } + public bool IsExternalLoginOnly => EnableLocalRegister == false && ExternalProviders?.Count() == 1; + public string ExternalLoginScheme => IsExternalLoginOnly ? ExternalProviders?.SingleOrDefault()?.AuthenticationScheme : null; + + protected IAuthenticationSchemeProvider SchemeProvider { get; } + + protected AbpAccountOptions AccountOptions { get; } + + public RegisterModel( + IAccountAppService accountAppService, + IAuthenticationSchemeProvider schemeProvider, + IOptions accountOptions) { + SchemeProvider = schemeProvider; AccountAppService = accountAppService; + AccountOptions = accountOptions.Value; } public virtual async Task OnGetAsync() { - await CheckSelfRegistrationAsync(); + ExternalProviders = await GetExternalProviders(); + + if (!await CheckSelfRegistrationAsync()) + { + if (IsExternalLoginOnly) + { + return await OnPostExternalLogin(ExternalLoginScheme); + } + + Alerts.Warning(L["SelfRegistrationDisabledMessage"]); + } + await TrySetEmailAsync(); + return Page(); } @@ -76,7 +106,12 @@ public class RegisterModel : AccountPageModel { try { - await CheckSelfRegistrationAsync(); + ExternalProviders = await GetExternalProviders(); + + if (!await CheckSelfRegistrationAsync()) + { + throw new UserFriendlyException(L["SelfRegistrationDisabledMessage"]); + } if (IsExternalLogin) { @@ -147,13 +182,46 @@ public class RegisterModel : AccountPageModel await SignInManager.SignInAsync(user, isPersistent: true, ExternalLoginAuthSchema); } - protected virtual async Task CheckSelfRegistrationAsync() + protected virtual async Task CheckSelfRegistrationAsync() { - if (!await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled) || - !await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin)) + EnableLocalRegister = await SettingProvider.IsTrueAsync(AccountSettingNames.EnableLocalLogin) && + await SettingProvider.IsTrueAsync(AccountSettingNames.IsSelfRegistrationEnabled); + + if (IsExternalLogin) { - throw new UserFriendlyException(L["SelfRegistrationDisabledMessage"]); + return true; } + + if (!EnableLocalRegister) + { + return false; + } + + return true; + } + + protected virtual async Task> GetExternalProviders() + { + + var schemes = await SchemeProvider.GetAllSchemesAsync(); + + return schemes + .Where(x => x.DisplayName != null || x.Name.Equals(AccountOptions.WindowsAuthenticationSchemeName, StringComparison.OrdinalIgnoreCase)) + .Select(x => new ExternalProviderModel + { + DisplayName = x.DisplayName, + AuthenticationScheme = x.Name + }) + .ToList(); + } + + protected virtual async Task OnPostExternalLogin(string provider) + { + var redirectUrl = Url.Page("./Login", pageHandler: "ExternalLoginCallback", values: new { ReturnUrl, ReturnUrlHash }); + var properties = SignInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + properties.Items["scheme"] = provider; + + return await Task.FromResult(Challenge(properties, provider)); } public class PostInput @@ -173,4 +241,10 @@ public class RegisterModel : AccountPageModel [DisableAuditing] public string Password { get; set; } } + + public class ExternalProviderModel + { + public string DisplayName { get; set; } + public string AuthenticationScheme { get; set; } + } } diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Members/Index.cshtml b/modules/blogging/src/Volo.Blogging.Web/Pages/Members/Index.cshtml index e24b1dd5af..9ac8ee9462 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Members/Index.cshtml +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Members/Index.cshtml @@ -1,11 +1,8 @@ @page -@using System.Globalization @using Microsoft.Extensions.Localization @using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Tab -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Utils @using Volo.Abp.AspNetCore.Mvc.UI.Bundling.TagHelpers @using Volo.Abp.Users -@using Volo.Blogging @using Volo.Blogging.Areas.Blog.Helpers.TagHelpers @using Volo.Blogging.Localization @model Volo.Blogging.Pages.Members.IndexModel diff --git a/modules/blogging/src/Volo.Blogging.Web/Pages/Members/Index.css b/modules/blogging/src/Volo.Blogging.Web/Pages/Members/Index.css index 668e566cba..3a2c2675c5 100644 --- a/modules/blogging/src/Volo.Blogging.Web/Pages/Members/Index.css +++ b/modules/blogging/src/Volo.Blogging.Web/Pages/Members/Index.css @@ -1,6 +1,3 @@ .post-desc { overflow-wrap: break-word; } -a:not(#all-posts-tab,#edit-profile-tab) { - color: unset!important; -} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/UpdateCommentInput.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/UpdateCommentInput.cs index a85783ee3f..ca273cd94a 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/UpdateCommentInput.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/UpdateCommentInput.cs @@ -15,4 +15,8 @@ public class UpdateCommentInput : ExtensibleObject, IHasConcurrencyStamp public string Text { get; set; } public string ConcurrencyStamp { get; set; } + + public Guid? CaptchaToken { get; set; } + + public int CaptchaAnswer { get; set; } } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicCommentsController.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicCommentsController.cs index 00851bce15..a2e2dea013 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicCommentsController.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicCommentsController.cs @@ -1,7 +1,9 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; +using Volo.Abp; using Volo.Abp.AspNetCore.Mvc; using Volo.Abp.ObjectMapping; using Volo.CmsKit.Comments; @@ -12,7 +14,7 @@ using Volo.CmsKit.Public.Web.Security.Captcha; namespace Volo.CmsKit.Public.Web.Controllers; //[Route("cms-kit/public-comments")] -public class CmsKitPublicCommentsController : AbpController +public class CmsKitPublicCommentsController : CmsKitPublicControllerBase { public ICommentPublicAppService CommentPublicAppService { get; } protected CmsKitCommentOptions CmsKitCommentOptions { get; } @@ -31,12 +33,35 @@ public class CmsKitPublicCommentsController : AbpController [HttpPost] public virtual async Task ValidateAsync([FromBody] CreateCommentWithParametersInput input) { - if (CmsKitCommentOptions.IsRecaptchaEnabled && input.CaptchaToken.HasValue) + if (CmsKitCommentOptions.IsRecaptchaEnabled) { + CheckCaptchaTokenNullity(input.CaptchaToken); + SimpleMathsCaptchaGenerator.Validate(input.CaptchaToken.Value, input.CaptchaAnswer); } var dto = ObjectMapper.Map (input); await CommentPublicAppService.CreateAsync(input.EntityType, input.EntityId, dto); } + + [HttpPost] + public virtual async Task UpdateAsync(Guid id, [FromBody] UpdateCommentInput input) + { + if (CmsKitCommentOptions.IsRecaptchaEnabled) + { + CheckCaptchaTokenNullity(input.CaptchaToken); + + SimpleMathsCaptchaGenerator.Validate(input.CaptchaToken.Value, input.CaptchaAnswer); + } + + await CommentPublicAppService.UpdateAsync(id, input); + } + + private void CheckCaptchaTokenNullity(Guid? captchaToken) + { + if (!captchaToken.HasValue) + { + throw new UserFriendlyException(L["CaptchaCodeMissingMessage"]); + } + } } 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 new file mode 100644 index 0000000000..f22444036d --- /dev/null +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicControllerBase.cs @@ -0,0 +1,12 @@ +using Volo.Abp.AspNetCore.Mvc; +using Volo.CmsKit.Localization; + +namespace Volo.CmsKit.Public.Web.Controllers; + +public abstract class CmsKitPublicControllerBase : AbpController +{ + public CmsKitPublicControllerBase() + { + LocalizationResource = typeof(CmsKitResource); + } +} diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicGlobalResourcesController.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicGlobalResourcesController.cs index 115dfd3fcc..eb4356e8b9 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicGlobalResourcesController.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicGlobalResourcesController.cs @@ -11,7 +11,7 @@ using Volo.CmsKit.Public.GlobalResources; namespace Volo.CmsKit.Public.Web.Controllers; [Route("cms-kit/global-resources")] -public class CmsKitPublicGlobalResourcesController: AbpController +public class CmsKitPublicGlobalResourcesController : CmsKitPublicControllerBase { private readonly IGlobalResourcePublicAppService _globalResourcePublicAppService; private readonly IDistributedCache _resourceCache; diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs index c73fba95e7..67a9d64f67 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicWidgetsController.cs @@ -7,7 +7,7 @@ using Volo.CmsKit.Public.Web.Pages.CmsKit.Shared.Components.ReactionSelection; namespace Volo.CmsKit.Public.Web.Controllers; -public class CmsKitPublicWidgetsController : AbpController +public class CmsKitPublicWidgetsController : CmsKitPublicControllerBase { public Task ReactionSelection(string entityType, string entityId) { diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs index b1ea42397f..4452dc5eec 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs @@ -80,12 +80,7 @@ public class CommentingViewComponent : AbpViewComponent if (CmsKitCommentOptions.IsRecaptchaEnabled) { - CaptchaOutput = SimpleMathsCaptchaGenerator.Generate(new CaptchaOptions( - number1MinValue: 1, - number1MaxValue: 10, - number2MinValue: 5, - number2MaxValue: 15) - ); + CaptchaOutput = GetCaptcha(); viewModel.CaptchaImageBase64 = GetCaptchaImageBase64(CaptchaOutput.ImageBytes); } @@ -93,7 +88,17 @@ public class CommentingViewComponent : AbpViewComponent return View("~/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml", this); } - private string GetCaptchaImageBase64(byte[] bytes) + public CaptchaOutput GetCaptcha() + { + return SimpleMathsCaptchaGenerator.Generate(new CaptchaOptions( + number1MinValue: 1, + number1MaxValue: 10, + number2MinValue: 5, + number2MaxValue: 15) + ); + } + + public string GetCaptchaImageBase64(byte[] bytes) { return $"data:image/jpg;base64,{Convert.ToBase64String(bytes)}"; } diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml index 34ce468187..5d11f624f3 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/Default.cshtml @@ -39,7 +39,7 @@
- @L["MarkdownSupported"] + @L["MarkdownSupported"]
@if (CmsKitCommentOptions.Value.IsRecaptchaEnabled) @@ -127,15 +127,32 @@ +
+ @L["MarkdownSupported"] +
+ + @if (CmsKitCommentOptions.Value.IsRecaptchaEnabled) + { + var output = Model.GetCaptcha(); +
+ +
+
+ +
+
+ +
+ +
+
+ }
@L["Update"] @L["Cancel"]
-
- @L["MarkdownSupported"] -
diff --git a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js index c5c26672f1..75f3288982 100644 --- a/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js +++ b/modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/default.js @@ -111,14 +111,23 @@ $form.submit(function (e) { e.preventDefault(); let formAsObject = $form.serializeFormToObject(); - volo.cmsKit.public.comments.commentPublic.update( - formAsObject.id, - { + $.ajax({ + type: 'POST', + url: '/CmsKitPublicComments/Update/' + formAsObject.id, + contentType: 'application/json; charset=utf-8', + dataType: 'json', + data: JSON.stringify({ text: formAsObject.commentText, - concurrencyStamp: formAsObject.commentConcurrencyStamp + concurrencyStamp: formAsObject.commentConcurrencyStamp, + captchaToken: formAsObject.captchaId, + captchaAnswer: formAsObject.input?.captcha + }), + success: function () { + widgetManager.refresh($widget); + }, + error: function (data) { + abp.message.error(data.responseJSON.error.message); } - ).then(function () { - widgetManager.refresh($widget); }); }); }); diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs index 30ebf9a96f..c0a5b1295f 100644 --- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs +++ b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs @@ -143,6 +143,27 @@ namespace Volo.Docs.Documents.FullSearch.Elastic CancellationToken cancellationToken = default) { ValidateElasticSearchEnabled(); + + FieldNameQueryBase query; + // if context starts with " or ends with " then we search for exact match + if (context.StartsWith("\"") && context.EndsWith("\"")) + { + context = context.Trim('"'); + + query = new MatchPhraseQuery + { + Query = context + }; + } + else + { + query = new MatchQuery + { + Query = context + }; + } + + query.Field = "content"; var request = new SearchRequest { @@ -152,11 +173,7 @@ namespace Volo.Docs.Documents.FullSearch.Elastic { Must = new QueryContainer[] { - new MatchQuery - { - Field = "content", - Query = context - } + query, }, Filter = new QueryContainer[] { diff --git a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs index 737bc2fe36..4898baf215 100644 --- a/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs +++ b/modules/docs/src/Volo.Docs.Web/Pages/Documents/Project/Index.cshtml.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Web; +using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.Extensions.Localization; @@ -108,6 +109,12 @@ namespace Volo.Docs.Pages.Documents.Project public virtual async Task OnGetAsync() { + var displayUrl = Request.GetDisplayUrl(); + var decodedUrl = HttpUtility.UrlDecode(displayUrl); + if (decodedUrl != displayUrl) + { + return Redirect(decodedUrl); + } try { return await SetPageAsync();