Merge branch 'dev' into auto-merge/rel-7-4/2183

pull/17623/head
Engincan VESKE 2 years ago committed by GitHub
commit 18fdf370a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
name: 💡 Feature request
description: Suggest an idea for this project
labels: [feature]
labels: [feature-request]
body:
- type: checkboxes
attributes:

@ -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.

@ -167,7 +167,7 @@
"ABPDiscordServer": "ABP سيرفر الدسكورد",
"ABPCommunityTalks": "برامج منتدى ABP الحوارية",
"ABPCommunityPosts": "منشورات منتدى ABP",
"BuyAndGetMonths": "شراء 12 شهر، <span class=\"text-info\">احصل على 14 شهرا!</span>",
"BuyAndGetMonths": "شراء 12 شهر، <span>احصل على 14 شهرا!</span>",
"GetYourDeal": "احصل على صفقتك",
"BuyOrRenewLicense": "اشترِ أو جدد الرخصة الآن واحصل على شهرين إضافيين!",
"BuyOrRenewLicenseToGetExtra2Months": "اشترِ أو جدد الرخصة الآن واحصل على شهرين إضافيين! اسرع! ⏰ آخر يوم: {0}",

@ -171,7 +171,7 @@
"ABPDiscordServer": "ABP Discord Server",
"ABPCommunityTalks": "ABP Community Talks",
"ABPCommunityPosts": "ABP Community Posts",
"BuyAndGetMonths": "BUY 12 MONTHS, <span class=\"text-info\">GET 14 MONTHS!</span>",
"BuyAndGetMonths": "BUY 12 MONTHS, <span>GET 14 MONTHS!</span>",
"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": "<strong>BLACK</strong> <span>FRIDAY</span>",
"ValidForExistingCustomers": "Also valid for the <br> existing customers!",
"CampaignBetweenDates": "From {0} <br>to {1}",
"CampaignBetweenDates": "From {0} <br>To {1}",
"SaveUpTo": "<span>SAVE</span> UP TO<strong>${0}K</strong>",
"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}<span>EXTRA MONTHS</span>"
}
}

@ -170,7 +170,7 @@
"ABPDiscordServer": "ABP Discord-palvelin",
"ABPCommunityTalks": "ABP Community Talks",
"ABPCommunityPosts": "ABP-yhteisön viestit",
"BuyAndGetMonths": "OSTA 12 KUUKAUTA, <span class=\"text-info\">SAAT 14 KUUKAUTA!</span>",
"BuyAndGetMonths": "OSTA 12 KUUKAUTA, <span>SAAT 14 KUUKAUTA!</span>",
"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}",

@ -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, <span class=\"text-info\">14 HÓNAPOT KAP!</span>",
"BuyAndGetMonths": "VÁSÁROLJON 12 HÓNAPOT, <span>14 HÓNAPOT KAP!</span>",
"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}",

@ -170,7 +170,7 @@
"ABPDiscordServer": "ABP Discord 服务器",
"ABPCommunityTalks": "ABP社区讲话",
"ABPCommunityPosts": "ABP社区文章",
"BuyAndGetMonths": "购买 12 个月,<span class=\"text-info\">获得 14 个月!</span>",
"BuyAndGetMonths": "购买 12 个月,<span>获得 14 个月!</span>",
"GetYourDeal": "得到你的交易",
"BuyOrRenewLicense": "立即购买或续订许可证并额外获得 2 个月!",
"BuyOrRenewLicenseToGetExtra2Months": "立即购买或续订 ABP 商业许可证(适用于所有版本)并额外获得 2 个月!",

@ -567,7 +567,7 @@
"TotalPrice": "Total Price",
"ThereIsNoInvoice": "There is no invoice",
"MyOrganizations_Detail_PaymentProviderInfo": "If you have purchased your license through <i>{0}</i> gateway, it sends the PDF invoice to your email address, see <a href=\"{1}\" target=\"_blank\">{0} invoicing.</a>",
"MyOrganizations_Detail_PayUInfo": "If you have purchased through the <i>PayU</i> gateway, click the \"Request Invoice\" button and fill in the billing information.",
"MyOrganizations_Detail_PayUInfo": "If you have purchased through the <i>Iyzico</i> 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 <span class=\"text-primary\">{0}</span> license",
"Continue": "Continue",
@ -1001,19 +1001,19 @@
"ABPSOLUTION": "ABP SOLUTION",
"CreatingAnEmptySolution_ABPSOLUTION_Description": "ABP provides a well-architected, layered and production-ready <a href=\"https://docs.abp.io/en/abp/latest/Startup-Templates/Application\" target=\"_blank\">startup solution</a> based on the <a href=\"https://docs.abp.io/en/abp/latest/Domain-Driven-Design\" target=\"_blank\">Domain Driven Design</a> principles. The solution also includes a pre-configured unit and integration <a href=\"https://docs.abp.io/en/abp/latest/Testing\" target=\"_blank\">test</a> 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 <a href=\"https://docs.abp.io/en/abp/latest/UI/AspNetCore/Theming\" target=\"_blank\"> theming</a> 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 <a href=\"/themes\">themes</a>.",
"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. <a href=\"https://docs.abp.io/en/abp/latest/Tutorials/Part-1\" target=\"_blank\">Tutorials</a> and <a href=\"https://docs.abp.io/en/abp/latest/Best-Practices/Index\" target=\"_blank\">best practice guides</a> 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 <span class=\"text-white\">get an additional 2 months</span> 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}"
}
}

@ -37,6 +37,7 @@
"ThisExtensionIsNotAllowed": "This extension is not allowed.",
"TheFileIsTooLarge": "The file is too large.",
"GoToThePost": "Go to the Post",
"GoToTheVideo": "Go to the Video",
"Contribute": "Contribute",
"OverallProgress": "Overall Progress",
"Done": "Done",
@ -189,6 +190,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 <span class=\"d-inline-block d-md-block gradient-community\">ABP Lovers</span>"
}
}

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Version>7.4.0-rc.3</Version>
<Version>8.0.0</Version>
<NoWarn>$(NoWarn);CS1591;CS0436</NoWarn>
<PackageIconUrl>https://abp.io/assets/abp_nupkg.png</PackageIconUrl>
<PackageProjectUrl>https://abp.io/</PackageProjectUrl>

@ -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<TextTemplateManagementOptions>(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 `<suite-custom-code-autogenerated>...</suite-custom-code-autogenerated>` 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!

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

@ -594,7 +594,7 @@ namespace BookStore.EntityFrameworkCore
/* Configure your own tables/entities inside here */
builder.Entity<Author>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Authors" + BookStoreConsts.DbSchema);
b.ToTable(BookStoreConsts.DbTablePrefix + "Authors", BookStoreConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name)
@ -608,7 +608,7 @@ namespace BookStore.EntityFrameworkCore
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books" + BookStoreConsts.DbSchema);
b.ToTable(BookStoreConsts.DbTablePrefix + "Books", BookStoreConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name)
@ -624,7 +624,7 @@ namespace BookStore.EntityFrameworkCore
builder.Entity<Category>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Categories" + BookStoreConsts.DbSchema);
b.ToTable(BookStoreConsts.DbTablePrefix + "Categories", BookStoreConsts.DbSchema);
b.ConfigureByConvention();
b.Property(x => x.Name)
@ -634,7 +634,7 @@ namespace BookStore.EntityFrameworkCore
builder.Entity<BookCategory>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "BookCategories" + BookStoreConsts.DbSchema);
b.ToTable(BookStoreConsts.DbTablePrefix + "BookCategories", BookStoreConsts.DbSchema);
b.ConfigureByConvention();
//define composite key

@ -0,0 +1,450 @@
# Cascading Option Loading with Extensions System in ABP Angular
This article will show how to load cascading options with an extensions system in ABP Angular. For this example, we'll simulate renting a book process. Besides our default form properties, we'll contribute `Name` property to our `Rent Form Modal` in the Books module. This property will be loaded after `Genre` is selected.
> Before starting this article, I suggest you read the [ABP Angular Dynamic Form Extensions](https://docs.abp.io/en/abp/latest/UI/Angular/Dynamic-Form-Extensions)
### Environment
- **ABP Framework Version:** ~7.3.0 (`~` means that use the latest patch version of the specified release)
- **DB Provider:** MongoDB
- **Angular Version:** ~16.0.0
### Project structure
The books module is not a library; for this demo, it'll placed in the application itself.
![Folder structure](./assets/img/folder-structure.png)
- **books folder:** Contains default form properties, tokens, models, etc. It's similar to the ABP module structure.
- Also I've used **standalone** and **signals** feature in this demo.
- **books-extended folder:** Contains only `Name` property for the contribute `Rent Form Modal` inside the Books module.
- **For more readability, I've used TS path aliases in this demo. Don't forget to export files in `index.ts` file 🙂**
![tsconfig.json file](./assets/img/ts-config-file.png)
### First look at the demo
![Cascading Loading Demo](assets/gif/cascading-loading-demo.gif)
### What is the Extension system?
![Extensions System Document](./assets/img/extensions-system-document.png)
# Reviewing the code step by step
**1. Create default form properties for `Rent Form` in the `Books` module**
- `getInjected` function is the key point of the cascading loading
- We can reach and track any value from `Service` or `Component`
- In that way we can load options according to the selected value
```ts
// ~/books/defaults/default-books-form.props.ts
import { Validators } from "@angular/forms";
import { map, of } from "rxjs";
import { ePropType, FormProp } from "@abp/ng.theme.shared/extensions";
import { BookDto, AuthorService, BooksService } from "../proxy";
import { RentBookComponent } from "../components";
import { DefaultOption } from "../utils";
const { required } = Validators;
export const DEFAULT_RENT_FORM_PROPS = FormProp.createMany<BookDto>([
{
type: ePropType.String,
id: "authorId",
name: "authorId",
displayName: "BookStore::Author",
defaultValue: null,
validators: () => [required],
options: (data) => {
const { authors } = data.getInjected(AuthorService);
return of([
DefaultOption,
...authors().map((author) => ({ value: author.id, key: author.name })),
]);
},
},
{
type: ePropType.String,
id: "genreId",
name: "genreId",
displayName: "BookStore::Genre",
defaultValue: null,
validators: () => [required],
options: (data) => {
const rentBookComponent = data.getInjected(RentBookComponent);
const { genres } = data.getInjected(BooksService);
const genreOptions = genres().map(({ id, name }) => ({
value: id,
key: name,
}));
return rentBookComponent.form.controls.authorId.valueChanges.pipe(
map((value: string | undefined) =>
value ? [DefaultOption, ...genreOptions] : [DefaultOption]
)
);
},
},
{
type: ePropType.Date,
id: "returnDate",
name: "returnDate",
displayName: "BookStore::ReturnDate",
defaultValue: null,
validators: () => [required],
},
]);
```
**2. Configure tokens and config options**
The documentation explains these steps; that's why I won't explain it again. If documents or samples are not enough, please let me know in the comments 🙂
**Extensions Token**
```ts
// ~/books/tokens/extensions.token.ts
import { CreateFormPropContributorCallback } from "@abp/ng.theme.shared/extensions";
import { InjectionToken } from "@angular/core";
import { BookDto } from "../proxy";
import { eBooksComponents } from "../enums";
import { DEFAULT_RENT_FORM_PROPS } from "../defaults";
export const DEFAULT_BOOK_STORE_CREATE_FORM_PROPS = {
[eBooksComponents.RentBook]: DEFAULT_RENT_FORM_PROPS,
};
export const BOOK_STORE_RENT_FORM_PROP_CONTRIBUTORS =
new InjectionToken<CreateFormPropContributors>(
"BOOK_STORE_RENT_FORM_PROP_CONTRIBUTORS"
);
type CreateFormPropContributors = Partial<{
[eBooksComponents.RentBook]: CreateFormPropContributorCallback<BookDto>[];
/**
* Other creation form prop contributors...
*/
// [eBooksComponents.CreateBook]: CreateFormPropContributorCallback<BookDto>[];
}>;
```
**Extensions Config Option**
```ts
// ~/books/models/config-options.ts
import { CreateFormPropContributorCallback } from "@abp/ng.theme.shared/extensions";
import { BookDto } from "../proxy";
import { eBooksComponents } from "../enums";
export type BookStoreRentFormPropContributors = Partial<{
[eBooksComponents.RentBook]: CreateFormPropContributorCallback<BookDto>[];
}>;
export interface BooksConfigOptions {
rentFormPropContributors?: BookStoreRentFormPropContributors;
}
```
**3. Extensions Guard**
It'll to collect all contributors from [ExtensionsService](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/theme-shared/extensions/src/lib/services/extensions.service.ts)
```ts
// ~/books/guards/extensions.guard.ts
import { Injectable, inject } from "@angular/core";
import { Observable, map, tap } from "rxjs";
import { ConfigStateService, IAbpGuard } from "@abp/ng.core";
import {
ExtensionsService,
getObjectExtensionEntitiesFromStore,
mapEntitiesToContributors,
mergeWithDefaultProps,
} from "@abp/ng.theme.shared/extensions";
import {
BOOK_STORE_RENT_FORM_PROP_CONTRIBUTORS,
DEFAULT_BOOK_STORE_CREATE_FORM_PROPS,
} from "../tokens";
@Injectable()
export class BooksExtensionsGuard implements IAbpGuard {
protected readonly configState = inject(ConfigStateService);
protected readonly extensions = inject(ExtensionsService);
canActivate(): Observable<boolean> {
const createFormContributors =
inject(BOOK_STORE_RENT_FORM_PROP_CONTRIBUTORS, { optional: true }) || {};
return getObjectExtensionEntitiesFromStore(
this.configState,
"BookStore"
).pipe(
mapEntitiesToContributors(this.configState, "BookStore"),
tap((objectExtensionContributors) => {
mergeWithDefaultProps(
this.extensions.createFormProps,
DEFAULT_BOOK_STORE_CREATE_FORM_PROPS,
objectExtensionContributors.createForm,
createFormContributors
);
}),
map(() => true)
);
}
}
```
Yes, I'm still using class-based guard 🙂 much more flexible...
**4. RentBookComponent**
- Our trackable variable is defined here `(form:FormGroup)`, which means We'll track this variable in `options` property at defaults || contributors files.
- Providing `AuthorService`, also `EXTENSIONS_IDENTIFIER` for the reach dynamic properties
```ts
import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Injector,
Output,
inject,
} from "@angular/core";
import { FormGroup } from "@angular/forms";
import { CoreModule, uuid } from "@abp/ng.core";
import { ThemeSharedModule } from "@abp/ng.theme.shared";
import {
EXTENSIONS_IDENTIFIER,
FormPropData,
UiExtensionsModule,
generateFormFromProps,
} from "@abp/ng.theme.shared/extensions";
import { AuthorService, BookDto, BooksService } from "../../proxy";
import { eBooksComponents } from "../../enums";
@Component({
standalone: true,
selector: "app-rent-book",
templateUrl: "./rent-book.component.html",
imports: [CoreModule, UiExtensionsModule, ThemeSharedModule],
providers: [
{
provide: EXTENSIONS_IDENTIFIER,
useValue: eBooksComponents.RentBook,
},
AuthorService,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RentBookComponent {
protected readonly injector = inject(Injector);
protected readonly authorService = inject(AuthorService);
protected readonly booksService = inject(BooksService);
//#region Just for demo
readonly #authors = this.authorService.authors();
readonly #genres = this.booksService.genres();
readonly #books = this.booksService.books();
//#endregion
protected modalVisible = true;
@Output() modalVisibleChange = new EventEmitter<boolean>();
selected: BookDto;
form: FormGroup;
modalBusy = false;
protected buildForm(): void {
const data = new FormPropData(this.injector, this.selected);
this.form = generateFormFromProps(data);
}
constructor() {
this.buildForm();
}
save(): void {
if (this.form.invalid) {
return;
}
this.modalBusy = true;
const { authorId, genreId, bookId, returnDate } = this.form.value;
//#region Just for demo
const authorName = this.#authors.find(({ id }) => id === authorId).name;
const genreName = this.#genres.find(({ id }) => id === genreId).name;
const bookName = this.#books.find(({ id }) => id === bookId).name;
//#endregion
this.booksService.rentedBooks.update((books) => [
{
id: uuid(),
name: bookName,
author: authorName,
genre: genreName,
returnDate,
},
...books,
]);
this.modalBusy = false;
this.modalVisible = false;
}
}
```
```html
<abp-modal
[visible]="modalVisible"
[busy]="modalBusy"
(visibleChange)="modalVisibleChange.next($event)"
>
<ng-template #abpHeader>
<h3>{{ 'BookStore::RentABook' | abpLocalization }}</h3>
</ng-template>
<ng-template #abpBody>
<ng-template #loaderRef>
<div class="text-center">
<i class="fa fa-pulse fa-spinner" aria-hidden="true"></i>
</div>
</ng-template>
<form
*ngIf="form; else loaderRef"
[formGroup]="form"
(ngSubmit)="save()"
validateOnSubmit
>
<abp-extensible-form [selectedRecord]="selected"></abp-extensible-form>
</form>
</ng-template>
<ng-template #abpFooter>
<button abpClose type="button" class="btn btn-secondary">
{{ 'AbpIdentity::Cancel' | abpLocalization }}
</button>
<abp-button
iconClass="fa fa-check"
[disabled]="form?.invalid"
(click)="save()"
>
{{ 'AbpIdentity::Save' | abpLocalization }}
</abp-button>
</ng-template>
</abp-modal>
```
Up to now, we have constructed our module's default form properties.
- As you can see, there are no book names we'll add them via contributors
![Rent Form Without Contribution](./assets/img/rent-form-without-contribution.png)
## Next, add new property dynamically (book name list as dropdown)
- Created new folder ./src/app/books-extended
- Create contributors/form-prop.contributors.ts
```ts
// ~/books-extened/contributors/form-prop.contributors.ts
import { Validators } from "@angular/forms";
import { map } from "rxjs";
import {
ePropType,
FormProp,
FormPropList,
} from "@abp/ng.theme.shared/extensions";
import {
BookDto,
BookStoreRentFormPropContributors,
BooksService,
DefaultOption,
RentBookComponent,
eBooksComponents,
} from "@book-store/books";
const { required, maxLength } = Validators;
const bookIdProp = new FormProp<BookDto>({
type: ePropType.String,
id: "bookId",
name: "bookId",
displayName: "BookStore::Name",
options: (data) => {
const rentBook = data.getInjected(RentBookComponent);
const { books } = data.getInjected(BooksService);
const bookOptions = books().map(({ id, name }) => ({
value: id,
key: name,
}));
return rentBook.form.controls.genreId.valueChanges.pipe(
map((value: string | undefined) =>
value ? [DefaultOption, ...bookOptions] : [DefaultOption]
)
);
},
validators: () => [required, maxLength(255)],
});
export function bookIdPropContributor(propList: FormPropList<BookDto>) {
propList.addByIndex(bookIdProp, 2);
}
export const bookStoreRentFormPropContributors: BookStoreRentFormPropContributors =
{
[eBooksComponents.RentBook]: [bookIdPropContributor],
};
```
- Load new contributions via routing & forLazy method
```ts
// ~/app-routing.module.ts
import { bookStoreRentFormPropContributors } from "./books-extended/contributors/form-prop.contributors";
const routes: Routes = [
// other routes...
{
path: "books",
loadChildren: () =>
import("@book-store/books").then((m) =>
m.BooksModule.forLazy({
rentFormPropContributors: bookStoreRentFormPropContributors,
})
),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes, {})],
exports: [RouterModule],
})
export class AppRoutingModule {}
```
Finally, we've added a new property to our module, and it'll be loaded after `Genre` is selected.
## Conclusion
![Cascading Loading Demo](assets/gif/cascading-loading-demo.gif)
- In ABP Angular, we can create form properties and load dropdown options dynamically via the Extensions System
- We can reach and track any value from `Service` or `Component`
- We can create our custom library or module and contribute it to any module in the application
Thanks for reading, I hope it was helpful. If you have any questions, please let me know in the comments section. 👋👋
> You can find the source code of this article on [Github](https://github.com/abpframework/abp-samples/tree/master/AngularCascadingOptionLoading/Volo.BookStore)

@ -58,7 +58,11 @@ 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**.
* **additionalEmailSendingArgs**: This parameter is used to pass additional arguments to the `IEmailSender` implementation. Include: CC(Carbon copy), a list of `EmailAttachment` and an extra properties.
> `IEmailSender` is the suggested way to send emails, since it makes your code provider independent.

@ -173,7 +173,7 @@ First step is to change the connection string section inside all the `appsetting
````json
"ConnectionStrings": {
"Default": "Server=(LocalDb)\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
}
````
@ -184,7 +184,7 @@ Change it as shown below:
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True",
"AbpPermissionManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpSettingManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpAuditLogging": "Server=(LocalDb)\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True"
"AbpAuditLogging": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True"
}
````

@ -594,6 +594,18 @@ 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. If you need to track the entities, you can still use the [AsTracking()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) extension method on the LINQ expression, or `EnableTracking()` extension method on the repository object (See *Enabling / Disabling the Change Tracking* section in this document).
> 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.
## Enabling / Disabling the Change Tracking
In addition to the read-only repositories, ABP allows to manually control the change tracking behavior for querying objects. Please see the *Enabling / Disabling the Change Tracking* section of the [Repositories documentation](Repositories.md) to learn how to use it.
## 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:

@ -20,7 +20,7 @@ Check the **connection string** in the `appsettings.json` file under the `YourPr
````json
"ConnectionStrings": {
"Default": "Server=(LocalDb)\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
}
````

@ -21,7 +21,7 @@ Check the **connection string** in the `appsettings.json` file under the {{if Ti
````json
"ConnectionStrings": {
"Default": "Server=(LocalDb)\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
}
````

@ -320,9 +320,22 @@ public class MyCustomUserMapper : IObjectMapper<User, UserDto>, 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<TSource, TDestination>` 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<TSource, TDestination>` 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<User, UserDto>`, ABP can automatically convert a collection of `User` objects to a collection of `UserDto` objects. The following generic collection types are supported:
* `IEnumerable<T>`
* `ICollection<T>`
* `Collection<T>`
* `IList<T>`
* `List<T>`
* `T[]` (array)
**Example:**
````csharp
var users = await _userRepository.GetListAsync(); // returns List<User>
var dtos = ObjectMapper.Map<List<User>, List<UserDto>>(users); // creates List<UserDto>
````

@ -176,6 +176,77 @@ Some features (like soft-delete, multi-tenancy and audit logging) won't work, so
The `EnsureExistsAsync` extension method accepts entity id or entities query expression to ensure entities exist, otherwise, it will throw `EntityNotFoundException`.
### Enabling / Disabling the Change Tracking
ABP provides repository extension methods and attributes those can be used to control the change tracking behavior for queried entities in the underlying database provider.
Disabling change tracking can gain performance if you query many entities from the database for read-only purposes. Querying single or a few entities won't make much performance difference, but you are free to use it whenever you like.
> If the underlying database provider doesn't support change tracking, then this system won't have any effect. [Entity Framework Core](Entity-Framework-Core.md) supports change tracking, for example, while the [MongoDB](MongoDB.md) provider doesn't support it.
#### Repository Extension Methods for Change Tracking
Change tracking is enabled unless you explicitly disable it.
**Example: Using the `DisableTracking` extension method**
````csharp
public class MyDemoService : ApplicationService
{
private readonly IRepository<Person, Guid> _personRepository;
public MyDemoService(IRepository<Person, Guid> personRepository)
{
_personRepository = personRepository;
}
public async Task DoItAsync()
{
// Change tracking is enabled in that point (by default)
using (_personRepository.DisableTracking())
{
// Change tracking is disabled in that point
var list = await _personRepository.GetPagedListAsync(0, 100, "Name ASC");
}
// Change tracking is enabled in that point (by default)
}
}
````
> `DisableTracking` extension method returns a `IDisposable` object, so you can safely **restore** the change tracking behavior to the **previous state** one the `using` block ends. Basically, `DisableTracking` method ensures that the change tracking is disabled inside the `using` block, but doesn't affect outside of the `using` block. That means, if change tracking was already disabled, `DisableTracking` and the disposable return value do nothing.
`EnableTracking()` method works exactly opposite to the `DisableTracking()` method. You typically won't use it (because the change tracking is already enabled by default), but it is there in case of you need that.
#### Attributes for Change Tracking
You typically use the `DisableTracking()` method for the application service methods those only returns data, but doesn't make any change on entities. For such cases, you can use the `DisableEntityChangeTracking` attribute on your method/class as a shortcut to disable the change tracking for whole method body.
**Example: Using the `DisableEntityChangeTracking` attribute on a method**
````csharp
[DisableEntityChangeTracking]
public virtual async Task<List<PersonDto>> GetListAsync()
{
/* We disabled the change tracking in this method
because we won't change the people objects */
var people = await _personRepository.GetListAsync();
return ObjectMapper.Map<List<Person>, List<PersonDto>(people);
}
````
`EnableEntityChangeTracking` can be used for the opposite purpose, and it ensures that the change tracking is enabled for a given method. Since the change tracking is enabled by default, `EnableEntityChangeTracking` may be needed only if you know that your method is called from a context that disables the change tracking.
`DisableEntityChangeTracking` and `EnableEntityChangeTracking` attributes can be used on a **method** or on a **class** (which affects all of the class methods).
ABP uses dynamic proxying to make these attributes working. There are some rules here:
* If you are **not injecting** the service over an interface (like `IPersonAppService`), then the methods of the service must be `virtual`. Otherwise, [dynamic proxy / interception](Dynamic-Proxying-Interceptors.md) system can not work.
* Only `async` methods (methods returning a `Task` or `Task<T>`) are intercepted.
> Change tracking behavior doesn't affect tracking entity objects returned from `InsertAsync` and `UpdateAsync` methods. The objects returned from these methods are always tracked (if the underlying provider has the change tracking feature) and any change you made to these objects are saved into the database.
## Other Generic Repository Types
Standard `IRepository<TEntity, TKey>` interface exposes the standard `IQueryable<TEntity>` and you can freely query using the standard LINQ methods. This is fine for most of the applications. However, some ORM providers or database systems may not support standard `IQueryable` interface. If you want to use such providers, you can't rely on the `IQueryable`.
@ -205,8 +276,6 @@ Methods:
- `WithDetails()` 1 overload
- `WithDetailsAsync()` 1 overload
Where as the `IReadOnlyBasicRepository<Tentity, TKey>` provides the following methods:
- `GetCountAsync()`
@ -217,6 +286,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 use the [AsTracking()](https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.entityframeworkqueryableextensions.astracking) extension method on the LINQ expression, or `EnableTracking()` extension method on the repository object (See *Enabling / Disabling the Change Tracking* section in this document).
> 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<TEntity, TKey>` (or basic/readonly versions) defined above. In that case, you can inject and use `IRepository<TEntity>` for your entity.

@ -198,23 +198,10 @@ ABP Framework doesn't provide any infrastructure to test your JavaScript code. Y
> Volo.Abp.AspNetCore.TestBase package is already installed in the `.Web.Tests` project.
This package provides the `AbpAspNetCoreIntegratedTestBase` as the fundamental base class to derive the test classes from. The `MyProjectWebTestBase` base class used above inherits from the `AbpAspNetCoreIntegratedTestBase`, so we indirectly inherited the `AbpAspNetCoreIntegratedTestBase`.
This package provides the `AbpWebApplicationFactoryIntegratedTest` as the fundamental base class to derive the test classes from. It's inherited from the [WebApplicationFactory](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests) class provided by the ASP.NET Core.
### Base Properties
The `AbpAspNetCoreIntegratedTestBase` provides the following base properties those are used in the tests:
* `Server`: A `TestServer` instance that hosts the web application in tests.
* `Client`: An `HttpClient` instance that is configured to perform requests to the test server.
* `ServiceProvider`: The service provider that you can resolve services in case of need.
### Base Methods
`AbpAspNetCoreIntegratedTestBase` provides the following methods that you can override if you need to customize the test server:
* `ConfigureServices` can be overridden to register/replace services only for the derived test class.
* `CreateHostBuilder` can be used to customize building the `IHostBuilder`.
The `MyProjectWebTestBase` base class used above inherits from the `AbpWebApplicationFactoryIntegratedTest`, so we indirectly inherited the `AbpWebApplicationFactoryIntegratedTest`.
See Also
* [Integration tests in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/test/integration-tests)
* [Overall / Server Side Testing](../../Testing.md)

@ -21,7 +21,7 @@
````json
"ConnectionStrings": {
"Default": "Server=(LocalDb)\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
}
````

@ -198,23 +198,10 @@ ABP框架不提供任何基础设施来测试JavaScript代码. 你可以使用
> Volo.Abp.AspNetCore.TestBase 已经安装在 `.Web.Tests` 项目中.
此包提供的`AbpAspNetCoreIntegratedTestBase`作为派生测试类的基类. 上面使用的`MyProjectWebTestBase`继承自`AbpAspNetCoreIntegratedTestBase`, 因此我们间接继承了`AbpAspNetCoreIntegratedTestBase`.
### 基本属性
`AbpAspNetCoreIntegratedTestBase` 提供了测试中使用的以下基本属性:
* `Server`: 在测试中托管web应用程序的`TestServer`实例.
* `Client`: 为执行对测试服务器的请求配置`HttpClient`实例.
* `ServiceProvider`: 可以在你需要时处理服务提供服务.
### 基本方法
`AbpAspNetCoreIntegratedTestBase` 提供了以下方法, 如果需要自定义测试服务器, 可以重写这些方法:
* `ConfigureServices` 仅为派生测试类注册/替换服务时可以重写使用.
* `CreateHostBuilder` 可用于自定义生成 `IHostBuilder`.
此包提供的`AbpWebApplicationFactoryIntegratedTest`作为派生测试类的基类. 它继承自ASP.NET Core提供的[WebApplicationFactory](https://learn.microsoft.com/zh-cn/aspnet/core/test/integration-tests)类。
上面使用的`MyProjectWebTestBase`继承自`AbpWebApplicationFactoryIntegratedTest`, 因此我们间接继承了`AbpWebApplicationFactoryIntegratedTest`.
另请参阅
* [ASP.NET Core 中的集成测试](https://learn.microsoft.com/zh-cn/aspnet/core/test/integration-tests)
* [总览/服务器端测试](../../Testing.md)

@ -30,7 +30,7 @@ namespace Dapr
/// <summary>
/// An optional delegate used to configure the subscriptions.
/// </summary>
public Func<List<AbpSubscription>, Task> SubscriptionsCallback { get; set; }
public Func<List<AbpSubscription>, Task>? SubscriptionsCallback { get; set; }
}
/// <summary>
@ -41,32 +41,32 @@ namespace Dapr
/// <summary>
/// Gets or sets the topic name.
/// </summary>
public string Topic { get; set; }
public string Topic { get; set; } = default!;
/// <summary>
/// Gets or sets the pubsub name
/// </summary>
public string PubsubName { get; set; }
public string PubsubName { get; set; } = default!;
/// <summary>
/// Gets or sets the route
/// </summary>
public string Route { get; set; }
public string? Route { get; set; }
/// <summary>
/// Gets or sets the routes
/// </summary>
public AbpRoutes Routes { get; set; }
public AbpRoutes? Routes { get; set; }
/// <summary>
/// Gets or sets the metadata.
/// </summary>
public AbpMetadata Metadata { get; set; }
public AbpMetadata? Metadata { get; set; }
/// <summary>
/// Gets or sets the deadletter topic.
/// </summary>
public string DeadLetterTopic { get; set; }
public string? DeadLetterTopic { get; set; }
}
/// <summary>
@ -99,12 +99,12 @@ namespace Dapr
/// <summary>
/// Gets or sets the default route
/// </summary>
public string Default { get; set; }
public string? Default { get; set; }
/// <summary>
/// Gets or sets the routing rules
/// </summary>
public List<AbpRule> Rules { get; set; }
public List<AbpRule>? Rules { get; set; }
}
/// <summary>
@ -115,12 +115,12 @@ namespace Dapr
/// <summary>
/// Gets or sets the CEL expression to match this route.
/// </summary>
public string Match { get; set; }
public string Match { get; set; } = default!;
/// <summary>
/// Gets or sets the path of the route.
/// </summary>
public string Path { get; set; }
public string Path { get; set; } = default!;
}
}
@ -166,7 +166,7 @@ namespace Microsoft.AspNetCore.Builder
return CreateSubscribeEndPoint(endpoints, options);
}
private static IEndpointConventionBuilder CreateSubscribeEndPoint(IEndpointRouteBuilder endpoints, AbpSubscribeOptions options = null)
private static IEndpointConventionBuilder CreateSubscribeEndPoint(IEndpointRouteBuilder endpoints, AbpSubscribeOptions? options = null)
{
if (endpoints is null)
{
@ -175,7 +175,7 @@ namespace Microsoft.AspNetCore.Builder
return endpoints.MapGet("dapr/subscribe", async context =>
{
var logger = context.RequestServices.GetRequiredService<ILoggerFactory>().CreateLogger("DaprTopicSubscription");
var logger = context.RequestServices.GetService<ILoggerFactory>()?.CreateLogger("DaprTopicSubscription");
var dataSource = context.RequestServices.GetRequiredService<EndpointDataSource>();
var subscriptions = dataSource.Endpoints
.OfType<RouteEndpoint>()
@ -185,7 +185,7 @@ namespace Microsoft.AspNetCore.Builder
var topicMetadata = e.Metadata.GetOrderedMetadata<ITopicMetadata>();
var originalTopicMetadata = e.Metadata.GetOrderedMetadata<IOriginalTopicMetadata>();
var subs = new List<(string PubsubName, string Name, string DeadLetterTopic, bool? EnableRawPayload, string Match, int Priority, Dictionary<string, string[]> OriginalTopicMetadata, string MetadataSeparator, RoutePattern RoutePattern)>();
var subs = new List<(string PubsubName, string Name, string? DeadLetterTopic, bool? EnableRawPayload, string Match, int Priority, Dictionary<string, string[]> OriginalTopicMetadata, string? MetadataSeparator, RoutePattern RoutePattern)>();
for (int i = 0; i < topicMetadata.Count(); i++)
{
@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.Builder
{
var first = e.First();
var rawPayload = e.Any(e => e.EnableRawPayload.GetValueOrDefault());
var metadataSeparator = e.FirstOrDefault(e => !string.IsNullOrEmpty(e.MetadataSeparator)).MetadataSeparator ?? ",";
var metadataSeparator = e.FirstOrDefault(e => !string.IsNullOrEmpty(e.MetadataSeparator)).MetadataSeparator?.ToString() ?? ",";
var rules = e.Where(e => !string.IsNullOrEmpty(e.Match)).ToList();
var defaultRoutes = e.Where(e => string.IsNullOrEmpty(e.Match)).Select(e => RoutePatternToString(e.RoutePattern)).ToList();
var defaultRoute = defaultRoutes.FirstOrDefault();
@ -276,7 +276,7 @@ namespace Microsoft.AspNetCore.Builder
.OrderBy(e => (e.PubsubName, e.Topic))
.ToList();
await options?.SubscriptionsCallback(subscriptions);
await options?.SubscriptionsCallback!(subscriptions)!;
await context.Response.WriteAsync(JsonSerializer.Serialize(subscriptions,
new JsonSerializerOptions
{

@ -5,6 +5,8 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<RootNamespace />
</PropertyGroup>

@ -41,8 +41,8 @@ public class AbpAspNetCoreMvcDaprEventsController : AbpController
}
else
{
var eventData = daprSerializer.Deserialize(data, distributedEventBus.GetEventType(topic));
await distributedEventBus.TriggerHandlersAsync(distributedEventBus.GetEventType(topic), eventData);
var eventData = daprSerializer.Deserialize(data, distributedEventBus.GetEventType(topic!));
await distributedEventBus.TriggerHandlersAsync(distributedEventBus.GetEventType(topic!), eventData);
}
return Ok();

@ -5,6 +5,8 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<WarningsAsErrors>Nullable</WarningsAsErrors>
<RootNamespace />
</PropertyGroup>

@ -50,9 +50,9 @@ public class DaprAppApiTokenValidator : IDaprAppApiTokenValidator, ISingletonDep
return expectedAppApiToken == headerAppApiToken;
}
public virtual string GetDaprAppApiTokenOrNull()
public virtual string? GetDaprAppApiTokenOrNull()
{
string apiTokenHeader = HttpContext.Request.Headers["dapr-api-token"];
string? apiTokenHeader = HttpContext.Request.Headers["dapr-api-token"];
if (string.IsNullOrEmpty(apiTokenHeader) || apiTokenHeader.Length < 1)
{
return null;
@ -61,7 +61,7 @@ public class DaprAppApiTokenValidator : IDaprAppApiTokenValidator, ISingletonDep
return apiTokenHeader;
}
protected virtual string GetConfiguredAppApiTokenOrNull()
protected virtual string? GetConfiguredAppApiTokenOrNull()
{
return HttpContext
.RequestServices

@ -22,7 +22,7 @@ public static class DaprHttpContextExtensions
.IsValidDaprAppApiToken();
}
public static string GetDaprAppApiTokenOrNull(HttpContext httpContext)
public static string? GetDaprAppApiTokenOrNull(HttpContext httpContext)
{
return httpContext
.RequestServices

@ -6,5 +6,5 @@ public interface IDaprAppApiTokenValidator
bool IsValidDaprAppApiToken();
string GetDaprAppApiTokenOrNull();
string? GetDaprAppApiTokenOrNull();
}

@ -145,7 +145,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide
ActionApiDescriptionModel.Create(
uniqueMethodName,
method,
apiDescription.RelativePath,
apiDescription.RelativePath!,
apiDescription.HttpMethod,
GetSupportedVersions(controllerType, method, setting),
allowAnonymous,

@ -40,11 +40,11 @@ public class AbpModelMetadataProvider : DefaultModelMetadataProvider
{
foreach (var validationAttribute in detail.ModelAttributes.Attributes.OfType<ValidationAttribute>())
{
NormalizeValidationAttrbute(validationAttribute);
NormalizeValidationAttribute(validationAttribute);
}
}
protected virtual void NormalizeValidationAttrbute(ValidationAttribute validationAttribute)
protected virtual void NormalizeValidationAttribute(ValidationAttribute validationAttribute)
{
if (validationAttribute.ErrorMessage == null)
{

@ -32,7 +32,7 @@ public class ServiceProxyGenerationModel
public ProxyScriptingModel CreateOptions()
{
var options = new ProxyScriptingModel(Type, UseCache);
var options = new ProxyScriptingModel(Type!, UseCache);
if (!Modules.IsNullOrEmpty())
{

@ -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
}
}
}

@ -28,6 +28,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCorePackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftAspNetCorePackageVersion)" />
</ItemGroup>
</Project>

@ -13,6 +13,7 @@ using Volo.Abp.Modularity;
namespace Volo.Abp.AspNetCore.TestBase;
[Obsolete("Use AbpWebApplicationFactoryIntegratedTest instead.")]
public class AbpAspNetCoreAsyncIntegratedTestBase<TModule>
where TModule : IAbpModule
{

@ -14,6 +14,7 @@ namespace Volo.Abp.AspNetCore.TestBase;
/// <typeparam name="TStartupModule">
/// Can be a module type or old-style ASP.NET Core Startup class.
/// </typeparam>
[Obsolete("Use AbpWebApplicationFactoryIntegratedTest instead.")]
public abstract class AbpAspNetCoreIntegratedTestBase<TStartupModule> : AbpTestBaseWithServiceProvider, IDisposable
where TStartupModule : class
{
@ -51,7 +52,7 @@ public abstract class AbpAspNetCoreIntegratedTestBase<TStartupModule> : AbpTestB
{
webBuilder.UseStartup<TStartupModule>();
}
webBuilder.UseAbpTestServer();
})
.UseAutofac()

@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Volo.Abp.AspNetCore.TestBase;
public abstract class AbpWebApplicationFactoryIntegratedTest<TProgram> : WebApplicationFactory<TProgram>
where TProgram : class
{
protected HttpClient Client { get; set; }
protected IServiceProvider ServiceProvider => Services;
protected AbpWebApplicationFactoryIntegratedTest()
{
Client = CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false
});
ServiceProvider.GetRequiredService<ITestServerAccessor>().Server = Server;
}
protected override IHost CreateHost(IHostBuilder builder)
{
builder.ConfigureServices(ConfigureServices);
return base.CreateHost(builder);
}
protected virtual T? GetService<T>()
{
return Services.GetService<T>();
}
protected virtual T GetRequiredService<T>() where T : notnull
{
return Services.GetRequiredService<T>();
}
protected virtual void ConfigureServices(IServiceCollection services)
{
}
#region GetUrl
/// <summary>
/// Gets default URL for given controller type.
/// </summary>
/// <typeparam name="TController">The type of the controller.</typeparam>
protected virtual string GetUrl<TController>()
{
return "/" + typeof(TController).Name.RemovePostFix("Controller", "AppService", "ApplicationService", "IntService", "IntegrationService", "Service");
}
/// <summary>
/// Gets default URL for given controller type's given action.
/// </summary>
/// <typeparam name="TController">The type of the controller.</typeparam>
protected virtual string GetUrl<TController>(string actionName)
{
return GetUrl<TController>() + "/" + actionName;
}
/// <summary>
/// Gets default URL for given controller type's given action with query string parameters (as anonymous object).
/// </summary>
/// <typeparam name="TController">The type of the controller.</typeparam>
protected virtual string GetUrl<TController>(string actionName, object queryStringParamsAsAnonymousObject)
{
var url = GetUrl<TController>(actionName);
var dictionary = new RouteValueDictionary(queryStringParamsAsAnonymousObject);
if (dictionary.Any())
{
url += "?" + dictionary.Select(d => $"{d.Key}={d.Value}").JoinAsString("&");
}
return url;
}
#endregion
}

@ -0,0 +1,27 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Volo.Abp.Modularity;
namespace Volo.Abp.AspNetCore.TestBase;
public static class WebApplicationBuilderExtensions
{
public async static Task RunAbpModuleAsync<TModule>(this WebApplicationBuilder builder, Action<AbpApplicationCreationOptions>? optionsAction = null)
where TModule : IAbpModule
{
var assemblyName = typeof(TModule).Assembly.GetName()?.Name;
if (!assemblyName.IsNullOrWhiteSpace())
{
// Set the application name as the assembly name of the module will automatically add assembly to the ApplicationParts of MVC application.
builder.Environment.ApplicationName = assemblyName!;
}
builder.Host.UseAutofac();
await builder.AddApplicationAsync<TModule>(optionsAction);
var app = builder.Build();
await app.InitializeApplicationAsync();
await app.RunAsync();
}
}

@ -98,13 +98,13 @@ public class AbpAuditingMiddleware : IMiddleware, ITransientDependency
{
return false;
}
if (!AuditingOptions.IsEnabledForIntegrationServices &&
if (!AuditingOptions.IsEnabledForIntegrationServices &&
context.Request.Path.Value.StartsWith($"/{AbpAspNetCoreConsts.DefaultIntegrationServiceApiPrefix}/"))
{
return true;
}
if (AspNetCoreAuditingOptions.IgnoredUrls.Any(x => context.Request.Path.Value.StartsWith(x)))
{
return true;
@ -134,7 +134,8 @@ public class AbpAuditingMiddleware : IMiddleware, ITransientDependency
}
if (!AuditingOptions.IsEnabledForGetRequests &&
string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase))
(string.Equals(httpContext.Request.Method, HttpMethods.Get, StringComparison.OrdinalIgnoreCase) ||
string.Equals(httpContext.Request.Method, HttpMethods.Head, StringComparison.OrdinalIgnoreCase)))
{
return false;
}

@ -1,9 +1,6 @@
using JetBrains.Annotations;
namespace Volo.Abp.Auditing;
namespace Volo.Abp.Auditing;
public interface IAuditLogScope
{
[NotNull]
AuditLogInfo Log { get; }
}

@ -1,6 +1,4 @@
using JetBrains.Annotations;
namespace Volo.Abp.Auditing;
namespace Volo.Abp.Auditing;
public interface IAuditingManager
{

@ -1,12 +1,11 @@
using System.Collections.Generic;
using Volo.Abp;
using Volo.Abp.AutoMapper;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;
namespace AutoMapper;
public static class AbpAutoMapperExtensibleDtoExtensions
public static class AbpAutoMapperExtensibleObjectExtensions
{
public static IMappingExpression<TSource, TDestination> MapExtraProperties<TSource, TDestination>(
this IMappingExpression<TSource, TDestination> mappingExpression,

@ -14,7 +14,7 @@ public class AzureServiceBusMessageConsumerFactory : IAzureServiceBusMessageCons
ServiceScope = serviceScopeFactory.CreateScope();
}
public IAzureServiceBusMessageConsumer CreateMessageConsumer(string topicName, string subscriptionName, string connectionName)
public IAzureServiceBusMessageConsumer CreateMessageConsumer(string topicName, string subscriptionName, string? connectionName)
{
var processor = ServiceScope.ServiceProvider.GetRequiredService<AzureServiceBusMessageConsumer>();
processor.Initialize(topicName, subscriptionName, connectionName);

@ -28,7 +28,7 @@ public class ConnectionPool : IConnectionPool, ISingletonDependency
Logger = new NullLogger<ConnectionPool>();
}
public ServiceBusClient GetClient(string connectionName)
public ServiceBusClient GetClient(string? connectionName)
{
connectionName ??= AzureServiceBusConnections.DefaultConnectionName;
return _clients.GetOrAdd(
@ -40,7 +40,7 @@ public class ConnectionPool : IConnectionPool, ISingletonDependency
).Value;
}
public ServiceBusAdministrationClient GetAdministrationClient(string connectionName)
public ServiceBusAdministrationClient GetAdministrationClient(string? connectionName)
{
connectionName ??= AzureServiceBusConnections.DefaultConnectionName;
return _adminClients.GetOrAdd(

@ -16,5 +16,5 @@ public interface IAzureServiceBusMessageConsumerFactory
IAzureServiceBusMessageConsumer CreateMessageConsumer(
string topicName,
string subscriptionName,
string connectionName);
string? connectionName);
}

@ -6,7 +6,7 @@ namespace Volo.Abp.AzureServiceBus;
public interface IConnectionPool : IAsyncDisposable
{
ServiceBusClient GetClient(string connectionName);
ServiceBusClient GetClient(string? connectionName);
ServiceBusAdministrationClient GetAdministrationClient(string connectionName);
ServiceBusAdministrationClient GetAdministrationClient(string? connectionName);
}

@ -6,5 +6,5 @@ namespace Volo.Abp.AzureServiceBus;
public interface IPublisherPool : IAsyncDisposable
{
Task<ServiceBusSender> GetAsync(string topicName, string connectionName);
Task<ServiceBusSender> GetAsync(string topicName, string? connectionName);
}

@ -24,7 +24,7 @@ public class PublisherPool : IPublisherPool, ISingletonDependency
Logger = new NullLogger<PublisherPool>();
}
public async Task<ServiceBusSender> GetAsync(string topicName, string connectionName)
public async Task<ServiceBusSender> GetAsync(string topicName, string? connectionName)
{
var admin = _connectionPool.GetAdministrationClient(connectionName);
await admin.SetupTopicAsync(topicName);

@ -98,6 +98,7 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
Logger.LogInformation($"'{projectName}' has been successfully created to '{projectArgs.OutputFolder}'");
ConfigureNpmPackagesForTheme(projectArgs);
await CreateOpenIddictPfxFilesAsync(projectArgs);
await RunGraphBuildForMicroserviceServiceTemplate(projectArgs);
await CreateInitialMigrationsAsync(projectArgs);

@ -16,6 +16,7 @@ using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.ProjectBuilding.Building;
using Volo.Abp.Cli.ProjectBuilding.Events;
using Volo.Abp.Cli.ProjectBuilding.Templates;
using Volo.Abp.Cli.ProjectBuilding.Templates.App;
using Volo.Abp.Cli.ProjectBuilding.Templates.Microservice;
using Volo.Abp.Cli.ProjectBuilding.Templates.Module;
@ -481,6 +482,36 @@ public abstract class ProjectCreationCommandBase
await InitialMigrationCreator.CreateAsync(Path.GetDirectoryName(efCoreProjectPath), isLayeredTemplate);
}
protected Task CreateOpenIddictPfxFilesAsync(ProjectBuildArgs projectArgs)
{
var module = projectArgs.ExtraProperties[nameof(RandomizeAuthServerPassPhraseStep)];
if (string.IsNullOrWhiteSpace(module))
{
return Task.CompletedTask;
}
var moduleDirectory = projectArgs.OutputFolder + module;
if (projectArgs.UiFramework != UiFramework.Angular)
{
moduleDirectory = moduleDirectory.Replace("/aspnet-core/", "/");
}
moduleDirectory = Path.GetDirectoryName(projectArgs.SolutionName.CompanyName == null
? moduleDirectory.Replace("MyCompanyName.MyProjectName", projectArgs.SolutionName.ProjectName)
: moduleDirectory.Replace("MyCompanyName", projectArgs.SolutionName.CompanyName).Replace("MyProjectName", projectArgs.SolutionName.ProjectName));
if (Directory.Exists(moduleDirectory))
{
Logger.LogInformation($"Creating openiddict.pfx file on {moduleDirectory}");
CmdHelper.RunCmd($"dotnet dev-certs https -ep openiddict.pfx -p {RandomizeAuthServerPassPhraseStep.RandomOpenIddictPassword}", moduleDirectory);
}
else
{
Logger.LogWarning($"Couldn't find the module directory to create openiddict.pfx file: {moduleDirectory}");
}
return Task.CompletedTask;
}
protected async Task ConfigurePwaSupportForAngular(ProjectBuildArgs projectArgs)
{
var isAngular = projectArgs.UiFramework == UiFramework.Angular;

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Volo.Abp.Cli.ProjectBuilding.Building;
@ -6,16 +7,30 @@ namespace Volo.Abp.Cli.ProjectBuilding.Templates;
public class RandomizeAuthServerPassPhraseStep : ProjectBuildPipelineStep
{
protected const string DefaultPassPhrase = "00000000-0000-0000-0000-000000000000";
private const string DefaultPassword = "00000000-0000-0000-0000-000000000000";
private const string KestrelCertificatesDefaultPassword = "Kestrel__Certificates__Default__Password=00000000-0000-0000-0000-000000000000";
private const string LocalhostPfx = "localhost.pfx -p 00000000-0000-0000-0000-000000000000";
private const string DotnetDevCerts = "openiddict.pfx -p 00000000-0000-0000-0000-000000000000";
private const string ProductionEncryptionAndSigningCertificate = "AddProductionEncryptionAndSigningCertificate(\"openiddict.pfx\", \"00000000-0000-0000-0000-000000000000\");";
private readonly static string RandomPassword = Guid.NewGuid().ToString("D");
public readonly static string RandomOpenIddictPassword = Guid.NewGuid().ToString("D");
public override void Execute(ProjectBuildContext context)
{
var files = context.Files
.Where(x => !x.IsDirectory)
.Where(x => x.Content.IndexOf(DefaultPassPhrase, StringComparison.InvariantCultureIgnoreCase) >= 0)
.Where(x => x.Name.EndsWith(".cs") ||
x.Name.EndsWith(".json") ||
x.Name.EndsWith(".yml") ||
x.Name.EndsWith(".yaml") ||
x.Name.EndsWith(".md") ||
x.Name.EndsWith(".ps1") ||
x.Name.EndsWith(".sh") ||
x.Name.Contains("Dockerfile"))
.Where(x => x.Content.IndexOf(DefaultPassword, StringComparison.InvariantCultureIgnoreCase) >= 0)
.ToList();
var randomPassPhrase = Guid.NewGuid().ToString("D");
string module = null;
foreach (var file in files)
{
file.NormalizeLineEndings();
@ -23,13 +38,43 @@ public class RandomizeAuthServerPassPhraseStep : ProjectBuildPipelineStep
var lines = file.GetLines();
for (var i = 0; i < lines.Length; i++)
{
if (lines[i].Contains(DefaultPassPhrase))
if (lines[i].Contains(KestrelCertificatesDefaultPassword))
{
lines[i] = lines[i].Replace(DefaultPassPhrase, randomPassPhrase);
lines[i] = lines[i].Replace(KestrelCertificatesDefaultPassword,
KestrelCertificatesDefaultPassword.Replace(DefaultPassword,
RandomPassword));
}
if (lines[i].Contains(LocalhostPfx))
{
lines[i] = lines[i].Replace(LocalhostPfx,
LocalhostPfx.Replace(DefaultPassword,
RandomPassword));
}
if (lines[i].Contains(DotnetDevCerts))
{
lines[i] = lines[i].Replace(DotnetDevCerts,
DotnetDevCerts.Replace(DefaultPassword,
RandomOpenIddictPassword));
}
if (lines[i].Contains(ProductionEncryptionAndSigningCertificate))
{
lines[i] = lines[i].Replace(ProductionEncryptionAndSigningCertificate,
ProductionEncryptionAndSigningCertificate.Replace(DefaultPassword,
RandomOpenIddictPassword));
module = file.Name;
}
}
file.SetLines(lines);
}
if (!module.IsNullOrWhiteSpace())
{
context.BuildArgs.ExtraProperties[nameof(RandomizeAuthServerPassPhraseStep)] = module;
}
}
}

@ -22,6 +22,7 @@ using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Json;
using System.Text.RegularExpressions;
namespace Volo.Abp.Cli.ProjectModification;
@ -114,12 +115,11 @@ public class SolutionModuleAdder : ITransientDependency
var projectFiles = ProjectFinder.GetProjectFiles(solutionFile);
await AddNugetAndNpmReferences(module, projectFiles, !(newTemplate || newProTemplate));
var modulesFolderInSolution = Path.Combine(Path.GetDirectoryName(solutionFile), "modules");
if (withSourceCode || newTemplate || newProTemplate)
{
await PublishEventAsync(5, $"Downloading source code of {moduleName}");
await DownloadSourceCodesToSolutionFolder(module, modulesFolderInSolution, version, newTemplate, newProTemplate);
@ -147,6 +147,8 @@ public class SolutionModuleAdder : ITransientDependency
else
{
await AddAngularPackages(solutionFile, module);
await TryConfigureModuleConfigurationsForAngular(solutionFile, module);
}
await RunBundleForBlazorAsync(projectFiles, module);
@ -167,10 +169,83 @@ public class SolutionModuleAdder : ITransientDependency
return module;
}
private async Task TryConfigureModuleConfigurationsForAngular(string solutionFilePath, ModuleWithMastersInfo module)
{
var angularPath = Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(solutionFilePath)), "angular");
if (!Directory.Exists(angularPath))
{
return;
}
var angularPackages = module.NpmPackages?
.Where(p => p.ApplicationType.HasFlag(NpmApplicationType.Angular))
.ToList();
if (!angularPackages.Any())
{
return;
}
await PublishEventAsync(6, "Configuring angular projects...");
var moduleName = module.Name.Split('.').Last();
ConfigureAngularPackagesForAppModuleFile(angularPath, angularPackages, moduleName);
ConfigureAngularPackagesForAppRoutingModuleFile(angularPath, angularPackages, moduleName);
}
private void ConfigureAngularPackagesForAppModuleFile(string angularPath, List<NpmPackageInfo> angularPackages, string moduleName)
{
var appModulePath = Path.Combine(angularPath, "src", "app", "app.module.ts");
if (!File.Exists(appModulePath))
{
return;
}
var appModuleFileContent = File.ReadAllText(appModulePath);
foreach (var angularPackage in angularPackages)
{
var moduleNameAsConfigPath = angularPackage.Name.EnsureStartsWith('@').EnsureEndsWith('/') + "config";
appModuleFileContent = "import { " + moduleName + "ConfigModule } from '" + moduleNameAsConfigPath + "';" + Environment.NewLine + appModuleFileContent;
appModuleFileContent = Regex.Replace(appModuleFileContent, "imports\\s*:\\s*\\[",
"imports: [" + Environment.NewLine +
" " + moduleName + "ConfigModule.forRoot(),");
}
File.WriteAllText(appModulePath, appModuleFileContent);
}
private void ConfigureAngularPackagesForAppRoutingModuleFile(string angularPath, List<NpmPackageInfo> angularPackages, string moduleName)
{
var appRoutingModulePath = Path.Combine(angularPath, "src", "app", "app-routing.module.ts");
if (!File.Exists(appRoutingModulePath))
{
return;
}
var appRoutingModuleFileContent = File.ReadAllText(appRoutingModulePath);
foreach (var angularPackage in angularPackages)
{
appRoutingModuleFileContent = Regex.Replace(appRoutingModuleFileContent, "Routes\\s*=\\s*\\[",
"Routes = [" + Environment.NewLine +
" " + "{" + Environment.NewLine +
" " + "path: '" + moduleName.ToLower() + "'," + Environment.NewLine +
" " + "loadChildren: () => " + $"import('{angularPackage.Name.EnsureStartsWith('@')}').then(m => m.{moduleName}Module.forLazy())," + Environment.NewLine +
" " + "},");
}
File.WriteAllText(appRoutingModulePath, appRoutingModuleFileContent);
}
private async Task SetLeptonXAbpVersionsAsync(string solutionFile, string combine)
{
var abpVersion = SolutionPackageVersionFinder.FindByCsprojVersion(solutionFile);
var projects = Directory.GetFiles(Path.GetDirectoryName(solutionFile)!, "*.csproj", SearchOption.AllDirectories);
foreach (var project in projects)
@ -183,7 +258,8 @@ public class SolutionModuleAdder : ITransientDependency
private async Task PublishEventAsync(int currentStep, string message)
{
await LocalEventBus.PublishAsync(new ModuleInstallingProgressEvent {
await LocalEventBus.PublishAsync(new ModuleInstallingProgressEvent
{
CurrentStep = currentStep,
Message = message
}, false);
@ -562,7 +638,7 @@ public class SolutionModuleAdder : ITransientDependency
if (webPackagesWillBeAddedToBlazorServerProject)
{
if ( nugetTarget == NuGetPackageTarget.Web)
if (nugetTarget == NuGetPackageTarget.Web)
{
nugetTarget = NuGetPackageTarget.BlazorServer;
}
@ -636,7 +712,7 @@ public class SolutionModuleAdder : ITransientDependency
}
var dbMigrationsProject = projectFiles.FirstOrDefault(p => p.EndsWith(".DbMigrations.csproj"))
?? projectFiles.FirstOrDefault(p => p.EndsWith(".EntityFrameworkCore.csproj")) ;
?? projectFiles.FirstOrDefault(p => p.EndsWith(".EntityFrameworkCore.csproj"));
if (dbMigrationsProject == null)
{

@ -47,7 +47,7 @@ public static class AbpStringExtensions
/// Indicates whether this string is null or an System.String.Empty string.
/// </summary>
[ContractAnnotation("str:null => true")]
public static bool IsNullOrEmpty(this string? str)
public static bool IsNullOrEmpty([System.Diagnostics.CodeAnalysis.NotNullWhen(false)]this string? str)
{
return string.IsNullOrEmpty(str);
}
@ -56,7 +56,7 @@ public static class AbpStringExtensions
/// indicates whether this string is null, empty, or consists only of white-space characters.
/// </summary>
[ContractAnnotation("str:null => true")]
public static bool IsNullOrWhiteSpace(this string? str)
public static bool IsNullOrWhiteSpace([System.Diagnostics.CodeAnalysis.NotNullWhen(false)]this string? str)
{
return string.IsNullOrWhiteSpace(str);
}

@ -37,4 +37,10 @@
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETStandard' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.1')) ">
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETStandard' And $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.0')) ">
<PackageReference Include="Nullable" Version="1.3.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

@ -132,7 +132,7 @@ public abstract class AbstractKeyReadOnlyAppService<TEntity, TGetOutputDto, TGet
return query.OrderByDescending(e => ((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!");
}
/// <summary>

@ -1,4 +1,3 @@
using JetBrains.Annotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Localization;

@ -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<TEntity>
var readOnlyRepositoryInterface = typeof(IReadOnlyRepository<>).MakeGenericType(entityType);
if (readOnlyRepositoryInterface.IsAssignableFrom(repositoryImplementationType))
{
RegisterService(services, readOnlyRepositoryInterface, repositoryImplementationType, replaceExisting);
RegisterService(services, readOnlyRepositoryInterface, repositoryImplementationType, replaceExisting, true);
}
//IBasicRepository<TEntity>
@ -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<TEntity, TKey>
var readOnlyRepositoryInterfaceWithPk = typeof(IReadOnlyRepository<,>).MakeGenericType(entityType, primaryKeyType);
if (readOnlyRepositoryInterfaceWithPk.IsAssignableFrom(repositoryImplementationType))
{
RegisterService(services, readOnlyRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting);
RegisterService(services, readOnlyRepositoryInterfaceWithPk, repositoryImplementationType, replaceExisting, true);
}
//IBasicRepository<TEntity, TKey>
@ -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<IRepository>(), x => x.IsChangeTrackingEnabled, _ => false);
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);
}
}
}

@ -2,6 +2,7 @@
using Volo.Abp.Auditing;
using Volo.Abp.Caching;
using Volo.Abp.Data;
using Volo.Abp.Domain.ChangeTracking;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus;
using Volo.Abp.ExceptionHandling;
@ -30,5 +31,6 @@ public class AbpDddDomainModule : AbpModule
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddConventionalRegistrar(new AbpRepositoryConventionalRegistrar());
context.Services.OnRegistered(ChangeTrackingInterceptorRegistrar.RegisterIfNeeded);
}
}

@ -0,0 +1,53 @@
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using Volo.Abp.Domain.Repositories;
namespace Volo.Abp.Domain.ChangeTracking;
public static class ChangeTrackingHelper
{
public static bool IsEntityChangeTrackingType(TypeInfo implementationType)
{
return HasEntityChangeTrackingAttribute(implementationType) || AnyMethodHasEntityChangeTrackingAttribute(implementationType);
}
public static bool IsEntityChangeTrackingMethod([NotNull] MethodInfo methodInfo, out EntityChangeTrackingAttribute? entityChangeTrackingAttribute)
{
Check.NotNull(methodInfo, nameof(methodInfo));
//Method declaration
var attrs = methodInfo.GetCustomAttributes(true).OfType<EntityChangeTrackingAttribute>().ToArray();
if (attrs.Any())
{
entityChangeTrackingAttribute = attrs.First();
return true;
}
if (methodInfo.DeclaringType != null)
{
//Class declaration
attrs = methodInfo.DeclaringType.GetTypeInfo().GetCustomAttributes(true).OfType<EntityChangeTrackingAttribute>().ToArray();
if (attrs.Any())
{
entityChangeTrackingAttribute = attrs.First();
return true;
}
}
entityChangeTrackingAttribute = null;
return false;
}
private static bool AnyMethodHasEntityChangeTrackingAttribute(TypeInfo implementationType)
{
return implementationType
.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Any(HasEntityChangeTrackingAttribute);
}
private static bool HasEntityChangeTrackingAttribute(MemberInfo memberInfo)
{
return memberInfo.IsDefined(typeof(EntityChangeTrackingAttribute), true);
}
}

@ -0,0 +1,30 @@
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Domain.ChangeTracking;
public class ChangeTrackingInterceptor : AbpInterceptor, ITransientDependency
{
private readonly IEntityChangeTrackingProvider _entityChangeTrackingProvider;
public ChangeTrackingInterceptor(IEntityChangeTrackingProvider entityChangeTrackingProvider)
{
_entityChangeTrackingProvider = entityChangeTrackingProvider;
}
public async override Task InterceptAsync(IAbpMethodInvocation invocation)
{
if (!ChangeTrackingHelper.IsEntityChangeTrackingMethod(invocation.Method, out var changeTrackingAttribute))
{
await invocation.ProceedAsync();
return;
}
using (_entityChangeTrackingProvider.Change(changeTrackingAttribute?.IsEnabled))
{
await invocation.ProceedAsync();
}
}
}

@ -0,0 +1,22 @@
using System;
using System.Reflection;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
namespace Volo.Abp.Domain.ChangeTracking;
public class ChangeTrackingInterceptorRegistrar
{
public static void RegisterIfNeeded(IOnServiceRegistredContext context)
{
if (ShouldIntercept(context.ImplementationType))
{
context.Interceptors.TryAdd<ChangeTrackingInterceptor>();
}
}
private static bool ShouldIntercept(Type type)
{
return !DynamicProxyIgnoreTypes.Contains(type) && ChangeTrackingHelper.IsEntityChangeTrackingType(type.GetTypeInfo());
}
}

@ -0,0 +1,15 @@
using System;
namespace Volo.Abp.Domain.ChangeTracking;
/// <summary>
/// Ensures that the change tracking in enabled for the given method or class.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class DisableEntityChangeTrackingAttribute : EntityChangeTrackingAttribute
{
public DisableEntityChangeTrackingAttribute()
: base(false)
{
}
}

@ -0,0 +1,15 @@
using System;
namespace Volo.Abp.Domain.ChangeTracking;
/// <summary>
/// Ensures that the change tracking in enabled for the given method or class.
/// </summary>
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class EnableEntityChangeTrackingAttribute : EntityChangeTrackingAttribute
{
public EnableEntityChangeTrackingAttribute()
: base(true)
{
}
}

@ -0,0 +1,14 @@
using System;
namespace Volo.Abp.Domain.ChangeTracking;
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public abstract class EntityChangeTrackingAttribute : Attribute
{
public virtual bool IsEnabled { get; set; }
public EntityChangeTrackingAttribute(bool isEnabled)
{
IsEnabled = isEnabled;
}
}

@ -4,6 +4,8 @@ using System.Collections.Generic;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities;
@ -34,6 +36,14 @@ public abstract class BasicRepositoryBase<TEntity> :
public ICancellationTokenProvider CancellationTokenProvider => LazyServiceProvider.LazyGetService<ICancellationTokenProvider>(NullCancellationTokenProvider.Instance);
public ILoggerFactory? LoggerFactory => LazyServiceProvider.LazyGetService<ILoggerFactory>();
public ILogger Logger => LazyServiceProvider.LazyGetService<ILogger>(provider => LoggerFactory?.CreateLogger(GetType().FullName!) ?? NullLogger.Instance);
public IEntityChangeTrackingProvider EntityChangeTrackingProvider => LazyServiceProvider.LazyGetRequiredService<IEntityChangeTrackingProvider>();
public bool? IsChangeTrackingEnabled { get; protected set; }
protected BasicRepositoryBase()
{
@ -106,6 +116,24 @@ public abstract class BasicRepositoryBase<TEntity> :
{
return CancellationTokenProvider.FallbackToProvider(preferredValue);
}
protected virtual bool ShouldTrackingEntityChange()
{
// If IsChangeTrackingEnabled is set, it has the highest priority. This generally means the repository is read-only.
if (IsChangeTrackingEnabled.HasValue)
{
return IsChangeTrackingEnabled.Value;
}
// If Interface/Class/Method has Enable/DisableEntityChangeTrackingAttribute, it has the second highest priority.
if (EntityChangeTrackingProvider.Enabled.HasValue)
{
return EntityChangeTrackingProvider.Enabled.Value;
}
// Default behavior is tracking entity change.
return true;
}
}
public abstract class BasicRepositoryBase<TEntity, TKey> : BasicRepositoryBase<TEntity>, IBasicRepository<TEntity, TKey>

@ -0,0 +1,19 @@
using System;
using System.Threading;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Domain.Repositories;
public class EntityChangeTrackingProvider : IEntityChangeTrackingProvider, ISingletonDependency
{
public bool? Enabled => _current.Value;
private readonly AsyncLocal<bool?> _current = new AsyncLocal<bool?>();
public IDisposable Change(bool? enabled)
{
var previousValue = Enabled;
_current.Value = enabled;
return new DisposeAction(() => _current.Value = previousValue);
}
}

@ -0,0 +1,10 @@
using System;
namespace Volo.Abp.Domain.Repositories;
public interface IEntityChangeTrackingProvider
{
bool? Enabled { get; }
IDisposable Change(bool? enabled);
}

@ -12,7 +12,7 @@ namespace Volo.Abp.Domain.Repositories;
/// </summary>
public interface IRepository
{
bool? IsChangeTrackingEnabled { get; }
}
public interface IRepository<TEntity> : IReadOnlyRepository<TEntity>, IBasicRepository<TEntity>

@ -7,8 +7,8 @@ using Volo.Abp.Domain.Entities;
namespace Volo.Abp.Domain.Repositories;
public interface ISupportsExplicitLoading<TEntity, TKey>
where TEntity : class, IEntity<TKey>
public interface ISupportsExplicitLoading<TEntity>
where TEntity : class, IEntity
{
Task EnsureCollectionLoadedAsync<TProperty>(
TEntity entity,
@ -18,7 +18,7 @@ public interface ISupportsExplicitLoading<TEntity, TKey>
Task EnsurePropertyLoadedAsync<TProperty>(
TEntity entity,
Expression<Func<TEntity, TProperty>> propertyExpression,
Expression<Func<TEntity, TProperty?>> propertyExpression,
CancellationToken cancellationToken)
where TProperty : class;
}

@ -15,16 +15,16 @@ namespace Volo.Abp.Domain.Repositories;
public static class RepositoryExtensions
{
public async static Task EnsureCollectionLoadedAsync<TEntity, TKey, TProperty>(
this IBasicRepository<TEntity, TKey> repository,
public async static Task EnsureCollectionLoadedAsync<TEntity, TProperty>(
this IBasicRepository<TEntity> repository,
TEntity entity,
Expression<Func<TEntity, IEnumerable<TProperty>>> propertyExpression,
CancellationToken cancellationToken = default
)
where TEntity : class, IEntity<TKey>
where TEntity : class, IEntity
where TProperty : class
{
var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading<TEntity, TKey>;
var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading<TEntity>;
if (repo != null)
{
await repo.EnsureCollectionLoadedAsync(entity, propertyExpression, cancellationToken);
@ -34,13 +34,13 @@ public static class RepositoryExtensions
public async static Task EnsurePropertyLoadedAsync<TEntity, TKey, TProperty>(
this IBasicRepository<TEntity, TKey> repository,
TEntity entity,
Expression<Func<TEntity, TProperty>> propertyExpression,
Expression<Func<TEntity, TProperty?>> propertyExpression,
CancellationToken cancellationToken = default
)
where TEntity : class, IEntity<TKey>
where TProperty : class
{
var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading<TEntity, TKey>;
var repo = ProxyHelper.UnProxy(repository) as ISupportsExplicitLoading<TEntity>;
if (repo != null)
{
await repo.EnsurePropertyLoadedAsync(entity, propertyExpression, cancellationToken);
@ -60,12 +60,12 @@ public static class RepositoryExtensions
}
}
public async static Task EnsureExistsAsync<TEntity, TKey>(
this IRepository<TEntity, TKey> repository,
public async static Task EnsureExistsAsync<TEntity>(
this IRepository<TEntity> repository,
Expression<Func<TEntity, bool>> expression,
CancellationToken cancellationToken = default
)
where TEntity : class, IEntity<TKey>
where TEntity : class, IEntity
{
if (!await repository.AnyAsync(expression, cancellationToken))
{
@ -145,6 +145,40 @@ public static class RepositoryExtensions
}
}
/// <summary>
/// Disables change tracking mechanism for the given repository.
/// </summary>
/// <param name="repository">A repository object</param>
/// <returns>
/// A disposable object. Dispose it to restore change tracking mechanism back to its previous state.
/// </returns>
public static IDisposable DisableTracking(this IRepository repository)
{
return Tracking(repository, false);
}
/// <summary>
/// Enables change tracking mechanism for the given repository.
/// </summary>
/// <param name="repository">A repository object</param>
/// <returns>
/// A disposable object. Dispose it to restore change tracking mechanism back to its previous state.
/// </returns>
public static IDisposable EnableTracking(this IRepository repository)
{
return Tracking(repository, true);
}
private static IDisposable Tracking(this IRepository repository, bool enabled)
{
var previous = repository.IsChangeTrackingEnabled;
ObjectHelper.TrySetProperty(ProxyHelper.UnProxy(repository).As<IRepository>(), x => x.IsChangeTrackingEnabled, _ => enabled);
return new DisposeAction<IRepository>(_ =>
{
ObjectHelper.TrySetProperty(ProxyHelper.UnProxy(repository).As<IRepository>(), x => x.IsChangeTrackingEnabled, _ => previous);
}, repository);
}
private static IUnitOfWorkManager GetUnitOfWorkManager<TEntity>(
this IBasicRepository<TEntity> repository,
[CallerMemberName] string callingMethodName = nameof(GetUnitOfWorkManager)

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Data;
namespace Volo.Abp.Emailing;
[Serializable]
public class AdditionalEmailSendingArgs
{
public List<string>? CC { get; set; }
public List<EmailAttachment>? Attachments { get; set; }
public ExtraPropertyDictionary? ExtraProperties { get; set; }
}

@ -15,15 +15,15 @@ public class BackgroundEmailSendingJob : AsyncBackgroundJob<BackgroundEmailSendi
EmailSender = emailSender;
}
public override async Task ExecuteAsync(BackgroundEmailSendingJobArgs args)
public async override Task ExecuteAsync(BackgroundEmailSendingJobArgs args)
{
if (args.From.IsNullOrWhiteSpace())
{
await EmailSender.SendAsync(args.To, args.Subject, args.Body, args.IsBodyHtml);
await EmailSender.SendAsync(args.To, args.Subject, args.Body, args.IsBodyHtml, args.AdditionalEmailSendingArgs);
}
else
{
await EmailSender.SendAsync(args.From!, args.To, args.Subject, args.Body, args.IsBodyHtml);
await EmailSender.SendAsync(args.From!, args.To, args.Subject, args.Body, args.IsBodyHtml, args.AdditionalEmailSendingArgs);
}
}
}

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Data;
namespace Volo.Abp.Emailing;
@ -18,5 +20,5 @@ public class BackgroundEmailSendingJobArgs
/// </summary>
public bool IsBodyHtml { get; set; } = true;
//TODO: Add other properties and attachments
public AdditionalEmailSendingArgs? AdditionalEmailSendingArgs { get; set; }
}

@ -0,0 +1,11 @@
using System;
namespace Volo.Abp.Emailing;
[Serializable]
public class EmailAttachment
{
public string? Name { get; set; }
public byte[]? File { get; set; }
}

@ -1,4 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Text;
using System.Threading.Tasks;
@ -24,20 +26,44 @@ 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, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
{
await SendAsync(new MailMessage
{
To = { to },
Subject = subject,
Body = body,
IsBodyHtml = isBodyHtml
});
await SendAsync(BuildMailMessage(null, to, subject, body, isBodyHtml, additionalEmailSendingArgs));
}
public virtual async Task SendAsync(string from, string to, string? subject, string? body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
{
await SendAsync(BuildMailMessage(from, to, subject, body, isBodyHtml, additionalEmailSendingArgs));
}
public virtual async Task SendAsync(string from, string to, string? subject, string? body, bool isBodyHtml = true)
protected virtual MailMessage BuildMailMessage(string? from, string to, string? subject, string? body, bool isBodyHtml = true, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
{
await SendAsync(new MailMessage(from, to, subject, body) { IsBodyHtml = isBodyHtml });
var message = from == null
? new MailMessage { To = { to }, Subject = subject, Body = body, IsBodyHtml = isBodyHtml }
: new MailMessage(from, to, subject, body) { IsBodyHtml = isBodyHtml };
if (additionalEmailSendingArgs != null)
{
if (additionalEmailSendingArgs.Attachments != null)
{
foreach (var attachment in additionalEmailSendingArgs.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));
}
}
if (additionalEmailSendingArgs.CC != null)
{
foreach (var cc in additionalEmailSendingArgs.CC)
{
message.CC.Add(cc);
}
}
}
return message;
}
public virtual async Task SendAsync(MailMessage mail, bool normalize = true)
@ -50,11 +76,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, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
{
if (!BackgroundJobManager.IsAvailable())
{
await SendAsync(to, subject, body, isBodyHtml);
await SendAsync(to, subject, body, isBodyHtml, additionalEmailSendingArgs);
return;
}
@ -64,16 +90,17 @@ public abstract class EmailSenderBase : IEmailSender
To = to,
Subject = subject,
Body = body,
IsBodyHtml = isBodyHtml
IsBodyHtml = isBodyHtml,
AdditionalEmailSendingArgs = additionalEmailSendingArgs
}
);
}
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, AdditionalEmailSendingArgs? additionalEmailSendingArgs = null)
{
if (!BackgroundJobManager.IsAvailable())
{
await SendAsync(from, to, subject, body, isBodyHtml);
await SendAsync(from, to, subject, body, isBodyHtml, additionalEmailSendingArgs);
return;
}
@ -84,7 +111,8 @@ public abstract class EmailSenderBase : IEmailSender
To = to,
Subject = subject,
Body = body,
IsBodyHtml = isBodyHtml
IsBodyHtml = isBodyHtml,
AdditionalEmailSendingArgs = additionalEmailSendingArgs
}
);
}

@ -15,7 +15,8 @@ public interface IEmailSender
string to,
string? subject,
string? body,
bool isBodyHtml = true
bool isBodyHtml = true,
AdditionalEmailSendingArgs? additionalEmailSendingArgs = null
);
/// <summary>
@ -26,7 +27,8 @@ public interface IEmailSender
string to,
string? subject,
string? body,
bool isBodyHtml = true
bool isBodyHtml = true,
AdditionalEmailSendingArgs? additionalEmailSendingArgs = null
);
/// <summary>
@ -49,7 +51,8 @@ public interface IEmailSender
string to,
string subject,
string body,
bool isBodyHtml = true
bool isBodyHtml = true,
AdditionalEmailSendingArgs? additionalEmailSendingArgs = null
);
/// <summary>
@ -60,8 +63,7 @@ public interface IEmailSender
string to,
string subject,
string body,
bool isBodyHtml = true
bool isBodyHtml = true,
AdditionalEmailSendingArgs? additionalEmailSendingArgs = null
);
//TODO: Add other Queue methods too. Problem: MailMessage is not serializable so can not be used in background jobs.
}

@ -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())
{

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save