Browse Source

Merge branch 'dev' into languages/add-en-GB-English-UK-json-files

pull/6935/head
maliming 5 years ago
parent
commit
b2a78a5eda
  1. 4
      docs/en/Background-Workers.md
  2. 2
      docs/en/CLI.md
  3. 2
      docs/en/Contribution/Index.md
  4. 50
      docs/en/Customizing-Application-Modules-Guide.md
  5. 4
      docs/en/Emailing.md
  6. 10
      docs/en/Entity-Framework-Core-Migrations.md
  7. 2
      docs/en/Modules/Docs.md
  8. 6
      docs/en/Samples/Microservice-Demo.md
  9. 327
      docs/en/UI/Angular/Data-Table-Column-Extensions.md
  10. 325
      docs/en/UI/Angular/Dynamic-Form-Extensions.md
  11. 442
      docs/en/UI/Angular/Entity-Action-Extensions.md
  12. 66
      docs/en/UI/Angular/Form-Validation.md
  13. 420
      docs/en/UI/Angular/Page-Toolbar-Extensions.md
  14. BIN
      docs/en/UI/Angular/images/user-action-extension-click-me-ng.png
  15. BIN
      docs/en/UI/Angular/images/user-page-toolbar-extension-click-me-ng.png
  16. BIN
      docs/en/UI/Angular/images/user-page-toolbar-extension-custom-click-me-ng.png
  17. BIN
      docs/en/UI/Angular/images/user-prop-extension-date-of-birth-field-ng.png
  18. BIN
      docs/en/UI/Angular/images/user-prop-extension-name-column-ng.png
  19. 163
      docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md
  20. 3
      docs/en/UI/Blazor/Page-Progress.md
  21. 18
      docs/en/docs-nav.json
  22. BIN
      docs/en/images/page-toolbar-button.png
  23. BIN
      docs/en/images/page-toolbar-custom-component.png
  24. 2
      docs/zh-Hans/CLI.md
  25. 10
      docs/zh-Hans/Entity-Framework-Core-Migrations.md
  26. 2
      docs/zh-Hans/Modules/Docs.md
  27. 6
      docs/zh-Hans/Samples/Microservice-Demo.md
  28. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs
  29. 40
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js
  30. 2
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs
  31. 2
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs
  32. 4
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpUnitOfWorkMiddleware.cs
  33. 52
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AspNetCoreUnitOfWorkTransactionBehaviourProvider.cs
  34. 17
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AspNetCoreUnitOfWorkTransactionBehaviourProviderOptions.cs
  35. 6
      framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AuthorizationOptionsExtensions.cs
  36. 16
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs
  37. 10
      framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs
  38. 34
      framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs
  39. 26
      framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperModule.cs
  40. 140
      framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs
  41. 122
      framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceProvider.cs
  42. 77
      framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceProviderFactory.cs
  43. 67
      framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceScope.cs
  44. 65
      framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceScopeFactory.cs
  45. 4
      framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj
  46. 4
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IAsyncBackgroundJob.cs
  47. 4
      framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IBackgroundJob.cs
  48. 10
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs
  49. 2
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerAdapter.cs
  50. 9
      framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs
  51. 4
      framework/src/Volo.Abp.BlazoriseUI/AbpCrudPageBase.cs
  52. 3
      framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactoryExtensions.cs
  53. 25
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
  54. 11
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs
  55. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs
  56. 9
      framework/src/Volo.Abp.Cli/Properties/launchSettings.json
  57. 2
      framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs
  58. 2
      framework/src/Volo.Abp.Core/System/Collections/Generic/AbpCollectionExtensions.cs
  59. 4
      framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeFinder.cs
  60. 6
      framework/src/Volo.Abp.Core/Volo/Abp/Text/Formatting/FormattedStringValueExtracter.cs
  61. 12
      framework/src/Volo.Abp.Dapper/Volo/Abp/Domain/Repositories/Dapper/DapperRepository.cs
  62. 12
      framework/src/Volo.Abp.Dapper/Volo/Abp/Domain/Repositories/Dapper/IDapperRepository.cs
  63. 6
      framework/src/Volo.Abp.Data/Volo/Abp/Data/DataSeedContext.cs
  64. 16
      framework/src/Volo.Abp.Data/Volo/Abp/Data/DefaultConnectionStringResolver.cs
  65. 8
      framework/src/Volo.Abp.Data/Volo/Abp/Data/IConnectionStringResolver.cs
  66. 14
      framework/src/Volo.Abp.Data/Volo/Abp/Data/IConnectionStringResolverExtensions.cs
  67. 10
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs
  68. 36
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs
  69. 4
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs
  70. 4
      framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ReadOnlyAppService.cs
  71. 6
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/DependencyInjection/IAbpCommonDbContextRegistrationOptionsBuilder.cs
  72. 9
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IReadOnlyRepository.cs
  73. 5
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryAsyncExtensions.cs
  74. 22
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs
  75. 15
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs
  76. 178
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs
  77. 10
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/IEfCoreRepository.cs
  78. 4
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs
  79. 2
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EfCoreAsyncQueryableProvider.cs
  80. 10
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IDbContextProvider.cs
  81. 2
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionManagerExtensions.cs
  82. 132
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs
  83. 4
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IEventBus.cs
  84. 4
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IEventDataMayHaveTenantId.cs
  85. 4
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs
  86. 8
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureGroupDefinition.cs
  87. 9
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/IMemoryDbRepository.cs
  88. 77
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs
  89. 17
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDbCoreRepositoryExtensions.cs
  90. 12
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/MemoryDb/IMemoryDatabaseProvider.cs
  91. 36
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/Uow/MemoryDb/UnitOfWorkMemoryDatabaseProvider.cs
  92. 14
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/IMongoDbRepository.cs
  93. 253
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
  94. 24
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDbCoreRepositoryExtensions.cs
  95. 13
      framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/IMongoDbContextProvider.cs
  96. 2
      framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/MongoDbAsyncQueryableProvider.cs
  97. 133
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs
  98. 4
      framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantStore.cs
  99. 53
      framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenantConnectionStringResolver.cs
  100. 3
      framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs

4
docs/en/Background-Workers.md

@ -80,7 +80,7 @@ public class PassiveUserCheckerWorker : AsyncPeriodicBackgroundWorkerBase
## Register Background Worker
After creating a background worker class, you should to add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class:
After creating a background worker class, you should add it to the `IBackgroundWorkerManager`. The most common place is the `OnApplicationInitialization` method of your module class:
````csharp
[DependsOn(typeof(AbpBackgroundWorkersModule))]
@ -137,4 +137,4 @@ ABP Framework's background worker system is good to implement periodic tasks. Ho
## See Also
* [Quartz Integration for the background workers](Background-Workers-Quartz.md)
* [Background Jobs](Background-Jobs.md)
* [Background Jobs](Background-Jobs.md)

2
docs/en/CLI.md

@ -105,7 +105,7 @@ abp new Acme.BookStore
* `--preview`: Use latest preview version.
* `--template-source` or `-ts`: Specifies a custom template source to use to build the project. Local and network sources can be used(Like `D:\local-template` or `https://.../my-template-file.zip`).
* `--create-solution-folder` or `-csf`: Specifies if the project will be in a new folder in the output folder or directly the output folder.
* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution).
* `--connection-string` or `-cs`: Overwrites the default connection strings in all `appsettings.json` files. The default connection string is `Server=localhost;Database=MyProjectName;Trusted_Connection=True` for EF Core and it is configured to use the SQL Server. If you want to use the EF Core, but need to change the DBMS, you can change it as [described here](Entity-Framework-Core-Other-DBMS.md) (after creating the solution).
* `--database-management-system` or `-dbms`: Sets the database management system. Default is **SQL Server**. Supported DBMS's:
* `SqlServer`
* `MySQL`

2
docs/en/Contribution/Index.md

@ -10,7 +10,7 @@ If you want to write **articles** or **how to guides** related to the ABP Framew
You can always send pull requests to the GitHub repository.
- Clone the [ABP repository](https://github.com/abpframework/abp/) from GitHub.
- [Fork](https://docs.github.com/en/free-pro-team@latest/github/getting-started-with-github/fork-a-repo) the [ABP repository](https://github.com/abpframework/abp/) from GitHub.
- Build the repository using the `/build/build-all.ps1 -f` for one time.
- Make the necessary changes, including unit/integration tests.
- Send a pull request.

50
docs/en/Customizing-Application-Modules-Guide.md

@ -43,17 +43,59 @@ In any case, you can create a **separate solution** for the desired module and d
#### Publishing the Customized Module as Packages
One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company.
One alternative scenario could be re-packaging the module source code (as NuGet/NPM packages) and using as package references. You can use a local private NuGet/NPM server for your company, for example.
## Module Customization / Extending Approaches
This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways:
This section suggests some approaches if you decided to use pre-built application modules as NuGet/NPM package references. The following documents explain how to customize/extend existing modules in different ways.
### Module Entity Extension System
> Module entity extension system is the **main and high level extension system** that allows you to **define new properties** for existing entities of the depended modules. It automatically **adds properties to the entity, database, HTTP API and the user interface** in a single point.
See the [Module Entity Extensions document](Module-Entity-Extensions.md) to learn how to use it.
### Extending Entities
If you only need to get/set extra data on an existing entity, follow the [Extending Entities](Customizing-Application-Modules-Extending-Entities.md) document.
### Overriding Services/Components
In addition to the extensibility systems, you can partially or completely override any service or user interface page/component.
* [Extending Entities](Customizing-Application-Modules-Extending-Entities.md)
* [Overriding Services](Customizing-Application-Modules-Overriding-Services.md)
* [Overriding the User Interface](Customizing-Application-Modules-Overriding-User-Interface.md)
### See Also
### Additional UI Extensibility Points
There are some low level systems that you can control entity actions, table columns and page toolbar of a page defined by a module.
#### Entity Actions
Entity action extension system allows you to add a new action to the action menu for an entity on the user interface;
* [Entity Action Extensions for ASP.NET Core UI](UI/AspNetCore/Entity-Action-Extensions.md)
* [Entity Action Extensions for Angular](UI/Angular/Entity-Action-Extensions.md)
#### Data Table Column Extensions
Data table column extension system allows you to add a new column in the data table on the user interface;
* [Data Table Column Extensions for ASP.NET Core UI](UI/AspNetCore/Data-Table-Column-Extensions.md)
* [Data Table Column Extensions for Angular](UI/Angular/Data-Table-Column-Extensions.md)
#### Page Toolbar
Page toolbar system allows you to add components to the toolbar of a page;
* [Page Toolbar Extensions for ASP.NET Core UI](UI/AspNetCore/Page-Toolbar-Extensions.md)
* [Page Toolbar Extensions for Angular](UI/Angular/Page-Toolbar-Extensions.md)
#### Others
* [Dynamic Form Extensions for Angular](UI/Angular/Dynamic-Form-Extensions.md)
## See Also
Also, see the following documents:

4
docs/en/Emailing.md

@ -223,7 +223,7 @@ Pathes of the templates in the virtual file system are shown below:
* `/Volo/Abp/Emailing/Templates/Layout.tpl`
* `/Volo/Abp/Emailing/Templates/Message.tpl`
If you add files to the same localization in the virtual file system, your files will override them.
If you add files to the same location in the virtual file system, your files will override them.
Templates are inline localized, that means you can take the power of the [localization system](Localization.md) to make your templates multi-cultural.
@ -247,4 +247,4 @@ So, don't confuse if you don't receive emails on DEBUG mode. Emails will be sent
## See Also
* [MailKit integration for sending emails](MailKit.md)
* [MailKit integration for sending emails](MailKit.md)

10
docs/en/Entity-Framework-Core-Migrations.md

@ -586,7 +586,7 @@ First step is to change the connection string section inside all the `appsetting
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
````
@ -594,10 +594,10 @@ Change it as shown below:
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True"
}
````

2
docs/en/Modules/Docs.md

@ -47,7 +47,7 @@ The database connection string is located in `appsettings.json` of your `Acme.My
```json
{
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True"
}
}
```

6
docs/en/Samples/Microservice-Demo.md

@ -842,7 +842,7 @@ It has a dedicated MongoDB database (MsDemo_Blogging) to store blog and posts. I
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True",
"Blogging": "mongodb://localhost/MsDemo_Blogging"
}
````
@ -968,8 +968,8 @@ There are two connection strings in the `appsettings.json` file:
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
"ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True",
"ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True"
}
````

327
docs/en/UI/Angular/Data-Table-Column-Extensions.md

@ -0,0 +1,327 @@
# Data Table Column (or Entity Prop) Extensions for Angular UI
## Introduction
Entity prop extension system allows you to add a new column to the data table for an entity or change/remove an already existing one. A "Name" column was added to the user management page below:
![Entity Prop Extension Example: "Name" Column](images/user-prop-extension-name-column-ng.png)
You will have access to the current entity in your code and display its value, make the column sortable, perform visibility checks, and more. You can also render custom HTML in table cells.
## How to Set Up
In this example, we will add a "Name" column and display the value of the `name` field in the user management page of the [Identity Module](../../Modules/Identity.md).
### Step 1. Create Entity Prop Contributors
The following code prepares a constant named `identityEntityPropContributors`, ready to be imported and used in your root module:
```js
// entity-prop-contributors.ts
import { EntityProp, EntityPropList, ePropType } from '@abp/ng.theme.shared/extensions';
import { IdentityEntityPropContributors, IdentityUserDto } from '@volo/abp.ng.identity';
const nameProp = new EntityProp<IdentityUserDto>({
type: ePropType.String,
name: 'name',
displayName: 'AbpIdentity::Name',
sortable: true,
columnWidth: 250,
});
export function namePropContributor(propList: EntityPropList<IdentityUserDto>) {
propList.addAfter(
nameProp,
'userName',
(value, name) => value.name === name,
);
}
export const identityEntityPropContributors: IdentityEntityPropContributors = {
'Identity.UsersComponent': [namePropContributor],
};
```
The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addAfter` method, which adds a node with given value after the first node that has the previous value. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note 1:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `namePropContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
> **Important Note 2:** Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.
```js
export const identityEntityPropContributors: IdentityEntityPropContributors = {
'Identity.UsersComponent': [ namePropContributor ],
};
/* OR */
const identityContributors: IdentityEntityPropContributors = {};
identityContributors[eIdentityComponents.Users] = [ namePropContributor ];
export const identityEntityPropContributors = identityContributors;
```
### Step 2. Import and Use Entity Prop Contributors
Import `identityEntityPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
```js
import { identityEntityPropContributors } from './entity-prop-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
entityPropContributors: identityEntityPropContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `nameProp` entity prop will be added, and you will see the "Name" column next to the usernames on the grid in the users page (`UsersComponent`) of the `IdentityModule`.
## How to Render Custom HTML in Cells
You can use the `valueResolver` to render an HTML string in the table. Imagine we want to show a red times icon (❌) next to unconfirmed emails and phones, instead of showing a green check icon next to confirmed emails and phones. The contributors below would do that for you.
```js
// entity-prop-contributors.ts
import { EntityProp, EntityPropList, ePropType } from '@abp/ng.theme.shared/extensions';
import { IdentityUserDto } from '@volo/abp.ng.identity';
import { IdentityEntityPropContributors } from '@volo/abp.ng.identity/config';
export function emailPropContributor(propList: EntityPropList<IdentityUserDto>) {
const index = propList.indexOf('email', (value, name) => value.name === name);
const droppedNode = propList.dropByIndex(index);
const emailProp = new EntityProp<IdentityUserDto>({
...droppedNode.value,
valueResolver: data => {
const { email, emailConfirmed } = data.record;
const icon = email && !emailConfirmed ? `<i class="fa fa-times text-danger ml-1"></i>` : '';
return of((email || '') + icon); // should return an observable
},
});
propList.addByIndex(emailProp, index);
}
export function phonePropContributor(propList: EntityPropList<IdentityUserDto>) {
const index = propList.indexOf('phoneNumber', (value, name) => value.name === name);
const droppedNode = propList.dropByIndex(index);
const phoneProp = new EntityProp<IdentityUserDto>({
...droppedNode.value,
valueResolver: data => {
const { phoneNumber, phoneNumberConfirmed } = data.record;
const icon =
phoneNumber && !phoneNumberConfirmed ? `<i class="fa fa-times text-danger ml-1"></i>` : '';
return of((phoneNumber || '') + icon); // should return an observable
},
});
propList.addByIndex(phoneProp, index);
}
export const identityEntityPropContributors: IdentityEntityPropContributors = {
'Identity.UsersComponent': [emailPropContributor, phonePropContributor],
};
```
> The `valueResolver` method should return an observable. You can wrap your return values with `of` from RxJS for that.
## Object Extensions
Extra properties defined on an existing entity will be included in the table based on their configuration. The values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior.
## API
### PropData\<R = any\>
`PropData` is the shape of the parameter passed to all callbacks or predicates in an `EntityProp`.
It has the following properties:
- **record** is the row data, i.e. current value rendered in the table.
```js
{
type: ePropType.String,
name: 'name',
valueResolver: data => {
const name = data.record.name || '';
return of(name.toUpperCase());
},
}
```
- **index** is the table index where the record is at.
- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleTableComponent`, including, but not limited to, its parent component.
```js
{
type: ePropType.String,
name: 'name',
valueResolver: data => {
const restService = data.getInjected(RestService);
const usersComponent = data.getInjected(UsersComponent);
// Use restService and usersComponent public props and methods here
},
}
```
### PropCallback\<T, R = any\>
`PropCallback` is the type of the callback function that can be passed to an `EntityProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation:
```js
type PropCallback<T, R = any> = (data?: PropData<T>) => R;
```
### PropPredicate\<T\>
`PropPredicate` is the type of the predicate function that can be passed to an `EntityProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation:
```js
type PropPredicate<T> = (data?: PropData<T>) => boolean;
```
### EntityPropOptions\<R = any\>
`EntityPropOptions` is the type that defines required and optional properties you have to pass in order to create an entity prop.
Its type definition is as follows:
```js
type EntityPropOptions<R = any> = {
type: ePropType;
name: string;
displayName?: string;
valueResolver?: PropCallback<R, Observable<any>>;
sortable?: boolean;
columnWidth?: number;
permission?: string;
visible?: PropPredicate<R>;
};
```
As you see, passing `type` and `name` is enough to create an entity prop. Here is what each property is good for:
- **type** is the type of the prop value. It is used for custom rendering in the table. (_required_)
- **name** is the property name (or key) which will be used to read the value of the prop. (_required_)
- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`)
- **valueResolver** is a callback that is called when the cell is rendered. It must return an observable. (_default:_ `data => of(data.record[options.name])`)
- **sortable** defines if the table is sortable based on this entity prop. Sort icons are shown based on it. (_default:_ `false`)
- **columnWidth** defines a minimum width for the column. Good for horizontal scroll. (_default:_ `undefined`)
- **permission** is the permission context which will be used to decide if a column for this entity prop should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if this entity prop should be displayed on the table or not. (_default:_ `() => true`)
> Important Note: Do not use record in visibility predicates. First of all, the table header checks it too and the record will be `undefined`. Second, if some cells are displayed and others are not, the table will be broken. Use the `valueResolver` and render an empty cell when you need to hide a specific cell.
You may find a full example below.
### EntityProp\<R = any\>
`EntityProp` is the class that defines your entity props. It takes an `EntityPropOptions` and sets the default values to the properties, creating an entity prop that can be passed to an entity contributor.
```js
const options: EntityPropOptions<IdentityUserDto> = {
type: ePropType.String,
name: 'email',
displayName: 'AbpIdentity::EmailAddress',
valueResolver: data => {
const { email, emailConfirmed } = data.record;
return of(
(email || '') + (emailConfirmed ? `<i class="fa fa-check text-success ml-1"></i>` : ''),
);
},
sortable: true,
columnWidth: 250,
permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical
visible: data => {
const store = data.getInjected(Store);
const selectSensitiveDataVisibility = ConfigState.getSetting(
'Abp.Identity.IsSensitiveDataVisible' // hypothetical
);
return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true';
}
};
const prop = new EntityProp(options);
```
It also has two static methods to create its instances:
- **EntityProp.create\<R = any\>\(options: EntityPropOptions\<R\>\)** is used to create an instance of `EntityProp`.
```js
const prop = EntityProp.create(options);
```
- **EntityProp.createMany\<R = any\>\(options: EntityPropOptions\<R\>\[\]\)** is used to create multiple instances of `EntityProp` with given array of `EntityPropOptions`.
```js
const props = EntityProp.createMany(optionsArray);
```
### EntityPropList\<R = any\>
`EntityPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md).
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
```js
export function reorderUserContributors(
propList: EntityPropList<IdentityUserDto>,
) {
// drop email node
const emailPropNode = propList.dropByValue(
'AbpIdentity::EmailAddress',
(prop, text) => prop.text === text,
);
// add it back after phoneNumber
propList.addAfter(
emailPropNode.value,
'phoneNumber',
(value, name) => value.name === name,
);
}
```
### EntityPropContributorCallback\<R = any\>
`EntityPropContributorCallback` is the type that you can pass as entity prop contributor callbacks to static `forLazy` methods of the modules.
```js
export function isLockedOutPropContributor(
propList: EntityPropList<IdentityUserDto>,
) {
// add isLockedOutProp as 2nd column
propList.add(isLockedOutProp).byIndex(1);
}
export const identityEntityPropContributors = {
[eIdentityComponents.Users]: [isLockedOutPropContributor],
};
```
## See Also
- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md)

325
docs/en/UI/Angular/Dynamic-Form-Extensions.md

@ -0,0 +1,325 @@
# Dynamic Form (or Form Prop) Extensions for Angular UI
## Introduction
Form prop extension system allows you to add a new field to the create and/or edit forms for a form or change/remove an already existing one. A "Date of Birth" field was added to the user management page below:
![Form Prop Extension Example: "Date of Birth" Field](images/user-prop-extension-date-of-birth-field-ng.png)
You can validate the field, perform visibility checks, and do more. You will also have access to the current entity when creating a contibutor for an edit form.
## How to Set Up
In this example, we will add a "Date of Birth" field in the user management page of the [Identity Module](../../Modules/Identity.md) and validate it.
### Step 1. Create Form Prop Contributors
The following code prepares two constants named `identityCreateFormPropContributors` and `identityEditFormPropContributors`, ready to be imported and used in your root module:
```js
// form-prop-contributors.ts
import { Validators } from '@angular/forms';
import { ePropType, FormProp, FormPropList } from '@abp/ng.theme.shared/extensions';
import { IdentityCreateFormPropContributors, IdentityEditFormPropContributors, IdentityUserDto } from '@volo/abp.ng.identity';
const birthdayProp = new FormProp<IdentityUserDto>({
type: ePropType.Date,
name: 'birthday',
displayName: 'Date of Birth',
validators: () => [Validators.required],
});
export function birthdayPropContributor(propList: FormPropList<IdentityUserDto>) {
propList.addByIndex(birthdayProp, 4);
}
export const identityCreateFormPropContributors: IdentityCreateFormPropContributors = {
'Identity.UsersComponent': [birthdayPropContributor],
};
export const identityEditFormPropContributors: IdentityEditFormPropContributors = {
'Identity.UsersComponent': [birthdayPropContributor],
};
```
The list of props, conveniently named as `propList`, is a **doubly linked list**. That is why we have used the `addByIndex` method, which adds the given value to the specified index of the list. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note 1:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `birthdayPropContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
> **Important Note 2:** Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.
```js
export const identityCreateFormPropContributors: IdentityCreateFormPropContributors = {
'Identity.UsersComponent': [ birthdayPropContributor ],
};
/* OR */
const identityCreateContributors: IdentityCreateFormPropContributors = {};
identityCreateContributors[eIdentityComponents.Users] = [ birthdayPropContributor ];
export const identityCreateFormPropContributors = identityCreateContributors;
```
### Step 2. Import and Use Form Prop Contributors
Import `identityCreateFormPropContributors` and `identityEditFormPropContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
```js
import {
identityCreateFormPropContributors,
identityEditFormPropContributors,
} from './form-prop-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
createFormPropContributors: identityCreateFormPropContributors,
editFormPropContributors: identityEditFormPropContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `birthdayProp` form prop will be added, and you will see the datepicker for the "Date of Birth" field right before the "Email address" in the forms of the users page in the `IdentityModule`.
## Object Extensions
Extra properties defined on an existing entity will be included in the create and edit forms and validated based on their configuration. The form values will also be mapped to and from `extraProperties` automatically. They are available when defining custom contributors, so you can drop, modify, or reorder them. The `isExtra` identifier will be set to `true` for these properties and will define this automatic behavior.
## API
### PropData\<R = any\>
`PropData` is the shape of the parameter passed to all callbacks or predicates in a `FormProp`.
It has the following properties:
- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `ExtensibleFormPropComponent`, including, but not limited to, its parent components.
```js
{
type: ePropType.Enum,
name: 'myField',
options: data => {
const restService = data.getInjected(RestService);
const usersComponent = data.getInjected(UsersComponent);
// Use restService and usersComponent public props and methods here
}
},
```
- **record** is the row data, i.e. current value of the selected item to edit. This property is _available only on edit forms_.
```js
{
type: ePropType.String,
name: 'myProp',
readonly: data => data.record.someOtherProp,
}
```
### PropCallback\<T, R = any\>
`PropCallback` is the type of the callback function that can be passed to a `FormProp` as `prop` parameter. A prop callback gets a single parameter, the `PropData`. The return type may be anything, including `void`. Here is a simplified representation:
```js
type PropCallback<T, R = any> = (data?: PropData<T>) => R;
```
### PropPredicate\<T\>
`PropPredicate` is the type of the predicate function that can be passed to a `FormProp` as `visible` parameter. A prop predicate gets a single parameter, the `PropData`. The return type must be `boolean`. Here is a simplified representation:
```js
type PropPredicate<T> = (data?: PropData<T>) => boolean;
```
### FormPropOptions\<R = any\>
`FormPropOptions` is the type that defines required and optional properties you have to pass in order to create a form prop.
Its type definition is as follows:
```js
type FormPropOptions<R = any> = {
type: ePropType;
name: string;
displayName?: string;
id?: string;
permission?: string;
visible?: PropPredicate<R>;
readonly?: PropPredicate<R>;
disabled?: PropPredicate<R>;
validators?: PropCallback<R, ValidatorFn[]>;
asyncValidators?: PropCallback<R, AsyncValidatorFn[]>;
defaultValue?: boolean | number | string | Date;
options?: PropCallback<R, Observable<ABP.Option<any>[]>>;
autocomplete?: string;
isExtra? boolean;
};
```
As you see, passing `type` and `name` is enough to create a form prop. Here is what each property is good for:
- **type** is the type of the prop value. It defines which input is rendered for the prop in the form. (_required_)
- **name** is the property name (or key) which will be used to read the value of the prop. (_required_)
- **displayName** is the name of the property which will be localized and shown as column header. (_default:_ `options.name`)
- **id** will be set as the `for` attribute of the label and the `id` attribute of the input for the field. (_default:_ `options.name`)
- **permission** is the permission context which will be used to decide if a column for this form prop should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if this prop should be displayed on the form or not. (_default:_ `() => true`)
- **readonly** is a predicate that will be used to decide if this prop should be readonly or not. (_default:_ `() => false`)
- **disabled** is a predicate that will be used to decide if this prop should be disabled or not. (_default:_ `() => false`)
- **validators** is a callback that returns validators for the prop. (_default:_ `() => []`)
- **asyncValidators** is a callback that returns async validators for the prop. (_default:_ `() => []`)
- **defaultValue** is the initial value the field will have. (_default:_ `null`)
- **options** is a callback that is called when a dropdown is needed. It must return an observable. (_default:_ `undefined`)
- **autocomplete** will be set as the `autocomplete` attribute of the input for the field. Please check [possible values](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete#Values). (_default:_ `'off'`)
- **isExtra** indicates this prop is an object extension. When `true`, the value of the field will be mapped from and to `extraProperties` of the entity. (_default:_ `undefined`)
> Important Note: Do not use `record` property of `PropData` in create form predicates and callbacks, because it will be `undefined`. You can use it on edit form contributors though.
You may find a full example below.
### FormProp\<R = any\>
`FormProp` is the class that defines your form props. It takes a `FormPropOptions` and sets the default values to the properties, creating a form prop that can be passed to a form contributor.
```js
const options: FormPropOptions<IdentityUserDto> = {
type: ePropType.Enum,
name: 'myProp',
displayName: 'Default::MyPropName',
id: 'my-prop',
permission: 'AbpIdentity.Users.ReadSensitiveData', // hypothetical
visible: data => {
const store = data.getInjected(Store);
const selectSensitiveDataVisibility = ConfigState.getSetting(
'Abp.Identity.IsSensitiveDataVisible' // hypothetical
);
return store.selectSnapshot(selectSensitiveDataVisibility).toLowerCase() === 'true';
},
readonly: data => data.record.someProp,
disabled: data => data.record.someOtherProp,
validators: () => [Validators.required],
asyncValidators: data => {
const http = data.getInjected(HttpClient);
function validate(control: AbstractControl): Observable<ValidationErrors | null> {
if (control.pristine) return of(null);
return http
.get('https://api.my-brand.io/hypothetical/endpoint/' + control.value)
.pipe(map(response => (response.valid ? null : { invalid: true })));
}
return [validate];
},
defaultValue: 0,
options: data => {
const service = data.getInjected(MyIdentityService);
return service.getMyPropOptions()
.pipe(
map(({items}) => items.map(
item => ({key: item.name, value: item.id })
)),
);
},
autocomplete: 'off',
isExtra: true,
};
const prop = new FormProp(options);
```
It also has two static methods to create its instances:
- **FormProp.create\<R = any\>\(options: FormPropOptions\<R\>\)** is used to create an instance of `FormProp`.
```js
const prop = FormProp.create(options);
```
- **FormProp.createMany\<R = any\>\(options: FormPropOptions\<R\>\[\]\)** is used to create multiple instances of `FormProp` with given array of `FormPropOptions`.
```js
const props = FormProp.createMany(optionsArray);
```
### FormPropList\<R = any\>
`FormPropList` is the list of props passed to every prop contributor callback as the first parameter named `propList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md).
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
```js
export function reorderUserContributors(
propList: FormPropList<IdentityUserDto>,
) {
// drop email node
const emailPropNode = propList.dropByValue(
'AbpIdentity::EmailAddress',
(prop, displayName) => prop.displayName === displayName,
);
// add it back after phoneNumber
propList.addAfter(
emailPropNode.value,
'phoneNumber',
(value, name) => value.name === name,
);
}
```
### CreateFormPropContributorCallback\<R = any\>
`CreateFormPropContributorCallback` is the type that you can pass as **create form** prop contributor callbacks to static `forLazy` methods of the modules.
```js
export function myPropCreateContributor(
propList: FormPropList<IdentityUserDto>,
) {
// add myProp as 2nd field from the start
propList.add(myProp).byIndex(1);
}
export const identityCreateFormPropContributors = {
[eIdentityComponents.Users]: [myPropCreateContributor],
};
```
### EditFormPropContributorCallback\<R = any\>
`EditFormPropContributorCallback` is the type that you can pass as **edit form** prop contributor callbacks to static `forLazy` methods of the modules.
```js
export function myPropEditContributor(
propList: FormPropList<IdentityUserDto>,
) {
// add myProp as 2nd field from the end
propList.add(myProp).byIndex(-1);
}
export const identityEditFormPropContributors = {
[eIdentityComponents.Users]: [myPropEditContributor],
};
```
## See Also
- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md)

442
docs/en/UI/Angular/Entity-Action-Extensions.md

@ -0,0 +1,442 @@
# Entity Action Extensions for Angular UI
## Introduction
Entity action extension system allows you to add a new action to the action menu for an entity. A "Click Me" action was added to the user management page below:
![Entity Action Extension Example: "Click Me!" Action](images/user-action-extension-click-me-ng.png)
You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can access to the current entity in your code.
## How to Set Up
In this example, we will add a "Click Me!" action and alert the current row's `userName` in the user management page of the [Identity Module](../../Modules/Identity.md).
### Step 1. Create Entity Action Contributors
The following code prepares a constant named `identityEntityActionContributors`, ready to be imported and used in your root module:
```js
// entity-action-contributors.ts
import { EntityAction, EntityActionList } from '@abp/ng.theme.shared/extensions';
import { IdentityEntityActionContributors, IdentityUserDto } from '@volo/abp.ng.identity';
const alertUserName = new EntityAction<IdentityUserDto>({
text: 'Click Me!',
action: data => {
// Replace alert with your custom code
alert(data.record.userName);
},
// See EntityActionOptions in API section for all options
});
export function alertUserNameContributor(
actionList: EntityActionList<IdentityUserDto>,
) {
actionList.addTail(alertUserName);
}
export const identityEntityActionContributors: IdentityEntityActionContributors = {
// enum indicates the page to add contributors to
[eIdentityComponents.Users]: [
alertUserNameContributor,
// You can add more contributors here
],
};
```
The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addTail` method, which adds the given value to the end of the list. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note 1:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `alertUserNameContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
> **Important Note 2:** Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.
```js
export const identityEntityActionContributors: IdentityEntityActionContributors = {
'Identity.UsersComponent': [ alertUserNameContributor ],
};
/* OR */
const identityContributors: IdentityEntityActionContributors = {};
identityContributors[eIdentityComponents.Users] = [ alertUserNameContributor ];
export const identityEntityActionContributors = identityContributors;
```
### Step 2. Import and Use Entity Action Contributors
Import `identityEntityActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
```js
import { identityEntityActionContributors } from './entity-action-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
entityActionContributors: identityEntityActionContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `alertUserName` entity action will be added as the last action on the grid dropdown in the users page (`UsersComponent`) of the `IdentityModule`.
## How to Place a Custom Modal and Trigger It by Entity Actions
Incase you need to place a custom modal that will be triggered by an entity action, there are two ways to do it: A quick one and an elaborate one.
### The Quick Solution
1. Place your custom modal inside `AppComponent` template.
```html
<abp-modal [(visible)]="isModalOpen">
<ng-template #abpHeader>
<h3><!-- YOUR TITLE HERE --></h3>
</ng-template>
<ng-template #abpBody>
<!-- YOUR CONTENT HERE -->
</ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpIdentity::Cancel' | abpLocalization }}}%}
</button>
<!-- YOUR CONFIRMATION BUTTON HERE -->
</ng-template>
</abp-modal>
```
2. Add the following inside your `AppComponent` class:
```js
isModalOpen: boolean;
openModal(/* may take parameters */) {
/* and set things before showing the modal */
this.isModalOpen = true;
}
```
3. Add an entity action similar to this:
```js
const customModalAction = new EntityAction<IdentityUserDto>({
text: 'Custom Modal Action',
action: data => {
const component = data.getInjected(AppComponent);
component.openModal(/* you may pass parameters */);
},
});
```
That should work. However, there is a longer but lazy-loading solution, and we are going to use NGXS for it.
### The Elaborate Solution
Consider the modal will be displayed in the Identity module. How can we lazy-load it too?
1. Create a folder called `identity-extended` inside your app folder.
2. Create a file called `identity-popups.store.ts` in it.
3. Insert the following code in the new file:
```js
import { Action, Selector, State, StateContext } from '@ngxs/store';
export class ToggleIdentityPopup {
static readonly type = '[IdentityPopups] Toggle';
constructor(public readonly payload: boolean) {}
}
@State<IdentityPopupsStateModel>({
name: 'IdentityPopups',
defaults: {
isVisible: false,
},
})
export class IdentityPopupsState {
@Selector()
static isVisible(state: IdentityPopupsStateModel) {
return state.isVisible;
}
@Action(ToggleIdentityPopup)
toggleModal(
context: StateContext<IdentityPopupsStateModel>,
{ payload }: ToggleIdentityPopup,
) {
context.patchState({ isVisible: payload });
}
}
interface IdentityPopupsStateModel {
isVisible: boolean;
}
```
4. Create a file called `identity-extended.module.ts` in the same folder.
5. Insert the following code in the new file:
```js
import { CoreModule } from '@abp/ng.core';
import { ThemeSharedModule } from '@abp/ng.theme.shared';
import { Component, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { NgxsModule, Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { IdentityPopupsState, ToggleIdentityPopup } from './identity-popups.store';
@Component({
template: `
<router-outlet></router-outlet>
<router-outlet name="popup"></router-outlet>
`,
})
export class IdentityOutletComponent {}
@Component({
template: `
<abp-modal [visible]="isVisible$ | async" (disappear)="onDisappear()">
<ng-template #abpHeader>
<h3><!-- YOUR TITLE HERE --></h3>
</ng-template>
<ng-template #abpBody>
<!-- YOUR CONTENT HERE -->
</ng-template>
<ng-template #abpFooter>
<button type="button" class="btn btn-secondary" #abpClose>
{%{{{ 'AbpIdentity::Cancel' | abpLocalization }}}%}
</button>
<!-- YOUR CONFIRMATION BUTTON HERE -->
</ng-template>
</abp-modal>
`,
})
export class IdentityPopupsComponent {
@Select(IdentityPopupsState.isVisible)
isVisible$: Observable<boolean>;
constructor(private store: Store) {}
onDisappear() {
this.store.dispatch(new ToggleIdentityPopup(false));
}
}
@NgModule({
declarations: [IdentityPopupsComponent, IdentityOutletComponent],
imports: [
CoreModule,
ThemeSharedModule,
NgxsModule.forFeature([IdentityPopupsState]),
RouterModule.forChild([
{
path: '',
component: IdentityOutletComponent,
children: [
{
path: '',
outlet: 'popup',
component: IdentityPopupsComponent,
},
{
path: '',
loadChildren: () => import('@volo/abp.ng.identity').then(m => m.IdentityModule),
},
],
},
]),
],
})
export class IdentityExtendedModule {}
```
6. Change the `identity` path in your `AppRoutingModule` to this:
```js
{
path: 'identity',
loadChildren: () =>
import('./identity-extended/identity-extended.module').then(m => m.IdentityExtendedModule),
},
```
7. Add an entity action similar to this:
```js
const customModalAction = new EntityAction<IdentityUserDto>({
text: 'Custom Modal Action',
action: data => {
const store = data.getInjected(Store);
store.dispatch(new ToggleIdentityPopup(true));
},
});
```
It should now be working well with lazy-loading. The files are compact in the description to make it quicker to explain. You may split the files as you wish.
## API
### ActionData\<R = any\>
`ActionData` is the shape of the parameter passed to all callbacks or predicates in an `EntityAction`.
It has the following properties:
- **record** is the row data, i.e. current value rendered in the table.
```js
{
text: 'Click Me!',
action: data => {
alert(data.record.userName);
},
}
```
- **index** is the table index where the record is at.
- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `GridActionsComponent`, including, but not limited to, its parent component.
```js
{
text: 'Click Me!',
action: data => {
const restService = data.getInjected(RestService);
// Use restService public props and methods here
},
visible: data => {
const usersComponent = data.getInjected(UsersComponent);
// Use usersComponent public props and methods here
},
}
```
### ActionCallback\<T, R = any\>
`ActionCallback` is the type of the callback function that can be passed to an `EntityAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation:
```js
type ActionCallback<T, R = any> = (data?: ActionData<T>) => R;
```
### ActionPredicate\<T\>
`ActionPredicate` is the type of the predicate function that can be passed to an `EntityAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation:
```js
type ActionPredicate<T> = (data?: ActionData<T>) => boolean;
```
### EntityActionOptions\<R = any\>
`EntityActionOptions` is the type that defines required and optional properties you have to pass in order to create an entity action.
Its type definition is as follows:
```js
type EntityActionOptions<R = any> = {
action: ActionCallback<R>,
text: string,
icon?: string,
permission?: string,
visible?: ActionPredicate<R>,
};
```
As you see, passing `action` and `text` is enough to create an entity action. Here is what each property is good for:
- **action** is a callback that is called when the grid action is clicked. (_required_)
- **text** is the button text which will be localized. (_required_)
- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`)
- **permission** is the permission context which will be used to decide if this type of grid action should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if the current record should have this grid action or not. (_default:_ `() => true`)
You may find a full example below.
### EntityAction\<R = any\>
`EntityAction` is the class that defines your entity actions. It takes an `EntityActionOptions` and sets the default values to the properties, creating an entity action that can be passed to an entity contributor.
```js
const options: EntityActionOptions<IdentityUserDto> = {
action: data => {
const component = data.getInjected(UsersComponent);
component.unlock(data.record.id);
},
text: 'AbpIdentity::Unlock',
icon: 'fa fa-unlock',
permission: 'AbpIdentity.Users.Update',
visible: data => data.record.isLockedOut,
};
const action = new EntityAction(options);
```
It also has two static methods to create its instances:
- **EntityAction.create\<R = any\>\(options: EntityActionOptions\<R\>\)** is used to create an instance of `EntityAction`.
```js
const action = EntityAction.create(options);
```
- **EntityAction.createMany\<R = any\>\(options: EntityActionOptions\<R\>\[\]\)** is used to create multiple instances of `EntityAction` with given array of `EntityActionOptions`.
```js
const actions = EntityAction.createMany(optionsArray);
```
### EntityActionList\<R = any\>
`EntityActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md).
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
```js
export function reorderUserContributors(
actionList: EntityActionList<IdentityUserDto>,
) {
// drop "Unlock" button
const unlockActionNode = actionList.dropByValue(
'AbpIdentity::Unlock',
(action, text) => action.text === text,
);
// add it back to the head of the list
actionList.addHead(unlockActionNode.value);
}
```
### EntityActionContributorCallback\<R = any\>
`EntityActionContributorCallback` is the type that you can pass as entity action contributor callbacks to static `forLazy` methods of the modules.
```js
// lockUserContributor should have EntityActionContributorCallback<IdentityUserDto> type
export function lockUserContributor(
actionList: EntityActionList<IdentityUserDto>,
) {
// add lockUser as 3rd action
actionList.add(lockUser).byIndex(2);
}
export const identityEntityActionContributors = {
[eIdentityComponents.Users]: [lockUserContributor],
};
```
## See Also
- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md)

66
docs/en/UI/Angular/Form-Validation.md

@ -6,23 +6,52 @@ Reactive forms in ABP Angular UI are validated by [ngx-validate](https://www.npm
## How to Add New Error Messages
You can add a new error message by providing the `VALIDATION_BLUEPRINTS` injection token from your root module.
You can add a new error message by passing validation options to the `ThemeSharedModule` in your root module.
```js
import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core";
import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
@NgModule({
imports: [
ThemeSharedModule.forRoot({
validation: {
blueprints: {
uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]",
},
},
// rest of theme shared config
}),
// other imports
],
// rest of the module metadata
})
export class AppModule {}
```
Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work.
```js
import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core";
import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
@NgModule({
providers: [
// other providers
{
provide: VALIDATION_BLUEPRINTS,
useValue: {
...DEFAULT_VALIDATION_BLUEPRINTS,
uniqueUsername: "::AlreadyExists[{%{{{ username }}}%}]",
},
},
// other providers
],
// rest of the module metadata
})
export class AppModule {}
```
@ -40,7 +69,7 @@ In this example;
## How to Change Existing Error Messages
You can overwrite an existing error message by providing `VALIDATION_BLUEPRINTS` injection token from your root module. Let's imagine you have a custom localization resource for required inputs.
You can overwrite an existing error message by passing validation options to the `ThemeSharedModule` in your root module. Let's imagine you have a custom localization resource for required inputs.
```json
"RequiredInput": "Oops! We need this input."
@ -50,19 +79,48 @@ To use this instead of the built-in required input message, all you need to do i
```js
import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core";
import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
@NgModule({
imports: [
ThemeSharedModule.forRoot({
validation: {
blueprints: {
required: "::RequiredInput",
},
},
// rest of theme shared config
}),
// other imports
],
// rest of the module metadata
})
export class AppModule {}
```
Alternatively, you may provide the `VALIDATION_BLUEPRINTS` token directly in your root module. Please do not forget to spread `DEFAULT_VALIDATION_BLUEPRINTS`. Otherwise, built-in ABP validation messages will not work.
```js
import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core";
import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
@NgModule({
providers: [
// other providers
{
provide: VALIDATION_BLUEPRINTS,
useValue: {
...DEFAULT_VALIDATION_BLUEPRINTS,
required: "::RequiredInput",
},
},
// other providers
],
// rest of the module metadata
})
export class AppModule {}
```

420
docs/en/UI/Angular/Page-Toolbar-Extensions.md

@ -0,0 +1,420 @@
# Page Toolbar Extensions for Angular UI
## Introduction
Page toolbar extension system allows you to add a new action to the toolbar of a page. A "Click Me" action was added to the user management page below:
![Page Toolbar Extension Example: "Click Me!" Action](images/user-page-toolbar-extension-click-me-ng.png)
You can take any action (open a modal, make an HTTP API call, redirect to another page... etc) by writing your custom code. You can also access to page data (the main record, usually an entity list) in your code. Additionally, you can pass in custom components instead of using the default button.
## How to Add an Action to Page Toolbar
In this example, we will add a "Click Me!" action and log `userName` of all users in the user management page of the [Identity Module](../../Modules/Identity.md) to the console.
### Step 1. Create Toolbar Action Contributors
The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module:
```js
// toolbar-action-contributors.ts
import { ToolbarActionList, ToolbarAction } from '@abp/ng.theme.shared/extensions';
import { IdentityToolbarActionContributors, IdentityUserDto } from '@volo/abp.ng.identity';
const logUserNames = new ToolbarAction<IdentityUserDto[]>({
text: 'Click Me!',
action: data => {
// Replace log with your custom code
data.record.forEach(user => console.log(user.userName));
},
// See ToolbarActionOptions in API section for all options
});
export function logUserNamesContributor(
actionList: ToolbarActionList<IdentityUserDto[]>
) {
actionList.addHead(logUserNames);
}
export const identityToolbarActionContributors: IdentityToolbarActionContributors = {
// enum indicates the page to add contributors to
[eIdentityComponents.Users]: [
logUserNamesContributor,
// You can add more contributors here
],
};
```
The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addHead` method, which adds the given value to the beginning of the list. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `logUserNamesContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
### Step 2. Import and Use Toolbar Action Contributors
Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below:
```js
import { identityToolbarActionContributors } from './toolbar-action-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
toolbarActionContributors: identityToolbarActionContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule`.
## How to Add a Custom Component to Page Toolbar
In this example, we will add a custom "Click Me!" button and log `userName` of all users in the user management page of the [Identity Module](../../Modules/Identity.md) to the console.
### Step 1. Create A Custom Component
We need to have a component before we can pass it to the toolbar action contributors:
```js
// click-me-button.component.ts
import { Component, Inject } from '@angular/core';
import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.theme.shared/extensions';
import { IdentityUserDto } from '@volo/abp.ng.identity';
@Component({
selector: 'app-click-me-button',
template: `
<button class="btn btn-warning" (click)="handleClick()">Click Me!</button>
`,
})
export class ClickMeButtonComponent {
constructor(
@Inject(EXTENSIONS_ACTION_DATA)
private data: ActionData<IdentityUserDto[]>
) {}
handleClick() {
this.data.record.forEach(user => console.log(user.userName));
}
}
```
Here, `EXTENSIONS_ACTION_DATA` token provides us the context from the page toolbar. Therefore, we are able to reach the page data via `record`, which is an array of users, i.e. `IdentityUserDto[]`.
> We could also import `EXTENSIONS_ACTION_CALLBACK` from **@abp/ng.theme.shared/extensions** package, which is a higher order function that triggers the predefined `action` when called. It passes `ActionData` as the first parameter, so you do not have to pass it explicitly. In other words, `EXTENSIONS_ACTION_CALLBACK` can be called without any parameters and it will not fail.
### Step 2. Create Toolbar Action Contributors
The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module. When `ToolbarComponent` is used instead of `ToolbarAction`, we can pass a component in:
```js
// toolbar-action-contributors.ts
import { ToolbarActionList, ToolbarComponent } from '@abp/ng.theme.shared/extensions';
import { IdentityUserDto } from '@volo/abp.ng.identity';
import { IdentityToolbarActionContributors } from '@volo/abp.ng.identity/config';
import { ClickMeButtonComponent } from './click-me-button.component';
const logUserNames = new ToolbarComponent<IdentityUserDto[]>({
component: ClickMeButtonComponent,
// See ToolbarActionOptions in API section for all options
});
export function logUserNamesContributor(
actionList: ToolbarActionList<IdentityUserDto[]>
) {
actionList.addHead(logUserNames);
}
export const identityToolbarActionContributors: IdentityToolbarActionContributors = {
// enum indicates the page to add contributors to
[eIdentityComponents.Users]: [
logUserNamesContributor,
// You can add more contributors here
],
};
```
The list of actions, conveniently named as `actionList`, is a **doubly linked list**. That is why we have used the `addHead` method, which adds the given value to the beginning of the list. You may find [all available methods here](../Common/Utils/Linked-List.md).
> **Important Note 1:** AoT compilation does not support function calls in decorator metadata. This is why we have defined `logUserNamesContributor` as an exported function declaration here. Please do not forget exporting your contributor callbacks and forget about lambda functions (a.k.a. arrow functions). Please refer to [AoT metadata errors](https://angular.io/guide/aot-metadata-errors#function-calls-not-supported) for details.
> **Important Note 2:** Please use one of the following if Ivy is not enabled in your project. Otherwise, you will get an "Expression form not supported." error.
```js
export const identityToolbarActionContributors: IdentityToolbarActionContributors = {
'Identity.UsersComponent': [ logUserNamesContributor ],
};
/* OR */
const identityContributors: IdentityToolbarActionContributors = {};
identityContributors[eIdentityComponents.Users] = [ logUserNamesContributor ];
export const identityToolbarActionContributors = identityContributors;
```
### Step 3. Import and Use Toolbar Action Contributors
Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below. If Ivy is not enabled in your project, do not forget putting `ClickMeButtonComponent` into `entryComponents`:
```js
import { identityToolbarActionContributors } from './toolbar-action-contributors';
const routes: Routes = [
{
path: '',
component: DynamicLayoutComponent,
children: [
{
path: 'identity',
loadChildren: () =>
import('@volo/abp.ng.identity').then(m =>
m.IdentityModule.forLazy({
toolbarActionContributors: identityToolbarActionContributors,
}),
),
},
// other child routes
],
// other routes
}
];
```
That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule` and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components.
![Page Toolbar Extension Example: Custom "Click Me!" Button](images/user-page-toolbar-extension-custom-click-me-ng.png)
## How to Place a Custom Modal and Trigger It by Toolbar Actions
Please check the same topic in [entity action extensions document](Entity-Action-Extensions.md) and replace entity action with a toolbar action.
## API
### ActionData\<R = any\>
`ActionData` is the shape of the parameter passed to all callbacks or predicates in a `ToolbarAction`.
It has the following properties:
- **record** is the page data, the main record on a page, usually an entity list (e.g. list of users).
```js
{
text: 'Click Me!',
action: data => {
data.record.forEach(user => {
console.lof(user.userName);
});
},
}
```
- **getInjected** is the equivalent of [Injector.get](https://angular.io/api/core/Injector#get). You can use it to reach injected dependencies of `PageToolbarComponent`, including, but not limited to, its parent component.
```js
{
text: 'Click Me!',
action: data => {
const restService = data.getInjected(RestService);
// Use restService public props and methods here
},
visible: data => {
const usersComponent = data.getInjected(UsersComponent);
// Use usersComponent public props and methods here
},
}
```
### ActionCallback\<T, R = any\>
`ActionCallback` is the type of the callback function that can be passed to a `ToolbarAction` as `action` parameter. An action callback gets a single parameter, the `ActionData`. The return type may be anything, including `void`. Here is a simplified representation:
```js
type ActionCallback<T, R = any> = (data?: ActionData<T>) => R;
```
### ActionPredicate\<T\>
`ActionPredicate` is the type of the predicate function that can be passed to a `ToolbarAction` as `visible` parameter. An action predicate gets a single parameter, the `ActionData`. The return type must be `boolean`. Here is a simplified representation:
```js
type ActionPredicate<T> = (data?: ActionData<T>) => boolean;
```
### ToolbarActionOptions\<R = any\>
`ToolbarActionOptions` is the type that defines required and optional properties you have to pass in order to create an toolbar action.
Its type definition is as follows:
```js
type ToolbarActionOptions<R = any> = {
action: ActionCallback<R>,
text: string,
icon?: string,
permission?: string,
visible?: ActionPredicate<R>,
};
```
As you see, passing `action` and `text` is enough to create an toolbar action. Here is what each property is good for:
- **action** is a callback that is called when the toolbar action is clicked. (_required_)
- **text** is the button text which will be localized. (_required_)
- **icon** is the classes that define an icon to be placed before the text. (_default:_ `''`)
- **permission** is the permission context which will be used to decide if this toolbar action should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if the page toolbar should have this action or not. (_default:_ `() => true`)
You may find a full example below.
### ToolbarAction\<R = any\>
`ToolbarAction` is the class that defines your toolbar actions. It takes an `ToolbarActionOptions` and sets the default values to the properties, creating an toolbar action that can be passed to an toolbar contributor.
```js
const options: ToolbarActionOptions<IdentityUserDto[]> = {
action: data => {
const service = data.getInjected(MyCustomIdentityService);
const lockedUsers = data.record.filter(user => user.isLockedOut);
service.unlockAll(lockedUsers);
},
text: 'MyProjectName::UnlockAll',
icon: 'fa fa-unlock',
permission: 'AbpIdentity.Users.Update',
visible: data => data.record.some(user => user.isLockedOut),
};
const action = new ToolbarAction(options);
```
It also has two static methods to create its instances:
- **ToolbarAction.create\<R = any\>\(options: ToolbarActionOptions\<R\>\)** is used to create an instance of `ToolbarAction`.
```js
const action = ToolbarAction.create(options);
```
- **ToolbarAction.createMany\<R = any\>\(options: ToolbarActionOptions\<R\>\[\]\)** is used to create multiple instances of `ToolbarAction` with given array of `ToolbarActionOptions`.
### ToolbarComponentOptions\<R = any\>
`ToolbarComponentOptions` is the type that defines required and optional properties you have to pass in order to create an toolbar component.
Its type definition is as follows:
```js
type ToolbarComponentOptions<R = any> = {
component: Type<any>,
action?: ActionCallback<R>,
permission?: string,
visible?: ActionPredicate<R>,
};
```
As you see, passing `action` and `text` is enough to create an toolbar action. Here is what each property is good for:
- **component** is the constructor of the component to be projected. (_required_)
- **action** is a predefined callback that you can reach in your component via `EXTENSIONS_ACTION_CALLBACK` token and trigger. (_optional_)
- **permission** is the permission context which will be used to decide if this toolbar action should be displayed to the user or not. (_default:_ `undefined`)
- **visible** is a predicate that will be used to decide if the page toolbar should have this action or not. (_default:_ `() => true`)
You may find a full example below.
### ToolbarComponent\<R = any\>
`ToolbarComponent` is the class that defines toolbar actions which project a custom component. It takes an `ToolbarComponentOptions` and sets the default values to the properties, creating a toolbar action that can be passed to an toolbar contributor.
```js
const options: ToolbarComponentOptions<IdentityUserDto[]> = {
component: UnlockAllButton,
action: data => {
const service = data.getInjected(MyCustomIdentityService);
const lockedUsers = data.record.filter(user => user.isLockedOut);
service.unlockAll(lockedUsers);
},
permission: 'AbpIdentity.Users.Update',
visible: data => data.record.some(user => user.isLockedOut),
};
const action = new ToolbarComponent(options);
```
It also has two static methods to create its instances:
- **ToolbarComponent.create\<R = any\>\(options: ToolbarComponentOptions\<R\>\)** is used to create an instance of `ToolbarComponent`.
```js
const action = ToolbarComponent.create(options);
```
- **ToolbarComponent.createMany\<R = any\>\(options: ToolbarComponentOptions\<R\>\[\]\)** is used to create multiple instances of `ToolbarComponent` with given array of `ToolbarComponentOptions`.
```js
const actions = ToolbarComponent.createMany(optionsArray);
```
### ToolbarActionList\<R = any\>
`ToolbarActionList` is the list of actions passed to every action contributor callback as the first parameter named `actionList`. It is a **doubly linked list**. You may find [all available methods here](../Common/Utils/Linked-List.md).
The items in the list will be displayed according to the linked list order, i.e. from head to tail. If you want to re-order them, all you have to do is something like this:
```js
export function reorderUserContributors(
actionList: ToolbarActionList<IdentityUserDto[]>,
) {
// drop "New User" button
const newUserActionNode = actionList.dropByValue(
'AbpIdentity::NewUser',
(action, text) => action['text'] === text,
);
// add it back to the head of the list
actionList.addHead(newUserActionNode.value);
}
export const identityEntityActionContributors = {
[eIdentityComponents.Users]: [
logUserNamesContributor,
reorderUserContributors,
],
};
```
### ToolbarActionContributorCallback\<R = any\>
`ToolbarActionContributorCallback` is the type that you can pass as toolbar action contributor callbacks to static `forLazy` methods of the modules.
```js
// exportUsersContributor should have ToolbarActionContributorCallback<IdentityUserDto[]> type
export function exportUsersContributor(
actionList: ToolbarActionList<IdentityUserDto[]>,
) {
// add exportUsers just before the last action
actionList.add(exportUsers).byIndex(-1);
}
export const identityEntityActionContributors = {
[eIdentityComponents.Users]: [exportUsersContributor],
};
```
## See Also
- [Customizing Application Modules Guide](../../Customizing-Application-Modules-Guide.md)

BIN
docs/en/UI/Angular/images/user-action-extension-click-me-ng.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
docs/en/UI/Angular/images/user-page-toolbar-extension-click-me-ng.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
docs/en/UI/Angular/images/user-page-toolbar-extension-custom-click-me-ng.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

BIN
docs/en/UI/Angular/images/user-prop-extension-date-of-birth-field-ng.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

BIN
docs/en/UI/Angular/images/user-prop-extension-name-column-ng.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

163
docs/en/UI/AspNetCore/Page-Toolbar-Extensions.md

@ -0,0 +1,163 @@
# Page Toolbar Extensions for ASP.NET Core UI
Page toolbar system allows you to add components to the toolbar of any page. The page toolbar is the area right to the header of a page. A button ("Import users from excel") was added to the user management page below:
![page-toolbar-button](../../images/page-toolbar-button.png)
You can add any type of view component item to the page toolbar or modify existing items.
## How to Set Up
In this example, we will add an "Import users from excel" button and execute a JavaScript code for the user management page of the [Identity Module](../../Modules/Identity.md).
### Add a New Button to the User Management Page
Write the following code inside the `ConfigureServices` of your web module class:
````csharp
Configure<AbpPageToolbarOptions>(options =>
{
options.Configure<Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel>(toolbar =>
{
toolbar.AddButton(
LocalizableString.Create<MyProjectNameResource>("ImportFromExcel"),
icon: "file-import",
id: "ImportUsersFromExcel",
type: AbpButtonType.Secondary
);
});
});
````
`AddButton` is a shortcut to simply add a button component. Note that you need to add the `ImportFromExcel` to your localization dictionary (json file) to localize the text.
When you run the application, you will see the button added next to the current button list. There are some other parameters of the `AddButton` method (for example, use `order` to set the order of the button component relative to the other components).
### Create a JavaScript File
Now, we can go to the client side to handle click event of the new button. First, add a new JavaScript file to your solution. We added inside the `/Pages/Identity/Users` folder of the `.Web` project:
![user-action-extension-on-solution](../../images/user-action-extension-on-solution.png)
Here, the content of this JavaScript file:
````js
$(function () {
$('#ImportUsersFromExcel').click(function (e) {
e.preventDefault();
alert('TODO: import users from excel');
});
});
````
In the `click` event, you can do anything you need to do.
### Add the File to the User Management Page
Then you need to add this JavaScript file to the user management page. You can take the power of the [Bundling & Minification system](Bundling-Minification.md).
Write the following code inside the `ConfigureServices` of your module class:
````csharp
Configure<AbpBundlingOptions>(options =>
{
options.ScriptBundles.Configure(
typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName,
bundleConfiguration =>
{
bundleConfiguration.AddFiles(
"/Pages/Identity/Users/my-user-extensions.js"
);
});
});
````
This configuration adds `my-user-extensions.js` to the user management page of the Identity Module. `typeof(Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel).FullName` is the name of the bundle in the user management page. This is a common convention used for all the ABP Commercial modules.
## Advanced Use Cases
While you typically want to add a button action to the page toolbar, it is possible to add any type of component.
### Add View Component to a Page Toolbar
First, create a new view component in your project:
![page-toolbar-custom-component](../../images/page-toolbar-custom-component.png)
For this example, we've created a `MyToolbarItem` view component under the `/Pages/Identity/Users/MyToolbarItem` folder.
`MyToolbarItemViewComponent.cs` content:
````csharp
public class MyToolbarItemViewComponent : AbpViewComponent
{
public IViewComponentResult Invoke()
{
return View("~/Pages/Identity/Users/MyToolbarItem/Default.cshtml");
}
}
````
`Default.cshtml` content:
````xml
<span>
<button type="button" class="btn btn-dark">CLICK ME</button>
</span>
````
* `.cshtml` file can contain any type of component(s). It is a typical view component.
* `MyToolbarItemViewComponent` can inject and use any service if you need.
Then you can add the `MyToolbarItemViewComponent` to the user management page:
````csharp
Configure<AbpPageToolbarOptions>(options =>
{
options.Configure<Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel>(
toolbar =>
{
toolbar.AddComponent<MyToolbarItemViewComponent>();
}
);
});
````
* If your component accepts arguments (in the `Invoke`/`InvokeAsync` method), you can pass them to the `AddComponent` method as an anonymous object.
#### Permissions
If your button/component should be available based on a [permission/policy](../../Authorization.md), you can pass the permission/policy name as the `requiredPolicyName` parameter to the `AddButton` and `AddComponent` methods.
### Add a Page Toolbar Contributor
If you perform advanced custom logic while adding an item to a page toolbar, you can create a class that implements the `IPageToolbarContributor` interface or inherits from the `PageToolbarContributor` class:
````csharp
public class MyToolbarContributor : PageToolbarContributor
{
public override Task ContributeAsync(PageToolbarContributionContext context)
{
context.Items.Insert(0, new PageToolbarItem(typeof(MyToolbarItemViewComponent)));
return Task.CompletedTask;
}
}
````
* You can use `context.ServiceProvider` to resolve dependencies if you need.
Then add your class to the `Contributors` list:
````csharp
Configure<AbpPageToolbarOptions>(options =>
{
options.Configure<Volo.Abp.Identity.Web.Pages.Identity.Users.IndexModel>(
toolbar =>
{
toolbar.Contributors.Add(new MyToolbarContributor());
}
);
});
````

3
docs/en/UI/Blazor/Page-Progress.md

@ -0,0 +1,3 @@
# Blazor UI: Page Progress
TODO

18
docs/en/docs-nav.json

@ -353,22 +353,8 @@
"path": "PlugIn-Modules.md"
},
{
"text": "Customizing the Application Modules",
"path": "Customizing-Application-Modules-Guide.md",
"items": [
{
"text": "Extending Entities",
"path": "Customizing-Application-Modules-Extending-Entities.md"
},
{
"text": "Overriding Services",
"path": "Customizing-Application-Modules-Overriding-Services.md"
},
{
"text": "Overriding the User Interface",
"path": "Customizing-Application-Modules-Overriding-User-Interface.md"
}
]
"text": "Customizing/Extending Modules",
"path": "Customizing-Application-Modules-Guide.md"
},
{
"text": "Best Practices",

BIN
docs/en/images/page-toolbar-button.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
docs/en/images/page-toolbar-custom-component.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

2
docs/zh-Hans/CLI.md

@ -103,7 +103,7 @@ abp new Acme.BookStore
* `--preview`: 使用最新的预览版本.
* `--template-source` 或者 `-ts`: 指定自定义模板源用于生成项目,可以使用本地源和网络源(例如 `D:\local-templat``https://.../my-template-file.zip`).
* `--create-solution-folder` 或者 `-csf`: 指定项目是在输出文件夹中的新文件夹中还是直接在输出文件夹中.
* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True;MultipleActiveResultSets=true`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后).
* `--connection-string` 或者 `-cs`: 重写所有 `appsettings.json` 文件的默认连接字符串. 默认连接字符串是 `Server=localhost;Database=MyProjectName;Trusted_Connection=True`. 默认的数据库提供程序是 `SQL Server`. 如果你使用EF Core但需要更改DBMS,可以按[这里所述](Entity-Framework-Core-Other-DBMS.md)进行更改(创建解决方案之后).
* `--local-framework-ref --abp-path`: 使用对项目的本地引用,而不是替换为NuGet包引用.
### update

10
docs/zh-Hans/Entity-Framework-Core-Migrations.md

@ -588,7 +588,7 @@ public class IdentityRoleExtendingService : ITransientDependency
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
````
@ -596,10 +596,10 @@ public class IdentityRoleExtendingService : ITransientDependency
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True"
}
````

2
docs/zh-Hans/Modules/Docs.md

@ -47,7 +47,7 @@ ABP框架的[文档](docs.abp.io)也是使用的此模块.
```json
{
"ConnectionStrings": {
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=MyProject;Trusted_Connection=True"
}
}
```

6
docs/zh-Hans/Samples/Microservice-Demo.md

@ -843,7 +843,7 @@ Swagger UI已配置,是此服务的默认页面. 如果你导航到URL`http://lo
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True",
"Blogging": "mongodb://localhost/MsDemo_Blogging"
}
````
@ -969,8 +969,8 @@ public class ProductServiceMigrationDbContext : AbpDbContext<ProductServiceMigra
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True;MultipleActiveResultSets=true",
"ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True;MultipleActiveResultSets=true"
"Default": "Server=localhost;Database=MsDemo_Identity;Trusted_Connection=True",
"ProductManagement": "Server=localhost;Database=MsDemo_ProductManagement;Trusted_Connection=True"
}
````

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Bootstrap/TagHelpers/Pagination/PagerModel.cs

@ -69,7 +69,7 @@ namespace Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Pagination
}
/// <summary>
/// Gets first two, previous & current & next, last two pages
/// Gets first two, previous, current, next, last two pages
/// </summary>
private List<PageItem> GetPagesWithGaps()
{

40
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/ui-extensions.js

@ -96,7 +96,7 @@
get: _get
};
})();
function initializeObjectExtensions() {
var getShortEnumTypeName = function (enumType) {
@ -149,7 +149,7 @@
return defaultValue;
}
function localizeEnumMember(property, enumMemberValue) {
var enumType = property.config.type;
var enumInfo = abp.objectExtensions.enums[enumType];
@ -188,10 +188,28 @@
var propertyName = propertyNames[i];
var propertyConfig = objectConfig.properties[propertyName];
if (propertyConfig.ui.onTable.isVisible) {
tableProperties.push({
name: propertyName,
config: propertyConfig
});
if (propertyName.endsWith("_Text")) {
var lookupPropertyName = propertyName.replace("_Text", "");
var lookupProperty = objectConfig.properties[lookupPropertyName];
if (lookupProperty) {
tableProperties.push({
name: propertyName,
config: propertyConfig,
lookupPropertyName: lookupPropertyName,
lookupPropertyDisplayName: lookupProperty.displayName
});
} else {
tableProperties.push({
name: propertyName,
config: propertyConfig,
});
}
} else {
tableProperties.push({
name: propertyName,
config: propertyConfig,
});
}
}
}
@ -199,19 +217,23 @@
}
function getValueFromRow(property, row) {
return row.extraProperties[property.name];;
return row.extraProperties[property.name];
}
function convertPropertyToColumnConfig(property) {
var columnConfig = {
title: localizeDisplayName(property.name, property.config.displayName),
data: "extraProperties." + property.name,
orderable: false
};
if (property.lookupPropertyName) {
columnConfig.title = localizeDisplayName(property.lookupPropertyName, property.lookupPropertyDisplayName);
} else {
columnConfig.title = localizeDisplayName(property.name, property.config.displayName);
}
if (property.config.typeSimple === 'enum') {
columnConfig.render = function(data, type, row) {
columnConfig.render = function (data, type, row) {
var value = getValueFromRow(property, row);
return localizeEnumMember(property, value);
}

2
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowActionFilter.cs

@ -46,7 +46,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow
var options = CreateOptions(context, unitOfWorkAttr);
//Trying to begin a reserved UOW by AbpUnitOfWorkMiddleware
if (_unitOfWorkManager.TryBeginReserved(AbpUnitOfWorkMiddleware.UnitOfWorkReservationName, options))
if (_unitOfWorkManager.TryBeginReserved(UnitOfWork.UnitOfWorkReservationName, options))
{
var result = await next();
if (!Succeed(result))

2
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Uow/AbpUowPageFilter.cs

@ -50,7 +50,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow
var options = CreateOptions(context, unitOfWorkAttr);
//Trying to begin a reserved UOW by AbpUnitOfWorkMiddleware
if (_unitOfWorkManager.TryBeginReserved(AbpUnitOfWorkMiddleware.UnitOfWorkReservationName, options))
if (_unitOfWorkManager.TryBeginReserved(UnitOfWork.UnitOfWorkReservationName, options))
{
var result = await next();
if (!Succeed(result))

4
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AbpUnitOfWorkMiddleware.cs

@ -7,8 +7,6 @@ namespace Volo.Abp.AspNetCore.Uow
{
public class AbpUnitOfWorkMiddleware : IMiddleware, ITransientDependency
{
public const string UnitOfWorkReservationName = "_AbpActionUnitOfWork";
private readonly IUnitOfWorkManager _unitOfWorkManager;
public AbpUnitOfWorkMiddleware(IUnitOfWorkManager unitOfWorkManager)
@ -18,7 +16,7 @@ namespace Volo.Abp.AspNetCore.Uow
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
using (var uow = _unitOfWorkManager.Reserve(UnitOfWorkReservationName))
using (var uow = _unitOfWorkManager.Reserve(UnitOfWork.UnitOfWorkReservationName))
{
await next(context);
await uow.CompleteAsync(context.RequestAborted);

52
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AspNetCoreUnitOfWorkTransactionBehaviourProvider.cs

@ -0,0 +1,52 @@
using System;
using System.Net.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Uow;
namespace Volo.Abp.AspNetCore.Uow
{
public class AspNetCoreUnitOfWorkTransactionBehaviourProvider : IUnitOfWorkTransactionBehaviourProvider, ISingletonDependency
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly AspNetCoreUnitOfWorkTransactionBehaviourProviderOptions _options;
public virtual bool? IsTransactional
{
get
{
var httpContext = _httpContextAccessor.HttpContext;
if (httpContext == null)
{
return null;
}
var currentUrl = httpContext.Request.Path.Value;
if (currentUrl != null)
{
foreach (var url in _options.NonTransactionalUrls)
{
if (currentUrl.StartsWith(url, StringComparison.OrdinalIgnoreCase))
{
return false;
}
}
}
return !string.Equals(
httpContext.Request.Method,
HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase
);
}
}
public AspNetCoreUnitOfWorkTransactionBehaviourProvider(
IHttpContextAccessor httpContextAccessor,
IOptions<AspNetCoreUnitOfWorkTransactionBehaviourProviderOptions> options)
{
_httpContextAccessor = httpContextAccessor;
_options = options.Value;
}
}
}

17
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Uow/AspNetCoreUnitOfWorkTransactionBehaviourProviderOptions.cs

@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Uow
{
public class AspNetCoreUnitOfWorkTransactionBehaviourProviderOptions
{
public List<string> NonTransactionalUrls { get; }
public AspNetCoreUnitOfWorkTransactionBehaviourProviderOptions()
{
NonTransactionalUrls = new List<string>
{
"/connect/"
};
}
}
}

6
framework/src/Volo.Abp.Authorization/Microsoft/AspNetCore/Authorization/AuthorizationOptionsExtensions.cs

@ -11,9 +11,9 @@ namespace Microsoft.AspNetCore.Authorization
/// <summary>
/// Gets all policies.
///
///
/// IMPORTANT NOTE: Use this method carefully.
/// It relies on reflection to get all policies from a private field of the <see cref="options"/>.
/// It relies on reflection to get all policies from a private field of the <paramref name="options"/>.
/// This method may be removed in the future if internals of <see cref="AuthorizationOptions"/> changes.
/// </summary>
/// <param name="options"></param>
@ -23,4 +23,4 @@ namespace Microsoft.AspNetCore.Authorization
return ((IDictionary<string, AuthorizationPolicy>) PolicyMapProperty.GetValue(options)).Keys.ToList();
}
}
}
}

16
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionDefinition.cs

@ -54,7 +54,7 @@ namespace Volo.Abp.Authorization.Permissions
///
/// Disabling a permission would be helpful to hide a related application
/// functionality from users/clients.
///
///
/// Default: true.
/// </summary>
public bool IsEnabled { get; set; }
@ -64,8 +64,8 @@ namespace Volo.Abp.Authorization.Permissions
/// </summary>
/// <param name="name">Name of the property</param>
/// <returns>
/// Returns the value in the <see cref="Properties"/> dictionary by given <see cref="name"/>.
/// Returns null if given <see cref="name"/> is not present in the <see cref="Properties"/> dictionary.
/// Returns the value in the <see cref="Properties"/> dictionary by given <paramref name="name"/>.
/// Returns null if given <paramref name="name"/> is not present in the <see cref="Properties"/> dictionary.
/// </returns>
public object this[string name]
{
@ -74,7 +74,7 @@ namespace Volo.Abp.Authorization.Permissions
}
protected internal PermissionDefinition(
[NotNull] string name,
[NotNull] string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
@ -90,14 +90,14 @@ namespace Volo.Abp.Authorization.Permissions
}
public virtual PermissionDefinition AddChild(
[NotNull] string name,
[NotNull] string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
{
var child = new PermissionDefinition(
name,
displayName,
name,
displayName,
multiTenancySide,
isEnabled)
{
@ -138,4 +138,4 @@ namespace Volo.Abp.Authorization.Permissions
return $"[{nameof(PermissionDefinition)} {Name}]";
}
}
}
}

10
framework/src/Volo.Abp.Authorization/Volo/Abp/Authorization/Permissions/PermissionGroupDefinition.cs

@ -36,8 +36,8 @@ namespace Volo.Abp.Authorization.Permissions
/// </summary>
/// <param name="name">Name of the property</param>
/// <returns>
/// Returns the value in the <see cref="Properties"/> dictionary by given <see cref="name"/>.
/// Returns null if given <see cref="name"/> is not present in the <see cref="Properties"/> dictionary.
/// Returns the value in the <see cref="Properties"/> dictionary by given <paramref name="name"/>.
/// Returns null if given <paramref name="name"/> is not present in the <see cref="Properties"/> dictionary.
/// </returns>
public object this[string name]
{
@ -46,7 +46,7 @@ namespace Volo.Abp.Authorization.Permissions
}
protected internal PermissionGroupDefinition(
string name,
string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both)
{
@ -59,7 +59,7 @@ namespace Volo.Abp.Authorization.Permissions
}
public virtual PermissionDefinition AddPermission(
string name,
string name,
ILocalizableString displayName = null,
MultiTenancySides multiTenancySide = MultiTenancySides.Both,
bool isEnabled = true)
@ -131,4 +131,4 @@ namespace Volo.Abp.Authorization.Permissions
return null;
}
}
}
}

34
framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperConventionalRegistrar.cs

@ -0,0 +1,34 @@
using System;
using System.Linq;
using AutoMapper;
using AutoMapper.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.AutoMapper
{
public class AbpAutoMapperConventionalRegistrar : ConventionalRegistrarBase
{
protected readonly Type[] OpenTypes = {
typeof(IValueResolver<,,>),
typeof(IMemberValueResolver<,,,>),
typeof(ITypeConverter<,>),
typeof(IValueConverter<,>),
typeof(IMappingAction<,>)
};
public override void AddType(IServiceCollection services, Type type)
{
if (IsConventionalRegistrationDisabled(type))
{
return;
}
if (type.IsClass && !type.IsAbstract && OpenTypes.Any(type.ImplementsGenericInterface))
{
services.TryAddTransient(type);
}
}
}
}

26
framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperModule.cs

@ -13,24 +13,23 @@ namespace Volo.Abp.AutoMapper
typeof(AbpObjectMappingModule),
typeof(AbpObjectExtendingModule),
typeof(AbpAuditingModule)
)]
)]
public class AbpAutoMapperModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
public override void PreConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAutoMapperObjectMapper();
var mapperAccessor = new MapperAccessor();
context.Services.AddSingleton<IMapperAccessor>(_ => mapperAccessor);
context.Services.AddSingleton<MapperAccessor>(_ => mapperAccessor);
context.Services.AddConventionalRegistrar(new AbpAutoMapperConventionalRegistrar());
}
public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
public override void ConfigureServices(ServiceConfigurationContext context)
{
CreateMappings(context.ServiceProvider);
context.Services.AddAutoMapperObjectMapper();
context.Services.AddSingleton<MapperAccessor>(provider => CreateMappings(provider));
context.Services.AddSingleton<IMapperAccessor>(provider => provider.GetRequiredService<MapperAccessor>());
}
private void CreateMappings(IServiceProvider serviceProvider)
private MapperAccessor CreateMappings(IServiceProvider serviceProvider)
{
using (var scope = serviceProvider.CreateScope())
{
@ -48,7 +47,7 @@ namespace Volo.Abp.AutoMapper
{
foreach (var profileType in options.ValidatingProfiles)
{
config.AssertConfigurationIsValid(((Profile)Activator.CreateInstance(profileType)).ProfileName);
config.AssertConfigurationIsValid(((Profile) Activator.CreateInstance(profileType)).ProfileName);
}
}
@ -59,7 +58,10 @@ namespace Volo.Abp.AutoMapper
ValidateAll(mapperConfiguration);
scope.ServiceProvider.GetRequiredService<MapperAccessor>().Mapper = mapperConfiguration.CreateMapper();
return new MapperAccessor
{
Mapper = new Mapper(mapperConfiguration, serviceProvider.GetService)
};
}
}
}

140
framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs

@ -1,6 +1,6 @@
// This software is part of the Autofac IoC container
// Copyright © 2015 Autofac Contributors
// http://autofac.org
// https://autofac.org
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
@ -24,10 +24,13 @@
// OTHER DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Autofac.Builder;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Autofac;
using Volo.Abp;
using Volo.Abp.Modularity;
namespace Autofac.Extensions.DependencyInjection
@ -46,16 +49,62 @@ namespace Autofac.Extensions.DependencyInjection
/// The <see cref="ContainerBuilder"/> into which the registrations should be made.
/// </param>
/// <param name="services">
/// The set of service descriptors to register in the container.
/// A container builder that can be used to create an <see cref="IServiceProvider" />.
/// </param>
public static void Populate(
this ContainerBuilder builder,
IServiceCollection services)
this ContainerBuilder builder,
IServiceCollection services)
{
builder.RegisterType<AutofacServiceProvider>().As<IServiceProvider>();
builder.RegisterType<AutofacServiceScopeFactory>().As<IServiceScopeFactory>();
Populate(builder, services, null);
}
/// <summary>
/// Populates the Autofac container builder with the set of registered service descriptors
/// and makes <see cref="IServiceProvider"/> and <see cref="IServiceScopeFactory"/>
/// available in the container. Using this overload is incompatible with the ASP.NET Core
/// support for <see cref="IServiceProviderFactory{TContainerBuilder}"/>.
/// </summary>
/// <param name="builder">
/// The <see cref="ContainerBuilder"/> into which the registrations should be made.
/// </param>
/// <param name="services">
/// A container builder that can be used to create an <see cref="IServiceProvider" />.
/// </param>
/// <param name="lifetimeScopeTagForSingletons">
/// If provided and not <see langword="null"/> then all registrations with lifetime <see cref="ServiceLifetime.Singleton" /> are registered
/// using <see cref="IRegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.InstancePerMatchingLifetimeScope" />
/// with provided <paramref name="lifetimeScopeTagForSingletons"/>
/// instead of using <see cref="IRegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.SingleInstance"/>.
/// </param>
/// <remarks>
/// <para>
/// Specifying a <paramref name="lifetimeScopeTagForSingletons"/> addresses a specific case where you have
/// an application that uses Autofac but where you need to isolate a set of services in a child scope. For example,
/// if you have a large application that self-hosts ASP.NET Core items, you may want to isolate the ASP.NET
/// Core registrations in a child lifetime scope so they don't show up for the rest of the application.
/// This overload allows that. Note it is the developer's responsibility to execute this and create an
/// <see cref="AutofacServiceProvider"/> using the child lifetime scope.
/// </para>
/// </remarks>
public static void Populate(
this ContainerBuilder builder,
IServiceCollection services,
object lifetimeScopeTagForSingletons)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
Register(builder, services);
builder.RegisterType<AutofacServiceProvider>().As<IServiceProvider>().ExternallyOwned();
var autofacServiceScopeFactory = typeof(AutofacServiceProvider).Assembly.GetType("Autofac.Extensions.DependencyInjection.AutofacServiceScopeFactory");
if (autofacServiceScopeFactory == null)
{
throw new AbpException("Unable get type of Autofac.Extensions.DependencyInjection.AutofacServiceScopeFactory!");
}
builder.RegisterType(autofacServiceScopeFactory).As<IServiceScopeFactory>();
Register(builder, services, lifetimeScopeTagForSingletons);
}
/// <summary>
@ -65,18 +114,33 @@ namespace Autofac.Extensions.DependencyInjection
/// <typeparam name="TRegistrationStyle">The object registration style.</typeparam>
/// <param name="registrationBuilder">The registration being built.</param>
/// <param name="lifecycleKind">The lifecycle specified on the service registration.</param>
/// <param name="lifetimeScopeTagForSingleton">
/// If not <see langword="null"/> then all registrations with lifetime <see cref="ServiceLifetime.Singleton" /> are registered
/// using <see cref="IRegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.InstancePerMatchingLifetimeScope" />
/// with provided <paramref name="lifetimeScopeTagForSingleton"/>
/// instead of using <see cref="IRegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.SingleInstance"/>.
/// </param>
/// <returns>
/// The <paramref name="registrationBuilder" />, configured with the proper lifetime scope,
/// and available for additional configuration.
/// </returns>
private static IRegistrationBuilder<object, TActivatorData, TRegistrationStyle> ConfigureLifecycle<TActivatorData, TRegistrationStyle>(
this IRegistrationBuilder<object, TActivatorData, TRegistrationStyle> registrationBuilder,
ServiceLifetime lifecycleKind)
this IRegistrationBuilder<object, TActivatorData, TRegistrationStyle> registrationBuilder,
ServiceLifetime lifecycleKind,
object lifetimeScopeTagForSingleton)
{
switch (lifecycleKind)
{
case ServiceLifetime.Singleton:
registrationBuilder.SingleInstance();
if (lifetimeScopeTagForSingleton == null)
{
registrationBuilder.SingleInstance();
}
else
{
registrationBuilder.InstancePerMatchingLifetimeScope(lifetimeScopeTagForSingleton);
}
break;
case ServiceLifetime.Scoped:
registrationBuilder.InstancePerLifetimeScope();
@ -96,59 +160,67 @@ namespace Autofac.Extensions.DependencyInjection
/// The <see cref="ContainerBuilder"/> into which the registrations should be made.
/// </param>
/// <param name="services">
/// The set of service descriptors to register in the container.
/// A container builder that can be used to create an <see cref="IServiceProvider" />.
/// </param>
/// <param name="lifetimeScopeTagForSingletons">
/// If not <see langword="null"/> then all registrations with lifetime <see cref="ServiceLifetime.Singleton" /> are registered
/// using <see cref="IRegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.InstancePerMatchingLifetimeScope" />
/// with provided <paramref name="lifetimeScopeTagForSingletons"/>
/// instead of using <see cref="IRegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.SingleInstance"/>.
/// </param>
[SuppressMessage("CA2000", "CA2000", Justification = "Registrations created here are disposed when the built container is disposed.")]
private static void Register(
ContainerBuilder builder,
IServiceCollection services)
ContainerBuilder builder,
IServiceCollection services,
object lifetimeScopeTagForSingletons)
{
var moduleContainer = services.GetSingletonInstance<IModuleContainer>();
var registrationActionList = services.GetRegistrationActionList();
foreach (var service in services)
foreach (var descriptor in services)
{
if (service.ImplementationType != null)
if (descriptor.ImplementationType != null)
{
// Test if the an open generic type is being registered
var serviceTypeInfo = service.ServiceType.GetTypeInfo();
var serviceTypeInfo = descriptor.ServiceType.GetTypeInfo();
if (serviceTypeInfo.IsGenericTypeDefinition)
{
builder
.RegisterGeneric(service.ImplementationType)
.As(service.ServiceType)
.ConfigureLifecycle(service.Lifetime)
.RegisterGeneric(descriptor.ImplementationType)
.As(descriptor.ServiceType)
.ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons)
.FindConstructorsWith(new AbpAutofacConstructorFinder())
.ConfigureAbpConventions(moduleContainer, registrationActionList);
}
else
{
builder
.RegisterType(service.ImplementationType)
.As(service.ServiceType)
.ConfigureLifecycle(service.Lifetime)
.RegisterType(descriptor.ImplementationType)
.As(descriptor.ServiceType)
.ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons)
.FindConstructorsWith(new AbpAutofacConstructorFinder())
.ConfigureAbpConventions(moduleContainer, registrationActionList);
}
}
else if (service.ImplementationFactory != null)
else if (descriptor.ImplementationFactory != null)
{
var registration = RegistrationBuilder.ForDelegate(service.ServiceType, (context, parameters) =>
{
var serviceProvider = context.Resolve<IServiceProvider>();
return service.ImplementationFactory(serviceProvider);
})
.ConfigureLifecycle(service.Lifetime)
.CreateRegistration();
//TODO: ConfigureAbpConventions ?
var registration = RegistrationBuilder.ForDelegate(descriptor.ServiceType, (context, parameters) =>
{
var serviceProvider = context.Resolve<IServiceProvider>();
return descriptor.ImplementationFactory(serviceProvider);
})
.ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons)
.CreateRegistration();
//TODO: ConfigureAbpConventions ?
builder.RegisterComponent(registration);
}
else
{
builder
.RegisterInstance(service.ImplementationInstance)
.As(service.ServiceType)
.ConfigureLifecycle(service.Lifetime);
.RegisterInstance(descriptor.ImplementationInstance)
.As(descriptor.ServiceType)
.ConfigureLifecycle(descriptor.Lifetime, null);
}
}
}

122
framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceProvider.cs

@ -1,122 +0,0 @@
// This software is part of the Autofac IoC container
// Copyright © 2015 Autofac Contributors
// https://autofac.org
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Autofac.Extensions.DependencyInjection
{
/// <summary>
/// Autofac implementation of the ASP.NET Core <see cref="IServiceProvider"/>.
/// </summary>
/// <seealso cref="System.IServiceProvider" />
/// <seealso cref="Microsoft.Extensions.DependencyInjection.ISupportRequiredService" />
public class AutofacServiceProvider : IServiceProvider, ISupportRequiredService, IDisposable
{
private readonly ILifetimeScope _lifetimeScope;
private bool _disposed = false;
/// <summary>
/// Initializes a new instance of the <see cref="AutofacServiceProvider"/> class.
/// </summary>
/// <param name="lifetimeScope">
/// The lifetime scope from which services will be resolved.
/// </param>
public AutofacServiceProvider(ILifetimeScope lifetimeScope)
{
this._lifetimeScope = lifetimeScope;
}
/// <summary>
/// Gets service of type <paramref name="serviceType" /> from the
/// <see cref="AutofacServiceProvider" /> and requires it be present.
/// </summary>
/// <param name="serviceType">
/// An object that specifies the type of service object to get.
/// </param>
/// <returns>
/// A service object of type <paramref name="serviceType" />.
/// </returns>
/// <exception cref="Autofac.Core.Registration.ComponentNotRegisteredException">
/// Thrown if the <paramref name="serviceType" /> isn't registered with the container.
/// </exception>
/// <exception cref="Autofac.Core.DependencyResolutionException">
/// Thrown if the object can't be resolved from the container.
/// </exception>
public object GetRequiredService(Type serviceType)
{
return this._lifetimeScope.Resolve(serviceType);
}
/// <summary>
/// Gets the service object of the specified type.
/// </summary>
/// <param name="serviceType">
/// An object that specifies the type of service object to get.
/// </param>
/// <returns>
/// A service object of type <paramref name="serviceType" />; or <see langword="null" />
/// if there is no service object of type <paramref name="serviceType" />.
/// </returns>
public object GetService(Type serviceType)
{
return this._lifetimeScope.ResolveOptional(serviceType);
}
/// <summary>
/// Gets the underlying instance of <see cref="ILifetimeScope" />.
/// </summary>
public ILifetimeScope LifetimeScope => _lifetimeScope;
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing">
/// <see langword="true" /> to release both managed and unmanaged resources;
/// <see langword="false" /> to release only unmanaged resources.
/// </param>
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
this._disposed = true;
if (disposing)
{
this._lifetimeScope.Dispose();
}
}
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
}
}

77
framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceProviderFactory.cs

@ -1,77 +0,0 @@
// This software is part of the Autofac IoC container
// Copyright © 2017 Autofac Contributors
// http://autofac.org
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Autofac.Extensions.DependencyInjection
{
/// <summary>
/// A factory for creating a <see cref="ContainerBuilder"/> and an <see cref="T:System.IServiceProvider" />.
/// </summary>
public class AutofacServiceProviderFactory : IServiceProviderFactory<ContainerBuilder>
{
private readonly Action<ContainerBuilder> _configurationAction;
/// <summary>
/// Initializes a new instance of the <see cref="AutofacServiceProviderFactory"/> class.
/// </summary>
/// <param name="configurationAction">Action on a <see cref="ContainerBuilder"/> that adds component registrations to the container.</param>
public AutofacServiceProviderFactory(Action<ContainerBuilder> configurationAction = null)
{
_configurationAction = configurationAction ?? (builder => { });
}
/// <summary>
/// Creates a container builder from an <see cref="T:Microsoft.Extensions.DependencyInjection.IServiceCollection" />.
/// </summary>
/// <param name="services">The collection of services</param>
/// <returns>A container builder that can be used to create an <see cref="T:System.IServiceProvider" />.</returns>
public ContainerBuilder CreateBuilder(IServiceCollection services)
{
var builder = new ContainerBuilder();
builder.Populate(services);
_configurationAction(builder);
return builder;
}
/// <summary>
/// Creates an <see cref="T:System.IServiceProvider" /> from the container builder.
/// </summary>
/// <param name="containerBuilder">The container builder</param>
/// <returns>An <see cref="T:System.IServiceProvider" /></returns>
public IServiceProvider CreateServiceProvider(ContainerBuilder containerBuilder)
{
if (containerBuilder == null) throw new ArgumentNullException(nameof(containerBuilder));
var container = containerBuilder.Build();
return new AutofacServiceProvider(container);
}
}
}

67
framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceScope.cs

@ -1,67 +0,0 @@
// This software is part of the Autofac IoC container
// Copyright © 2015 Autofac Contributors
// http://autofac.org
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
using System;
using Microsoft.Extensions.DependencyInjection;
namespace Autofac.Extensions.DependencyInjection
{
/// <summary>
/// Autofac implementation of the ASP.NET Core <see cref="IServiceScope"/>.
/// </summary>
/// <seealso cref="Microsoft.Extensions.DependencyInjection.IServiceScope" />
internal class AutofacServiceScope : IServiceScope
{
private readonly ILifetimeScope _lifetimeScope;
/// <summary>
/// Initializes a new instance of the <see cref="AutofacServiceScope"/> class.
/// </summary>
/// <param name="lifetimeScope">
/// The lifetime scope from which services should be resolved for this service scope.
/// </param>
public AutofacServiceScope(ILifetimeScope lifetimeScope)
{
this._lifetimeScope = lifetimeScope;
this.ServiceProvider = this._lifetimeScope.Resolve<IServiceProvider>();
}
/// <summary>
/// Gets an <see cref="IServiceProvider" /> corresponding to this service scope.
/// </summary>
/// <value>
/// An <see cref="IServiceProvider" /> that can be used to resolve dependencies from the scope.
/// </value>
public IServiceProvider ServiceProvider { get; }
/// <summary>
/// Disposes of the lifetime scope and resolved disposable services.
/// </summary>
public void Dispose()
{
this._lifetimeScope.Dispose();
}
}
}

65
framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacServiceScopeFactory.cs

@ -1,65 +0,0 @@
// This software is part of the Autofac IoC container
// Copyright © 2015 Autofac Contributors
// http://autofac.org
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.DependencyInjection;
namespace Autofac.Extensions.DependencyInjection
{
/// <summary>
/// Autofac implementation of the ASP.NET Core <see cref="IServiceScopeFactory"/>.
/// </summary>
/// <seealso cref="Microsoft.Extensions.DependencyInjection.IServiceScopeFactory" />
[SuppressMessage("Microsoft.ApiDesignGuidelines", "CA2213", Justification = "The creator of the root service lifetime scope is responsible for disposal.")]
internal class AutofacServiceScopeFactory : IServiceScopeFactory
{
private readonly ILifetimeScope _lifetimeScope;
/// <summary>
/// Initializes a new instance of the <see cref="AutofacServiceScopeFactory"/> class.
/// </summary>
/// <param name="lifetimeScope">The lifetime scope.</param>
public AutofacServiceScopeFactory(ILifetimeScope lifetimeScope)
{
this._lifetimeScope = lifetimeScope;
}
/// <summary>
/// Creates an <see cref="IServiceScope" /> which contains an
/// <see cref="System.IServiceProvider" /> used to resolve dependencies within
/// the scope.
/// </summary>
/// <returns>
/// An <see cref="IServiceScope" /> controlling the lifetime of the scope. Once
/// this is disposed, any scoped services that have been resolved
/// from the <see cref="IServiceScope.ServiceProvider" />
/// will also be disposed.
/// </returns>
public IServiceScope CreateScope()
{
return new AutofacServiceScope(this._lifetimeScope.BeginLifetimeScope());
}
}
}

4
framework/src/Volo.Abp.Autofac/Volo.Abp.Autofac.csproj

@ -15,8 +15,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="6.0.0" />
<PackageReference Include="Autofac" Version="6.0.0" />
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="6.0.0" />
<PackageReference Include="Autofac.Extras.DynamicProxy" Version="6.0.0" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="$(MicrosoftPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="$(MicrosoftPackageVersion)" />
</ItemGroup>

4
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IAsyncBackgroundJob.cs

@ -8,9 +8,9 @@ namespace Volo.Abp.BackgroundJobs
public interface IAsyncBackgroundJob<in TArgs>
{
/// <summary>
/// Executes the job with the <see cref="args"/>.
/// Executes the job with the <paramref name="args"/>.
/// </summary>
/// <param name="args">Job arguments.</param>
Task ExecuteAsync(TArgs args);
}
}
}

4
framework/src/Volo.Abp.BackgroundJobs.Abstractions/Volo/Abp/BackgroundJobs/IBackgroundJob.cs

@ -6,9 +6,9 @@
public interface IBackgroundJob<in TArgs>
{
/// <summary>
/// Executes the job with the <see cref="args"/>.
/// Executes the job with the <paramref name="args"/>.
/// </summary>
/// <param name="args">Job arguments.</param>
void Execute(TArgs args);
}
}
}

10
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/AbpBackgroundWorkersQuartzModule.cs

@ -26,20 +26,20 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
public override void OnPreApplicationInitialization(ApplicationInitializationContext context)
{
var options = context.ServiceProvider.GetService<IOptions<AbpBackgroundWorkerOptions>>().Value;
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundWorkerOptions>>().Value;
if (!options.IsEnabled)
{
var quartzOptions = context.ServiceProvider.GetService<IOptions<AbpQuartzOptions>>().Value;
quartzOptions.StartSchedulerFactory = scheduler => Task.CompletedTask;
var quartzOptions = context.ServiceProvider.GetRequiredService<IOptions<AbpQuartzOptions>>().Value;
quartzOptions.StartSchedulerFactory = _ => Task.CompletedTask;
}
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var quartzBackgroundWorkerOptions = context.ServiceProvider.GetService<IOptions<AbpBackgroundWorkerQuartzOptions>>().Value;
var quartzBackgroundWorkerOptions = context.ServiceProvider.GetRequiredService<IOptions<AbpBackgroundWorkerQuartzOptions>>().Value;
if (quartzBackgroundWorkerOptions.IsAutoRegisterEnabled)
{
var backgroundWorkerManager = context.ServiceProvider.GetService<IBackgroundWorkerManager>();
var backgroundWorkerManager = context.ServiceProvider.GetRequiredService<IBackgroundWorkerManager>();
var works = context.ServiceProvider.GetServices<IQuartzBackgroundWorker>().Where(x=>x.AutoRegister);
foreach (var work in works)

2
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerAdapter.cs

@ -57,7 +57,7 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
.Build();
}
public async override Task Execute(IJobExecutionContext context)
public override async Task Execute(IJobExecutionContext context)
{
var worker = (IBackgroundWorker) ServiceProvider.GetService(typeof(TWorker));
var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider);

9
framework/src/Volo.Abp.BackgroundWorkers.Quartz/Volo/Abp/BackgroundWorkers/Quartz/QuartzBackgroundWorkerManager.cs

@ -19,14 +19,17 @@ namespace Volo.Abp.BackgroundWorkers.Quartz
public virtual async Task StartAsync(CancellationToken cancellationToken = default)
{
await _scheduler.ResumeAll(cancellationToken);
if (_scheduler.IsStarted && _scheduler.InStandbyMode)
{
await _scheduler.Start(cancellationToken);
}
}
public virtual async Task StopAsync(CancellationToken cancellationToken = default)
{
if (!_scheduler.IsShutdown)
if (_scheduler.IsStarted && !_scheduler.InStandbyMode)
{
await _scheduler.PauseAll(cancellationToken);
await _scheduler.Standby(cancellationToken);
}
}

4
framework/src/Volo.Abp.BlazoriseUI/AbpCrudPageBase.cs

@ -439,10 +439,10 @@ namespace Volo.Abp.BlazoriseUI
}
/// <summary>
/// Calls IAuthorizationService.CheckAsync for the given <see cref="policyName"/>.
/// Calls IAuthorizationService.CheckAsync for the given <paramref name="policyName"/>.
/// Throws <see cref="AbpAuthorizationException"/> if given policy was not granted for the current user.
///
/// Does nothing if <see cref="policyName"/> is null or empty.
/// Does nothing if <paramref name="policyName"/> is null or empty.
/// </summary>
/// <param name="policyName">A policy name to check</param>
protected virtual async Task CheckPolicyAsync([CanBeNull] string policyName)

3
framework/src/Volo.Abp.BlobStoring/Volo/Abp/BlobStoring/BlobContainerFactoryExtensions.cs

@ -6,7 +6,6 @@
/// Gets a named container.
/// </summary>
/// <param name="blobContainerFactory">The blob container manager</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>
/// The container object.
/// </returns>
@ -19,4 +18,4 @@
);
}
}
}
}

25
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs

@ -150,6 +150,13 @@ namespace Volo.Abp.Cli.Commands
Logger.LogInformation("Output folder: " + outputFolder);
if (connectionString == null &&
databaseManagementSystem != DatabaseManagementSystem.NotSpecified &&
databaseManagementSystem != DatabaseManagementSystem.SQLServer)
{
connectionString = GetNewConnectionStringByDbms(databaseManagementSystem, outputFolder);
}
commandLineArgs.Options.Add(CliConsts.Command, commandLineArgs.Command);
var result = await TemplateProjectBuilder.BuildAsync(
@ -219,6 +226,24 @@ namespace Volo.Abp.Cli.Commands
}
}
private string GetNewConnectionStringByDbms(DatabaseManagementSystem databaseManagementSystem, string outputFolder)
{
switch (databaseManagementSystem)
{
case DatabaseManagementSystem.MySQL:
return "Server=localhost;Port=3306;Database=MyProjectName;Uid=root;Pwd=myPassword;";
case DatabaseManagementSystem.PostgreSQL:
return "User ID=root;Password=myPassword;Host=localhost;Port=5432;Database=MyProjectName;Pooling=true;Min Pool Size=0;Max Pool Size=100;Connection Lifetime=0;";
//case DatabaseManagementSystem.Oracle:
case DatabaseManagementSystem.OracleDevart:
return "Data Source=MyProjectName;Integrated Security=yes;";
case DatabaseManagementSystem.SQLite:
return $"Data Source={Path.Combine(outputFolder,"database\\MyProjectName.db")};Version=3;";
default:
return null;
}
}
private void DeleteMigrationsIfNeeded(DatabaseProvider databaseProvider, DatabaseManagementSystem databaseManagementSystem, string outputFolder)
{
if (databaseManagementSystem == DatabaseManagementSystem.NotSpecified || databaseManagementSystem == DatabaseManagementSystem.SQLServer)

11
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/TemplateProjectBuildPipelineBuilder.cs

@ -17,6 +17,12 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building
pipeline.Steps.Add(new ProjectReferenceReplaceStep());
pipeline.Steps.Add(new TemplateCodeDeleteStep());
if (context.BuildArgs.ConnectionString != null)
{
pipeline.Steps.Add(new ConnectionStringChangeStep());
}
pipeline.Steps.Add(new SolutionRenameStep());
if (context.Template.Name == AppProTemplate.TemplateName ||
@ -37,11 +43,6 @@ namespace Volo.Abp.Cli.ProjectBuilding.Building
pipeline.Steps.Add(new RemoveRootFolderStep());
}
if (context.BuildArgs.ConnectionString != null)
{
pipeline.Steps.Add(new ConnectionStringChangeStep());
}
pipeline.Steps.Add(new CreateProjectResultZipStep());
return pipeline;

13
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs

@ -205,14 +205,25 @@ namespace Volo.Abp.Cli.ProjectModification
string postFix)
{
var srcPath = Path.Combine(Path.GetDirectoryName(moduleSolutionFile), targetFolder);
if (!Directory.Exists(srcPath))
{
return;
}
var projectFolderPath = Directory.GetDirectories(srcPath).FirstOrDefault(d=> d.EndsWith(postFix));
if (projectFolderPath == null)
{
return;
}
await SolutionFileModifier.RemoveProjectFromSolutionFileAsync(moduleSolutionFile, new DirectoryInfo(projectFolderPath).Name);
if (Directory.Exists(projectFolderPath))
{
Directory.Delete(projectFolderPath, true);
}
}
private async Task ChangeDomainTestReferenceToMongoDB(ModuleWithMastersInfo module, string moduleSolutionFile)

9
framework/src/Volo.Abp.Cli/Properties/launchSettings.json

@ -1,9 +0,0 @@
{
"profiles": {
"Volo.Abp.Cli": {
"commandName": "Project",
"commandLineArgs": "bundle --skip-version-check",
"workingDirectory": "C:\\Github\\volo\\abp\\templates\\app-pro\\aspnet-core\\src\\MyCompanyName.MyProjectName.Blazor"
}
}
}

2
framework/src/Volo.Abp.Core/System/AbpStringExtensions.cs

@ -88,7 +88,7 @@ namespace System
/// Gets index of nth occurrence of a char in a string.
/// </summary>
/// <param name="str">source string to be searched</param>
/// <param name="c">Char to search in <see cref="str"/></param>
/// <param name="c">Char to search in <paramref name="str"/></param>
/// <param name="n">Count of the occurrence</param>
public static int NthIndexOf(this string str, char c, int n)
{

2
framework/src/Volo.Abp.Core/System/Collections/Generic/AbpCollectionExtensions.cs

@ -107,7 +107,7 @@ namespace System.Collections.Generic
}
/// <summary>
/// Removes all items from the collection those satisfy the given <paramref name="predicate"/>.
/// Removes all items from the collection.
/// </summary>
/// <typeparam name="T">Type of the items in the collection</typeparam>
/// <param name="source">The collection</param>

4
framework/src/Volo.Abp.Core/Volo/Abp/Reflection/TypeFinder.cs

@ -37,7 +37,7 @@ namespace Volo.Abp.Reflection
allTypes.AddRange(typesInThisAssembly.Where(type => type != null));
}
catch (Exception ex)
catch
{
//TODO: Trigger a global event?
}
@ -46,4 +46,4 @@ namespace Volo.Abp.Reflection
return allTypes;
}
}
}
}

6
framework/src/Volo.Abp.Core/Volo/Abp/Text/Formatting/FormattedStringValueExtracter.cs

@ -11,7 +11,7 @@ namespace Volo.Abp.Text.Formatting
/// </summary>
/// <example>
/// Say that str is "My name is Neo." and format is "My name is {name}.".
/// Then Extract method gets "Neo" as "name".
/// Then Extract method gets "Neo" as "name".
/// </example>
public class FormattedStringValueExtracter
{
@ -84,7 +84,7 @@ namespace Volo.Abp.Text.Formatting
}
/// <summary>
/// Checks if given <see cref="str"/> fits to given <see cref="format"/>.
/// Checks if given <paramref name="str"/> fits to given <paramref name="format"/>.
/// Also gets extracted values.
/// </summary>
/// <param name="str">String including dynamic values</param>
@ -127,4 +127,4 @@ namespace Volo.Abp.Text.Formatting
}
}
}
}
}

12
framework/src/Volo.Abp.Dapper/Volo/Abp/Domain/Repositories/Dapper/DapperRepository.cs

@ -1,4 +1,6 @@
using System.Data;
using System;
using System.Data;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Volo.Abp.EntityFrameworkCore;
@ -16,8 +18,14 @@ namespace Volo.Abp.Domain.Repositories.Dapper
_dbContextProvider = dbContextProvider;
}
[Obsolete("Use GetDbConnectionAsync method.")]
public IDbConnection DbConnection => _dbContextProvider.GetDbContext().Database.GetDbConnection();
public async Task<IDbConnection> GetDbConnectionAsync() => (await _dbContextProvider.GetDbContextAsync()).Database.GetDbConnection();
[Obsolete("Use GetDbTransactionAsync method.")]
public IDbTransaction DbTransaction => _dbContextProvider.GetDbContext().Database.CurrentTransaction?.GetDbTransaction();
public async Task<IDbTransaction> GetDbTransactionAsync() => (await _dbContextProvider.GetDbContextAsync()).Database.CurrentTransaction?.GetDbTransaction();
}
}
}

12
framework/src/Volo.Abp.Dapper/Volo/Abp/Domain/Repositories/Dapper/IDapperRepository.cs

@ -1,11 +1,19 @@
using System.Data;
using System;
using System.Data;
using System.Threading.Tasks;
namespace Volo.Abp.Domain.Repositories.Dapper
{
public interface IDapperRepository
{
[Obsolete("Use GetDbConnectionAsync method.")]
IDbConnection DbConnection { get; }
Task<IDbConnection> GetDbConnectionAsync();
[Obsolete("Use GetDbTransactionAsync method.")]
IDbTransaction DbTransaction { get; }
Task<IDbTransaction> GetDbTransactionAsync();
}
}
}

6
framework/src/Volo.Abp.Data/Volo/Abp/Data/DataSeedContext.cs

@ -13,8 +13,8 @@ namespace Volo.Abp.Data
/// </summary>
/// <param name="name">Name of the property</param>
/// <returns>
/// Returns the value in the <see cref="Properties"/> dictionary by given <see cref="name"/>.
/// Returns null if given <see cref="name"/> is not present in the <see cref="Properties"/> dictionary.
/// Returns the value in the <see cref="Properties"/> dictionary by given <paramref name="name"/>.
/// Returns null if given <paramref name="name"/> is not present in the <see cref="Properties"/> dictionary.
/// </returns>
[CanBeNull]
public object this[string name]
@ -45,4 +45,4 @@ namespace Volo.Abp.Data
return this;
}
}
}
}

16
framework/src/Volo.Abp.Data/Volo/Abp/Data/DefaultConnectionStringResolver.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Volo.Abp.DependencyInjection;
@ -14,7 +15,18 @@ namespace Volo.Abp.Data
Options = options.Value;
}
[Obsolete("Use ResolveAsync method.")]
public virtual string Resolve(string connectionStringName = null)
{
return ResolveInternal(connectionStringName);
}
public virtual Task<string> ResolveAsync(string connectionStringName = null)
{
return Task.FromResult(ResolveInternal(connectionStringName));
}
private string ResolveInternal(string connectionStringName)
{
//Get module specific value if provided
if (!connectionStringName.IsNullOrEmpty())
@ -25,9 +37,9 @@ namespace Volo.Abp.Data
return moduleConnString;
}
}
//Get default value
return Options.ConnectionStrings.Default;
}
}
}
}

8
framework/src/Volo.Abp.Data/Volo/Abp/Data/IConnectionStringResolver.cs

@ -1,10 +1,16 @@
using JetBrains.Annotations;
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Volo.Abp.Data
{
public interface IConnectionStringResolver
{
[NotNull]
[Obsolete("Use ResolveAsync method.")]
string Resolve(string connectionStringName = null);
[NotNull]
Task<string> ResolveAsync(string connectionStringName = null);
}
}

14
framework/src/Volo.Abp.Data/Volo/Abp/Data/IConnectionStringResolverExtensions.cs

@ -1,10 +1,22 @@
namespace Volo.Abp.Data
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Volo.Abp.Data
{
public static class ConnectionStringResolverExtensions
{
[NotNull]
[Obsolete("Use ResolveAsync method")]
public static string Resolve<T>(this IConnectionStringResolver resolver)
{
return resolver.Resolve(ConnectionStringNameAttribute.GetConnStringName<T>());
}
[NotNull]
public static Task<string> ResolveAsync<T>(this IConnectionStringResolver resolver)
{
return resolver.ResolveAsync(ConnectionStringNameAttribute.GetConnStringName<T>());
}
}
}

10
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyCrudAppService.cs

@ -132,7 +132,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Maps <see cref="TCreateInput"/> to <see cref="TEntity"/> to create a new entity.
/// Maps <typeparamref name="TCreateInput"/> to <typeparamref name="TEntity"/> to create a new entity.
/// It uses <see cref="MapToEntity(TCreateInput)"/> by default.
/// It can be overriden for custom mapping.
/// Overriding this has higher priority than overriding the <see cref="MapToEntity(TCreateInput)"/>
@ -143,7 +143,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Maps <see cref="TCreateInput"/> to <see cref="TEntity"/> to create a new entity.
/// Maps <typeparamref name="TCreateInput"/> to <typeparamref name="TEntity"/> to create a new entity.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
@ -155,7 +155,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Sets Id value for the entity if <see cref="TKey"/> is <see cref="Guid"/>.
/// Sets Id value for the entity if <typeparamref name="TKey"/> is <see cref="Guid"/>.
/// It's used while creating a new entity.
/// </summary>
protected virtual void SetIdForGuids(TEntity entity)
@ -171,7 +171,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Maps <see cref="TUpdateInput"/> to <see cref="TEntity"/> to update the entity.
/// Maps <typeparamref name="TUpdateInput"/> to <typeparamref name="TEntity"/> to update the entity.
/// It uses <see cref="MapToEntity(TUpdateInput, TEntity)"/> by default.
/// It can be overriden for custom mapping.
/// Overriding this has higher priority than overriding the <see cref="MapToEntity(TUpdateInput, TEntity)"/>
@ -183,7 +183,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Maps <see cref="TUpdateInput"/> to <see cref="TEntity"/> to update the entity.
/// Maps <typeparamref name="TUpdateInput"/> to <typeparamref name="TEntity"/> to update the entity.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>

36
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/AbstractKeyReadOnlyAppService.cs

@ -62,7 +62,7 @@ namespace Volo.Abp.Application.Services
{
await CheckGetListPolicyAsync();
var query = CreateFilteredQuery(input);
var query = await CreateFilteredQueryAsync(input);
var totalCount = await AsyncExecuter.CountAsync(query);
@ -160,13 +160,37 @@ namespace Volo.Abp.Application.Services
/// methods.
/// </summary>
/// <param name="input">The input.</param>
[Obsolete("Override the CreateFilteredQueryAsync method instead.")]
protected virtual IQueryable<TEntity> CreateFilteredQuery(TGetListInput input)
{
return ReadOnlyRepository;
}
/// <summary>
/// Maps <see cref="TEntity"/> to <see cref="TGetOutputDto"/>.
/// This method should create <see cref="IQueryable{TEntity}"/> based on given input.
/// It should filter query if needed, but should not do sorting or paging.
/// Sorting should be done in <see cref="ApplySorting"/> and paging should be done in <see cref="ApplyPaging"/>
/// methods.
/// </summary>
/// <param name="input">The input.</param>
protected virtual async Task<IQueryable<TEntity>> CreateFilteredQueryAsync(TGetListInput input)
{
/* If user has overridden the CreateFilteredQuery method,
* we don't want to make breaking change in this point.
*/
#pragma warning disable 618
var query = CreateFilteredQuery(input);
#pragma warning restore 618
if (!ReferenceEquals(query, ReadOnlyRepository))
{
return query;
}
return await ReadOnlyRepository.GetQueryableAsync();
}
/// <summary>
/// Maps <typeparamref name="TEntity"/> to <typeparamref name="TGetOutputDto"/>.
/// It internally calls the <see cref="MapToGetOutputDto"/> by default.
/// It can be overriden for custom mapping.
/// Overriding this has higher priority than overriding the <see cref="MapToGetOutputDto"/>
@ -177,7 +201,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Maps <see cref="TEntity"/> to <see cref="TGetOutputDto"/>.
/// Maps <typeparamref name="TEntity"/> to <typeparamref name="TGetOutputDto"/>.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>
@ -187,7 +211,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Maps a list of <see cref="TEntity"/> to <see cref="TGetListOutputDto"/> objects.
/// Maps a list of <typeparamref name="TEntity"/> to <typeparamref name="TGetListOutputDto"/> objects.
/// It uses <see cref="MapToGetListOutputDtoAsync"/> method for each item in the list.
/// </summary>
protected virtual async Task<List<TGetListOutputDto>> MapToGetListOutputDtosAsync(List<TEntity> entities)
@ -203,7 +227,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Maps <see cref="TEntity"/> to <see cref="TGetListOutputDto"/>.
/// Maps <typeparamref name="TEntity"/> to <typeparamref name="TGetListOutputDto"/>.
/// It internally calls the <see cref="MapToGetListOutputDto"/> by default.
/// It can be overriden for custom mapping.
/// Overriding this has higher priority than overriding the <see cref="MapToGetListOutputDto"/>
@ -214,7 +238,7 @@ namespace Volo.Abp.Application.Services
}
/// <summary>
/// Maps <see cref="TEntity"/> to <see cref="TGetListOutputDto"/>.
/// Maps <typeparamref name="TEntity"/> to <typeparamref name="TGetListOutputDto"/>.
/// It uses <see cref="IObjectMapper"/> by default.
/// It can be overriden for custom mapping.
/// </summary>

4
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/CrudAppService.cs

@ -80,12 +80,12 @@ namespace Volo.Abp.Application.Services
Repository = repository;
}
protected async override Task DeleteByIdAsync(TKey id)
protected override async Task DeleteByIdAsync(TKey id)
{
await Repository.DeleteAsync(id);
}
protected async override Task<TEntity> GetEntityByIdAsync(TKey id)
protected override async Task<TEntity> GetEntityByIdAsync(TKey id)
{
return await Repository.GetAsync(id);
}

4
framework/src/Volo.Abp.Ddd.Application/Volo/Abp/Application/Services/ReadOnlyAppService.cs

@ -38,7 +38,7 @@ namespace Volo.Abp.Application.Services
where TGetOutputDto : IEntityDto<TKey>
where TGetListOutputDto : IEntityDto<TKey>
{
protected new IReadOnlyRepository<TEntity, TKey> Repository { get; }
protected IReadOnlyRepository<TEntity, TKey> Repository { get; }
protected ReadOnlyAppService(IReadOnlyRepository<TEntity, TKey> repository)
: base(repository)
@ -46,7 +46,7 @@ namespace Volo.Abp.Application.Services
Repository = repository;
}
protected async override Task<TEntity> GetEntityByIdAsync(TKey id)
protected override async Task<TEntity> GetEntityByIdAsync(TKey id)
{
return await Repository.GetAsync(id);
}

6
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/DependencyInjection/IAbpCommonDbContextRegistrationOptionsBuilder.cs

@ -9,11 +9,11 @@ namespace Volo.Abp.DependencyInjection
IServiceCollection Services { get; }
/// <summary>
/// Registers default repositories for this DbContext.
/// Registers default repositories for this DbContext.
/// </summary>
/// <param name="includeAllEntities">
/// Registers repositories only for aggregate root entities by default.
/// set <see cref="includeAllEntities"/> to true to include all entities.
/// set <paramref name="includeAllEntities"/> to true to include all entities.
/// </param>
IAbpCommonDbContextRegistrationOptionsBuilder AddDefaultRepositories(bool includeAllEntities = false);
@ -67,4 +67,4 @@ namespace Volo.Abp.DependencyInjection
/// <param name="otherDbContextType">The DbContext type to be replaced</param>
IAbpCommonDbContextRegistrationOptionsBuilder ReplaceDbContext(Type otherDbContextType);
}
}
}

9
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/IReadOnlyRepository.cs

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Linq;
@ -11,9 +12,17 @@ namespace Volo.Abp.Domain.Repositories
{
IAsyncQueryableExecuter AsyncExecuter { get; }
[Obsolete("Use WithDetailsAsync method.")]
IQueryable<TEntity> WithDetails();
[Obsolete("Use WithDetailsAsync method.")]
IQueryable<TEntity> WithDetails(params Expression<Func<TEntity, object>>[] propertySelectors);
Task<IQueryable<TEntity>> WithDetailsAsync(); //TODO: CancellationToken
Task<IQueryable<TEntity>> WithDetailsAsync(params Expression<Func<TEntity, object>>[] propertySelectors); //TODO: CancellationToken
Task<IQueryable<TEntity>> GetQueryableAsync(); //TODO: CancellationToken
}
public interface IReadOnlyRepository<TEntity, TKey> : IReadOnlyRepository<TEntity>, IReadOnlyBasicRepository<TEntity, TKey>

5
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryAsyncExtensions.cs

@ -12,13 +12,14 @@ namespace Volo.Abp.Domain.Repositories
{
#region Contains
public static Task<bool> ContainsAsync<T>(
public static async Task<bool> ContainsAsync<T>(
[NotNull] this IReadOnlyRepository<T> repository,
[NotNull] T item,
CancellationToken cancellationToken = default)
where T : class, IEntity
{
return repository.AsyncExecuter.ContainsAsync(repository, item, cancellationToken);
var queryable = await repository.GetQueryableAsync();
return await repository.AsyncExecuter.ContainsAsync(queryable, item, cancellationToken);
}
#endregion

22
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Repositories/RepositoryBase.cs

@ -17,34 +17,54 @@ namespace Volo.Abp.Domain.Repositories
public abstract class RepositoryBase<TEntity> : BasicRepositoryBase<TEntity>, IRepository<TEntity>, IUnitOfWorkManagerAccessor
where TEntity : class, IEntity
{
[Obsolete("This method will be removed in future versions.")]
public virtual Type ElementType => GetQueryable().ElementType;
[Obsolete("This method will be removed in future versions.")]
public virtual Expression Expression => GetQueryable().Expression;
[Obsolete("This method will be removed in future versions.")]
public virtual IQueryProvider Provider => GetQueryable().Provider;
[Obsolete("Use WithDetailsAsync method.")]
public virtual IQueryable<TEntity> WithDetails()
{
return GetQueryable();
}
[Obsolete("Use WithDetailsAsync method.")]
public virtual IQueryable<TEntity> WithDetails(params Expression<Func<TEntity, object>>[] propertySelectors)
{
return GetQueryable();
}
public virtual Task<IQueryable<TEntity>> WithDetailsAsync()
{
return GetQueryableAsync();
}
public virtual Task<IQueryable<TEntity>> WithDetailsAsync(params Expression<Func<TEntity, object>>[] propertySelectors)
{
return GetQueryableAsync();
}
[Obsolete("This method will be removed in future versions.")]
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
[Obsolete("This method will be removed in future versions.")]
public IEnumerator<TEntity> GetEnumerator()
{
return GetQueryable().GetEnumerator();
}
[Obsolete("Use GetQueryableAsync method.")]
protected abstract IQueryable<TEntity> GetQueryable();
public abstract Task<IQueryable<TEntity>> GetQueryableAsync();
public abstract Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
@ -103,8 +123,6 @@ namespace Volo.Abp.Domain.Repositories
await DeleteAsync(entity, autoSave, cancellationToken);
}
public async Task DeleteManyAsync([NotNull] IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
{
foreach (var id in ids)

15
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EfCoreRepositoryExtensions.cs

@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
@ -7,18 +8,32 @@ namespace Volo.Abp.Domain.Repositories
{
public static class EfCoreRepositoryExtensions
{
[Obsolete("Use GetDbContextAsync method.")]
public static DbContext GetDbContext<TEntity>(this IReadOnlyBasicRepository<TEntity> repository)
where TEntity : class, IEntity
{
return repository.ToEfCoreRepository().DbContext;
}
public static Task<DbContext> GetDbContextAsync<TEntity>(this IReadOnlyBasicRepository<TEntity> repository)
where TEntity : class, IEntity
{
return repository.ToEfCoreRepository().GetDbContextAsync();
}
[Obsolete("Use GetDbSetAsync method.")]
public static DbSet<TEntity> GetDbSet<TEntity>(this IReadOnlyBasicRepository<TEntity> repository)
where TEntity : class, IEntity
{
return repository.ToEfCoreRepository().DbSet;
}
public static Task<DbSet<TEntity>> GetDbSetAsync<TEntity>(this IReadOnlyBasicRepository<TEntity> repository)
where TEntity : class, IEntity
{
return repository.ToEfCoreRepository().GetDbSetAsync();
}
public static IEfCoreRepository<TEntity> ToEfCoreRepository<TEntity>(this IReadOnlyBasicRepository<TEntity> repository)
where TEntity : class, IEntity
{

178
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/EfCoreRepository.cs

@ -1,8 +1,6 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Nito.AsyncEx;
using System;
using System.Collections.Generic;
using System.Linq;
@ -21,18 +19,41 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
where TDbContext : IEfCoreDbContext
where TEntity : class, IEntity
{
public virtual DbSet<TEntity> DbSet => DbContext.Set<TEntity>();
[Obsolete("Use GetDbContextAsync() method.")]
protected virtual TDbContext DbContext => _dbContextProvider.GetDbContext();
[Obsolete("Use GetDbContextAsync() method.")]
DbContext IEfCoreRepository<TEntity>.DbContext => DbContext.As<DbContext>();
protected virtual TDbContext DbContext => _dbContextProvider.GetDbContext();
async Task<DbContext> IEfCoreRepository<TEntity>.GetDbContextAsync()
{
return await GetDbContextAsync() as DbContext;
}
protected virtual Task<TDbContext> GetDbContextAsync()
{
return _dbContextProvider.GetDbContextAsync();
}
[Obsolete("Use GetDbSetAsync() method.")]
public virtual DbSet<TEntity> DbSet => DbContext.Set<TEntity>();
Task<DbSet<TEntity>> IEfCoreRepository<TEntity>.GetDbSetAsync()
{
return GetDbSetAsync();
}
protected async Task<DbSet<TEntity>> GetDbSetAsync()
{
return (await GetDbContextAsync()).Set<TEntity>();
}
protected virtual AbpEntityOptions<TEntity> AbpEntityOptions => _entityOptionsLazy.Value;
private readonly IDbContextProvider<TDbContext> _dbContextProvider;
private readonly Lazy<AbpEntityOptions<TEntity>> _entityOptionsLazy;
public virtual IGuidGenerator GuidGenerator { get; set; }
public IGuidGenerator GuidGenerator { get; set; }
public IEfCoreBulkOperationProvider BulkOperationProvider { get; set; }
@ -49,15 +70,17 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
);
}
public async override Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public override async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
CheckAndSetId(entity);
var savedEntity = DbSet.Add(entity).Entity;
var dbContext = await GetDbContextAsync();
var savedEntity = (await dbContext.Set<TEntity>().AddAsync(entity, GetCancellationToken(cancellationToken))).Entity;
if (autoSave)
{
await DbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
await dbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
return savedEntity;
@ -65,7 +88,11 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
public override async Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
foreach (var entity in entities)
var entityArray = entities.ToArray();
var dbContext = await GetDbContextAsync();
cancellationToken = GetCancellationToken(cancellationToken);
foreach (var entity in entityArray)
{
CheckAndSetId(entity);
}
@ -74,30 +101,32 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
{
await BulkOperationProvider.InsertManyAsync<TDbContext, TEntity>(
this,
entities,
entityArray,
autoSave,
cancellationToken
);
return;
}
await DbSet.AddRangeAsync(entities);
await dbContext.Set<TEntity>().AddRangeAsync(entityArray, cancellationToken);
if (autoSave)
{
await DbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
}
public async override Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public override async Task<TEntity> UpdateAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
DbContext.Attach(entity);
var dbContext = await GetDbContextAsync();
dbContext.Attach(entity);
var updatedEntity = DbContext.Update(entity).Entity;
var updatedEntity = dbContext.Update(entity).Entity;
if (autoSave)
{
await DbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
await dbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
return updatedEntity;
@ -105,6 +134,8 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
public override async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
{
await BulkOperationProvider.UpdateManyAsync<TDbContext, TEntity>(
@ -117,65 +148,76 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
return;
}
DbSet.UpdateRange(entities);
var dbContext = await GetDbContextAsync();
dbContext.Set<TEntity>().UpdateRange(entities);
if (autoSave)
{
await DbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
}
public async override Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public override async Task DeleteAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
DbSet.Remove(entity);
var dbContext = await GetDbContextAsync();
dbContext.Set<TEntity>().Remove(entity);
if (autoSave)
{
await DbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
await dbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
}
public override async Task DeleteManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
cancellationToken = GetCancellationToken(cancellationToken);
if (BulkOperationProvider != null)
{
await BulkOperationProvider.DeleteManyAsync<TDbContext, TEntity>(
this,
entities,
autoSave,
cancellationToken);
cancellationToken
);
return;
}
DbSet.RemoveRange(entities);
var dbContext = await GetDbContextAsync();
dbContext.RemoveRange(entities);
if (autoSave)
{
await DbContext.SaveChangesAsync();
await dbContext.SaveChangesAsync(cancellationToken);
}
}
public async override Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
public override async Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
{
return includeDetails
? await WithDetails().ToListAsync(GetCancellationToken(cancellationToken))
: await DbSet.ToListAsync(GetCancellationToken(cancellationToken));
? await (await WithDetailsAsync()).ToListAsync(GetCancellationToken(cancellationToken))
: await (await GetDbSetAsync()).ToListAsync(GetCancellationToken(cancellationToken));
}
public async override Task<long> GetCountAsync(CancellationToken cancellationToken = default)
public override async Task<long> GetCountAsync(CancellationToken cancellationToken = default)
{
return await DbSet.LongCountAsync(GetCancellationToken(cancellationToken));
return await (await GetDbSetAsync()).LongCountAsync(GetCancellationToken(cancellationToken));
}
public async override Task<List<TEntity>> GetPagedListAsync(
public override async Task<List<TEntity>> GetPagedListAsync(
int skipCount,
int maxResultCount,
string sorting,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
var queryable = includeDetails ? WithDetails() : DbSet;
var queryable = includeDetails
? await WithDetailsAsync()
: await GetDbSetAsync();
return await queryable
.OrderBy(sorting)
@ -183,44 +225,53 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
.ToListAsync(GetCancellationToken(cancellationToken));
}
[Obsolete("Use GetQueryableAsync method.")]
protected override IQueryable<TEntity> GetQueryable()
{
return DbSet.AsQueryable();
}
protected override Task SaveChangesAsync(CancellationToken cancellationToken)
public override async Task<IQueryable<TEntity>> GetQueryableAsync()
{
return (await GetDbSetAsync()).AsQueryable();
}
protected override async Task SaveChangesAsync(CancellationToken cancellationToken)
{
return DbContext.SaveChangesAsync(cancellationToken);
await (await GetDbContextAsync()).SaveChangesAsync(cancellationToken);
}
public async override Task<TEntity> FindAsync(
public override async Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return includeDetails
? await WithDetails()
? await (await WithDetailsAsync())
.Where(predicate)
.SingleOrDefaultAsync(GetCancellationToken(cancellationToken))
: await DbSet
: await (await GetDbSetAsync())
.Where(predicate)
.SingleOrDefaultAsync(GetCancellationToken(cancellationToken));
}
public async override Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
public override async Task DeleteAsync(Expression<Func<TEntity, bool>> predicate, bool autoSave = false, CancellationToken cancellationToken = default)
{
var entities = await GetQueryable()
var dbContext = await GetDbContextAsync();
var dbSet = dbContext.Set<TEntity>();
var entities = await dbSet
.Where(predicate)
.ToListAsync(GetCancellationToken(cancellationToken));
foreach (var entity in entities)
{
DbSet.Remove(entity);
dbSet.Remove(entity);
}
if (autoSave)
{
await DbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
await dbContext.SaveChangesAsync(GetCancellationToken(cancellationToken));
}
}
@ -230,7 +281,7 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
CancellationToken cancellationToken = default)
where TProperty : class
{
await DbContext
await (await GetDbContextAsync())
.Entry(entity)
.Collection(propertyExpression)
.LoadAsync(GetCancellationToken(cancellationToken));
@ -242,12 +293,13 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
CancellationToken cancellationToken = default)
where TProperty : class
{
await DbContext
await (await GetDbContextAsync())
.Entry(entity)
.Reference(propertyExpression)
.LoadAsync(GetCancellationToken(cancellationToken));
}
[Obsolete("Use WithDetailsAsync")]
public override IQueryable<TEntity> WithDetails()
{
if (AbpEntityOptions.DefaultWithDetailsFunc == null)
@ -258,10 +310,37 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
return AbpEntityOptions.DefaultWithDetailsFunc(GetQueryable());
}
public override async Task<IQueryable<TEntity>> WithDetailsAsync()
{
if (AbpEntityOptions.DefaultWithDetailsFunc == null)
{
return await base.WithDetailsAsync();
}
return AbpEntityOptions.DefaultWithDetailsFunc(await GetQueryableAsync());
}
[Obsolete("Use WithDetailsAsync method.")]
public override IQueryable<TEntity> WithDetails(params Expression<Func<TEntity, object>>[] propertySelectors)
{
var query = GetQueryable();
return IncludeDetails(
GetQueryable(),
propertySelectors
);
}
public override async Task<IQueryable<TEntity>> WithDetailsAsync(params Expression<Func<TEntity, object>>[] propertySelectors)
{
return IncludeDetails(
await GetQueryableAsync(),
propertySelectors
);
}
private static IQueryable<TEntity> IncludeDetails(
IQueryable<TEntity> query,
Expression<Func<TEntity, object>>[] propertySelectors)
{
if (!propertySelectors.IsNullOrEmpty())
{
foreach (var propertySelector in propertySelectors)
@ -273,6 +352,7 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
return query;
}
[Obsolete("This method will be deleted in future versions.")]
public IAsyncEnumerator<TEntity> GetAsyncEnumerator(CancellationToken cancellationToken = default)
{
return DbSet.AsAsyncEnumerable().GetAsyncEnumerator(cancellationToken);
@ -329,8 +409,8 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
public virtual async Task<TEntity> FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return includeDetails
? await WithDetails().FirstOrDefaultAsync(e => e.Id.Equals(id), GetCancellationToken(cancellationToken))
: await DbSet.FindAsync(new object[] { id }, GetCancellationToken(cancellationToken));
? await (await WithDetailsAsync()).FirstOrDefaultAsync(e => e.Id.Equals(id), GetCancellationToken(cancellationToken))
: await (await GetDbSetAsync()).FindAsync(new object[] {id}, GetCancellationToken(cancellationToken));
}
public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)
@ -344,9 +424,11 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
await DeleteAsync(entity, autoSave, cancellationToken);
}
public async virtual Task DeleteManyAsync([NotNull] IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
public virtual async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
{
var entities = await DbSet.Where(x => ids.Contains(x.Id)).ToListAsync();
cancellationToken = GetCancellationToken(cancellationToken);
var entities = await (await GetDbSetAsync()).Where(x => ids.Contains(x.Id)).ToListAsync(cancellationToken);
await DeleteManyAsync(entities, autoSave, cancellationToken);
}

10
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Domain/Repositories/EntityFrameworkCore/IEfCoreRepository.cs

@ -1,3 +1,5 @@
using System;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Entities;
@ -6,9 +8,15 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
public interface IEfCoreRepository<TEntity> : IRepository<TEntity>
where TEntity : class, IEntity
{
[Obsolete("Use GetDbContextAsync() method.")]
DbContext DbContext { get; }
[Obsolete("Use GetDbSetAsync() method.")]
DbSet<TEntity> DbSet { get; }
Task<DbContext> GetDbContextAsync();
Task<DbSet<TEntity>> GetDbSetAsync();
}
public interface IEfCoreRepository<TEntity, TKey> : IEfCoreRepository<TEntity>, IRepository<TEntity, TKey>
@ -16,4 +24,4 @@ namespace Volo.Abp.Domain.Repositories.EntityFrameworkCore
{
}
}
}

4
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/DependencyInjection/DbContextOptionsFactory.cs

@ -86,7 +86,11 @@ namespace Volo.Abp.EntityFrameworkCore.DependencyInjection
}
var connectionStringName = ConnectionStringNameAttribute.GetConnStringName<TDbContext>();
//Use DefaultConnectionStringResolver.Resolve when we remove IConnectionStringResolver.Resolve
#pragma warning disable 618
var connectionString = serviceProvider.GetRequiredService<IConnectionStringResolver>().Resolve(connectionStringName);
#pragma warning restore 618
return new DbContextCreationContext(
connectionStringName,

2
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/EfCoreAsyncQueryableProvider.cs

@ -11,7 +11,7 @@ using Volo.Abp.Linq;
namespace Volo.Abp.EntityFrameworkCore
{
public class EfCoreAsyncQueryableProvider : IAsyncQueryableProvider, ITransientDependency
public class EfCoreAsyncQueryableProvider : IAsyncQueryableProvider, ISingletonDependency
{
public bool CanExecute<T>(IQueryable<T> queryable)
{

10
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/IDbContextProvider.cs

@ -1,8 +1,14 @@
using System;
using System.Threading.Tasks;
namespace Volo.Abp.EntityFrameworkCore
{
public interface IDbContextProvider<out TDbContext>
public interface IDbContextProvider<TDbContext>
where TDbContext : IEfCoreDbContext
{
[Obsolete("Use GetDbContextAsync method.")]
TDbContext GetDbContext();
Task<TDbContext> GetDbContextAsync();
}
}
}

2
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/ObjectExtending/EfCoreObjectExtensionManagerExtensions.cs

@ -143,7 +143,9 @@ namespace Volo.Abp.ObjectExtending
var propertyBuilder = typeBuilder.Property(property.Type, property.Name);
efCoreMapping.EntityTypeAndPropertyBuildAction?.Invoke(typeBuilder, propertyBuilder);
#pragma warning disable 618
efCoreMapping.PropertyBuildAction?.Invoke(propertyBuilder);
#pragma warning restore 618
}
}
}

132
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/Uow/EntityFrameworkCore/UnitOfWorkDbContextProvider.cs

@ -1,11 +1,15 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Data;
using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.DependencyInjection;
using Volo.Abp.Threading;
namespace Volo.Abp.Uow.EntityFrameworkCore
{
@ -14,19 +18,34 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
public class UnitOfWorkDbContextProvider<TDbContext> : IDbContextProvider<TDbContext>
where TDbContext : IEfCoreDbContext
{
public ILogger<UnitOfWorkDbContextProvider<TDbContext>> Logger { get; set; }
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConnectionStringResolver _connectionStringResolver;
private readonly ICancellationTokenProvider _cancellationTokenProvider;
public UnitOfWorkDbContextProvider(
IUnitOfWorkManager unitOfWorkManager,
IConnectionStringResolver connectionStringResolver)
IConnectionStringResolver connectionStringResolver,
ICancellationTokenProvider cancellationTokenProvider)
{
_unitOfWorkManager = unitOfWorkManager;
_connectionStringResolver = connectionStringResolver;
_cancellationTokenProvider = cancellationTokenProvider;
Logger = NullLogger<UnitOfWorkDbContextProvider<TDbContext>>.Instance;
}
[Obsolete("Use GetDbContextAsync method.")]
public TDbContext GetDbContext()
{
Logger.LogWarning(
"UnitOfWorkDbContextProvider.GetDbContext is deprecated. Use GetDbContextAsync instead! " +
"You are probably using LINQ (LINQ extensions) directly on a repository. In this case, use repository.GetQueryableAsync() method " +
"to obtain an IQueryable<T> instance and use LINQ (LINQ extensions) on this object. "
);
Logger.LogWarning(Environment.StackTrace.Truncate(2048));
var unitOfWork = _unitOfWorkManager.Current;
if (unitOfWork == null)
{
@ -47,6 +66,33 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
return ((EfCoreDatabaseApi<TDbContext>)databaseApi).DbContext;
}
public async Task<TDbContext> GetDbContextAsync()
{
var unitOfWork = _unitOfWorkManager.Current;
if (unitOfWork == null)
{
throw new AbpException("A DbContext can only be created inside a unit of work!");
}
var connectionStringName = ConnectionStringNameAttribute.GetConnStringName<TDbContext>();
var connectionString = await _connectionStringResolver.ResolveAsync(connectionStringName);
var dbContextKey = $"{typeof(TDbContext).FullName}_{connectionString}";
var databaseApi = unitOfWork.FindDatabaseApi(dbContextKey);
if (databaseApi == null)
{
databaseApi = new EfCoreDatabaseApi<TDbContext>(
await CreateDbContextAsync(unitOfWork, connectionStringName, connectionString)
);
unitOfWork.AddDatabaseApi(dbContextKey, databaseApi);
}
return ((EfCoreDatabaseApi<TDbContext>)databaseApi).DbContext;
}
private TDbContext CreateDbContext(IUnitOfWork unitOfWork, string connectionStringName, string connectionString)
{
var creationContext = new DbContextCreationContext(connectionStringName, connectionString);
@ -67,6 +113,26 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
}
}
private async Task<TDbContext> CreateDbContextAsync(IUnitOfWork unitOfWork, string connectionStringName, string connectionString)
{
var creationContext = new DbContextCreationContext(connectionStringName, connectionString);
using (DbContextCreationContext.Use(creationContext))
{
var dbContext = await CreateDbContextAsync(unitOfWork);
if (dbContext is IAbpEfCoreDbContext abpEfCoreDbContext)
{
abpEfCoreDbContext.Initialize(
new AbpEfCoreDbContextInitializationContext(
unitOfWork
)
);
}
return dbContext;
}
}
private TDbContext CreateDbContext(IUnitOfWork unitOfWork)
{
return unitOfWork.Options.IsTransactional
@ -74,7 +140,16 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
: unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
}
public TDbContext CreateDbContextWithTransaction(IUnitOfWork unitOfWork)
private async Task<TDbContext> CreateDbContextAsync(IUnitOfWork unitOfWork)
{
Logger.LogDebug($"Creating a new DbContext of type {typeof(TDbContext).FullName}");
return unitOfWork.Options.IsTransactional
? await CreateDbContextWithTransactionAsync(unitOfWork)
: unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
}
private TDbContext CreateDbContextWithTransaction(IUnitOfWork unitOfWork)
{
var transactionApiKey = $"EntityFrameworkCore_{DbContextCreationContext.Current.ConnectionString}";
var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as EfCoreTransactionApi;
@ -117,5 +192,54 @@ namespace Volo.Abp.Uow.EntityFrameworkCore
return dbContext;
}
}
private async Task<TDbContext> CreateDbContextWithTransactionAsync(IUnitOfWork unitOfWork)
{
var transactionApiKey = $"EntityFrameworkCore_{DbContextCreationContext.Current.ConnectionString}";
var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as EfCoreTransactionApi;
if (activeTransaction == null)
{
var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
var dbTransaction = unitOfWork.Options.IsolationLevel.HasValue
? await dbContext.Database.BeginTransactionAsync(unitOfWork.Options.IsolationLevel.Value, GetCancellationToken())
: await dbContext.Database.BeginTransactionAsync(GetCancellationToken());
unitOfWork.AddTransactionApi(
transactionApiKey,
new EfCoreTransactionApi(
dbTransaction,
dbContext
)
);
return dbContext;
}
else
{
DbContextCreationContext.Current.ExistingConnection = activeTransaction.DbContextTransaction.GetDbTransaction().Connection;
var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TDbContext>();
if (dbContext.As<DbContext>().HasRelationalTransactionManager())
{
await dbContext.Database.UseTransactionAsync(activeTransaction.DbContextTransaction.GetDbTransaction(), GetCancellationToken());
}
else
{
await dbContext.Database.BeginTransactionAsync(GetCancellationToken()); //TODO: Why not using the new created transaction?
}
activeTransaction.AttendedDbContexts.Add(dbContext);
return dbContext;
}
}
protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default)
{
return _cancellationTokenProvider.FallbackToProvider(preferredValue);
}
}
}
}

4
framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IEventBus.cs

@ -33,7 +33,7 @@ namespace Volo.Abp.EventBus
/// <summary>
/// Registers to an event.
/// A new instance of <see cref="THandler"/> object is created for every event occurrence.
/// A new instance of <typeparamref name="THandler"/> object is created for every event occurrence.
/// </summary>
/// <typeparam name="TEvent">Event type</typeparam>
/// <typeparam name="THandler">Type of the event handler</typeparam>
@ -116,4 +116,4 @@ namespace Volo.Abp.EventBus
/// <param name="eventType">Event type</param>
void UnsubscribeAll(Type eventType);
}
}
}

4
framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IEventDataMayHaveTenantId.cs

@ -16,8 +16,8 @@ namespace Volo.Abp.EventBus
{
/// <summary>
/// Returns true if this event data has a Tenant Id information.
/// If so, it should set the <see cref="tenantId"/> our parameter.
/// Otherwise, the <see cref="tenantId"/> our parameter value should not be informative
/// If so, it should set the <paramref name="tenantId"/> our parameter.
/// Otherwise, the <paramref name="tenantId"/> our parameter value should not be informative
/// (it will be null as expected, but doesn't indicate a tenant with null tenant id).
/// </summary>
/// <param name="tenantId">

4
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinition.cs

@ -69,8 +69,8 @@ namespace Volo.Abp.Features
/// </summary>
/// <param name="name">Name of the property</param>
/// <returns>
/// Returns the value in the <see cref="Properties"/> dictionary by given <see cref="name"/>.
/// Returns null if given <see cref="name"/> is not present in the <see cref="Properties"/> dictionary.
/// Returns the value in the <see cref="Properties"/> dictionary by given <paramref name="name"/>.
/// Returns null if given <paramref name="name"/> is not present in the <see cref="Properties"/> dictionary.
/// </returns>
[CanBeNull]
public object this[string name]

8
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureGroupDefinition.cs

@ -29,8 +29,8 @@ namespace Volo.Abp.Features
/// </summary>
/// <param name="name">Name of the property</param>
/// <returns>
/// Returns the value in the <see cref="Properties"/> dictionary by given <see cref="name"/>.
/// Returns null if given <see cref="name"/> is not present in the <see cref="Properties"/> dictionary.
/// Returns the value in the <see cref="Properties"/> dictionary by given <paramref name="name"/>.
/// Returns null if given <paramref name="name"/> is not present in the <see cref="Properties"/> dictionary.
/// </returns>
public object this[string name]
{
@ -39,7 +39,7 @@ namespace Volo.Abp.Features
}
protected internal FeatureGroupDefinition(
string name,
string name,
ILocalizableString displayName = null)
{
Name = name;
@ -108,4 +108,4 @@ namespace Volo.Abp.Features
return $"[{nameof(FeatureGroupDefinition)} {Name}]";
}
}
}
}

9
framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/IMemoryDbRepository.cs

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Entities;
namespace Volo.Abp.Domain.Repositories.MemoryDb
@ -6,9 +7,15 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
public interface IMemoryDbRepository<TEntity> : IRepository<TEntity>
where TEntity : class, IEntity
{
[Obsolete("Use GetDatabaseAsync() method.")]
IMemoryDatabase Database { get; }
[Obsolete("Use GetCollectionAsync() method.")]
IMemoryDatabaseCollection<TEntity> Collection { get; }
Task<IMemoryDatabase> GetDatabaseAsync();
Task<IMemoryDatabaseCollection<TEntity>> GetCollectionAsync();
}
public interface IMemoryDbRepository<TEntity, TKey> : IMemoryDbRepository<TEntity>, IRepository<TEntity, TKey>

77
framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs

@ -1,4 +1,3 @@
using JetBrains.Annotations;
using System;
using System.Collections.Generic;
using System.Linq;
@ -22,10 +21,22 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
{
//TODO: Add dbcontext just like mongodb implementation!
[Obsolete("Use GetCollectionAsync method.")]
public virtual IMemoryDatabaseCollection<TEntity> Collection => Database.Collection<TEntity>();
public async Task<IMemoryDatabaseCollection<TEntity>> GetCollectionAsync()
{
return (await GetDatabaseAsync()).Collection<TEntity>();
}
[Obsolete("Use GetDatabaseAsync method.")]
public virtual IMemoryDatabase Database => DatabaseProvider.GetDatabase();
public Task<IMemoryDatabase> GetDatabaseAsync()
{
return DatabaseProvider.GetDatabaseAsync();
}
protected IMemoryDatabaseProvider<TMemoryDbContext> DatabaseProvider { get; }
public ILocalEventBus LocalEventBus { get; set; }
@ -47,11 +58,17 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
EntityChangeEventHelper = NullEntityChangeEventHelper.Instance;
}
[Obsolete("This method will be removed in future versions.")]
protected override IQueryable<TEntity> GetQueryable()
{
return ApplyDataFilters(Collection.AsQueryable());
}
public override async Task<IQueryable<TEntity>> GetQueryableAsync()
{
return ApplyDataFilters((await GetCollectionAsync()).AsQueryable());
}
protected virtual async Task TriggerDomainEventsAsync(object entity)
{
var generatesDomainEventsEntity = entity as IGeneratesDomainEvents;
@ -163,39 +180,40 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
await TriggerDomainEventsAsync(entity);
}
public override Task<TEntity> FindAsync(
public override async Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return Task.FromResult(GetQueryable().Where(predicate).SingleOrDefault());
return (await GetQueryableAsync()).Where(predicate).SingleOrDefault();
}
public async override Task DeleteAsync(
public override async Task DeleteAsync(
Expression<Func<TEntity, bool>> predicate,
bool autoSave = false,
CancellationToken cancellationToken = default)
{
var entities = GetQueryable().Where(predicate).ToList();
var entities = (await GetQueryableAsync()).Where(predicate).ToList();
foreach (var entity in entities)
{
await DeleteAsync(entity, autoSave, cancellationToken);
}
}
public async override Task<TEntity> InsertAsync(
public override async Task<TEntity> InsertAsync(
TEntity entity,
bool autoSave = false,
CancellationToken cancellationToken = default)
{
await ApplyAbpConceptsForAddedEntityAsync(entity);
Collection.Add(entity);
(await GetCollectionAsync()).Add(entity);
return entity;
}
public async override Task<TEntity> UpdateAsync(
public override async Task<TEntity> UpdateAsync(
TEntity entity,
bool autoSave = false,
CancellationToken cancellationToken = default)
@ -214,12 +232,12 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
await TriggerDomainEventsAsync(entity);
Collection.Update(entity);
(await GetCollectionAsync()).Update(entity);
return entity;
}
public async override Task DeleteAsync(
public override async Task DeleteAsync(
TEntity entity,
bool autoSave = false,
CancellationToken cancellationToken = default)
@ -229,35 +247,35 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
if (entity is ISoftDelete softDeleteEntity && !IsHardDeleted(entity))
{
softDeleteEntity.IsDeleted = true;
Collection.Update(entity);
(await GetCollectionAsync()).Update(entity);
}
else
{
Collection.Remove(entity);
(await GetCollectionAsync()).Remove(entity);
}
}
public override Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
public override async Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
{
return Task.FromResult(GetQueryable().ToList());
return (await GetQueryableAsync()).ToList();
}
public override Task<long> GetCountAsync(CancellationToken cancellationToken = default)
public override async Task<long> GetCountAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(GetQueryable().LongCount());
return (await GetQueryableAsync()).LongCount();
}
public override Task<List<TEntity>> GetPagedListAsync(
public override async Task<List<TEntity>> GetPagedListAsync(
int skipCount,
int maxResultCount,
string sorting,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
return Task.FromResult(GetQueryable()
return (await GetQueryableAsync())
.OrderBy(sorting)
.PageBy(skipCount, maxResultCount)
.ToList());
.ToList();
}
}
@ -270,13 +288,13 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
{
}
public override Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
public override async Task<TEntity> InsertAsync(TEntity entity, bool autoSave = false, CancellationToken cancellationToken = default)
{
SetIdIfNeeded(entity);
return base.InsertAsync(entity, autoSave, cancellationToken);
await SetIdIfNeededAsync(entity);
return await base.InsertAsync(entity, autoSave, cancellationToken);
}
protected virtual void SetIdIfNeeded(TEntity entity)
protected virtual async Task SetIdIfNeededAsync(TEntity entity)
{
if (typeof(TKey) == typeof(int) ||
typeof(TKey) == typeof(long) ||
@ -284,7 +302,8 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
{
if (EntityHelper.HasDefaultId(entity))
{
EntityHelper.TrySetId(entity, () => Database.GenerateNextId<TEntity, TKey>());
var nextId = (await GetDatabaseAsync()).GenerateNextId<TEntity, TKey>();
EntityHelper.TrySetId(entity, () => nextId);
}
}
}
@ -301,9 +320,9 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
return entity;
}
public virtual Task<TEntity> FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
public virtual async Task<TEntity> FindAsync(TKey id, bool includeDetails = true, CancellationToken cancellationToken = default)
{
return Task.FromResult(GetQueryable().FirstOrDefault(e => e.Id.Equals(id)));
return (await GetQueryableAsync()).FirstOrDefault(e => e.Id.Equals(id));
}
public virtual async Task DeleteAsync(TKey id, bool autoSave = false, CancellationToken cancellationToken = default)
@ -311,10 +330,10 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
await DeleteAsync(x => x.Id.Equals(id), autoSave, cancellationToken);
}
public virtual async Task DeleteManyAsync([NotNull] IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
public virtual async Task DeleteManyAsync(IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
{
var entities = await AsyncExecuter.ToListAsync(GetQueryable().Where(x => ids.Contains(x.Id)));
DeleteManyAsync(entities, autoSave, cancellationToken);
var entities = await AsyncExecuter.ToListAsync((await GetQueryableAsync()).Where(x => ids.Contains(x.Id)), cancellationToken);
await DeleteManyAsync(entities, autoSave, cancellationToken);
}
}
}

17
framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDbCoreRepositoryExtensions.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Repositories.MemoryDb;
@ -7,18 +8,32 @@ namespace Volo.Abp.Domain.Repositories
{
public static class MemoryDbCoreRepositoryExtensions
{
[Obsolete("Use GetDatabaseAsync method.")]
public static IMemoryDatabase GetDatabase<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMemoryDbRepository().Database;
}
public static Task<IMemoryDatabase> GetDatabaseAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMemoryDbRepository().GetDatabaseAsync();
}
[Obsolete("Use GetCollectionAsync method.")]
public static IMemoryDatabaseCollection<TEntity> GetCollection<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMemoryDbRepository().Collection;
}
public static Task<IMemoryDatabaseCollection<TEntity>> GetCollectionAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMemoryDbRepository().GetCollectionAsync();
}
public static IMemoryDbRepository<TEntity, TKey> ToMemoryDbRepository<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
@ -31,4 +46,4 @@ namespace Volo.Abp.Domain.Repositories
return memoryDbRepository;
}
}
}
}

12
framework/src/Volo.Abp.MemoryDb/Volo/Abp/MemoryDb/IMemoryDatabaseProvider.cs

@ -1,12 +1,20 @@
using Volo.Abp.Domain.Repositories.MemoryDb;
using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories.MemoryDb;
namespace Volo.Abp.MemoryDb
{
public interface IMemoryDatabaseProvider<TMemoryDbContext>
where TMemoryDbContext : MemoryDbContext
{
[Obsolete("Use GetDbContextAsync method.")]
TMemoryDbContext DbContext { get; }
Task<TMemoryDbContext> GetDbContextAsync();
[Obsolete("Use GetDatabaseAsync method.")]
IMemoryDatabase GetDatabase();
Task<IMemoryDatabase> GetDatabaseAsync();
}
}
}

36
framework/src/Volo.Abp.MemoryDb/Volo/Abp/Uow/MemoryDb/UnitOfWorkMemoryDatabaseProvider.cs

@ -1,4 +1,6 @@
using Volo.Abp.Data;
using System;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.Domain.Repositories.MemoryDb;
using Volo.Abp.MemoryDb;
@ -8,7 +10,7 @@ namespace Volo.Abp.Uow.MemoryDb
where TMemoryDbContext : MemoryDbContext
{
public TMemoryDbContext DbContext { get; }
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConnectionStringResolver _connectionStringResolver;
private readonly MemoryDatabaseManager _memoryDatabaseManager;
@ -16,7 +18,7 @@ namespace Volo.Abp.Uow.MemoryDb
public UnitOfWorkMemoryDatabaseProvider(
IUnitOfWorkManager unitOfWorkManager,
IConnectionStringResolver connectionStringResolver,
TMemoryDbContext dbContext,
TMemoryDbContext dbContext,
MemoryDatabaseManager memoryDatabaseManager)
{
_unitOfWorkManager = unitOfWorkManager;
@ -25,6 +27,12 @@ namespace Volo.Abp.Uow.MemoryDb
_memoryDatabaseManager = memoryDatabaseManager;
}
public Task<TMemoryDbContext> GetDbContextAsync()
{
return Task.FromResult(DbContext);
}
[Obsolete("Use GetDatabaseAsync method.")]
public IMemoryDatabase GetDatabase()
{
var unitOfWork = _unitOfWorkManager.Current;
@ -44,5 +52,25 @@ namespace Volo.Abp.Uow.MemoryDb
return ((MemoryDbDatabaseApi)databaseApi).Database;
}
public async Task<IMemoryDatabase> GetDatabaseAsync()
{
var unitOfWork = _unitOfWorkManager.Current;
if (unitOfWork == null)
{
throw new AbpException($"A {nameof(IMemoryDatabase)} instance can only be created inside a unit of work!");
}
var connectionString = await _connectionStringResolver.ResolveAsync<TMemoryDbContext>();
var dbContextKey = $"{typeof(TMemoryDbContext).FullName}_{connectionString}";
var databaseApi = unitOfWork.GetOrAddDatabaseApi(
dbContextKey,
() => new MemoryDbDatabaseApi(
_memoryDatabaseManager.Get(connectionString)
));
return ((MemoryDbDatabaseApi)databaseApi).Database;
}
}
}
}

14
framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/IMongoDbRepository.cs

@ -1,4 +1,7 @@
using MongoDB.Driver;
using System;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Volo.Abp.Domain.Entities;
@ -7,11 +10,20 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
public interface IMongoDbRepository<TEntity> : IRepository<TEntity>
where TEntity : class, IEntity
{
[Obsolete("Use GetDatabaseAsync method.")]
IMongoDatabase Database { get; }
Task<IMongoDatabase> GetDatabaseAsync(CancellationToken cancellationToken = default);
[Obsolete("Use GetCollectionAsync method.")]
IMongoCollection<TEntity> Collection { get; }
Task<IMongoCollection<TEntity>> GetCollectionAsync(CancellationToken cancellationToken = default);
[Obsolete("Use GetMongoQueryableAsync method.")]
IMongoQueryable<TEntity> GetMongoQueryable();
Task<IMongoQueryable<TEntity>> GetMongoQueryableAsync(CancellationToken cancellationToken = default);
}
public interface IMongoDbRepository<TEntity, TKey> : IMongoDbRepository<TEntity>, IRepository<TEntity, TKey>

253
framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs

@ -27,13 +27,37 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
where TMongoDbContext : IAbpMongoDbContext
where TEntity : class, IEntity
{
[Obsolete("Use GetCollectionAsync method.")]
public virtual IMongoCollection<TEntity> Collection => DbContext.Collection<TEntity>();
public async Task<IMongoCollection<TEntity>> GetCollectionAsync(CancellationToken cancellationToken = default)
{
return (await GetDbContextAsync(GetCancellationToken(cancellationToken))).Collection<TEntity>();
}
[Obsolete("Use GetDatabaseAsync method.")]
public virtual IMongoDatabase Database => DbContext.Database;
public virtual IClientSessionHandle SessionHandle => DbContext.SessionHandle;
public async Task<IMongoDatabase> GetDatabaseAsync(CancellationToken cancellationToken = default)
{
return (await GetDbContextAsync(GetCancellationToken(cancellationToken))).Database;
}
[Obsolete("Use GetSessionHandleAsync method.")]
protected virtual IClientSessionHandle SessionHandle => DbContext.SessionHandle;
public virtual TMongoDbContext DbContext => DbContextProvider.GetDbContext();
protected async Task<IClientSessionHandle> GetSessionHandleAsync(CancellationToken cancellationToken = default)
{
return (await GetDbContextAsync(GetCancellationToken(cancellationToken))).SessionHandle;
}
[Obsolete("Use GetDbContextAsync method.")]
protected virtual TMongoDbContext DbContext => DbContextProvider.GetDbContext();
protected Task<TMongoDbContext> GetDbContextAsync(CancellationToken cancellationToken = default)
{
return DbContextProvider.GetDbContextAsync(GetCancellationToken(cancellationToken));
}
protected IMongoDbContextProvider<TMongoDbContext> DbContextProvider { get; }
@ -59,24 +83,27 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
GuidGenerator = SimpleGuidGenerator.Instance;
}
public async override Task<TEntity> InsertAsync(
public override async Task<TEntity> InsertAsync(
TEntity entity,
bool autoSave = false,
CancellationToken cancellationToken = default)
{
await ApplyAbpConceptsForAddedEntityAsync(entity);
if (SessionHandle != null)
var dbContext = await GetDbContextAsync(GetCancellationToken(cancellationToken));
var collection = dbContext.Collection<TEntity>();
if (dbContext.SessionHandle != null)
{
await Collection.InsertOneAsync(
SessionHandle,
await collection.InsertOneAsync(
dbContext.SessionHandle,
entity,
cancellationToken: GetCancellationToken(cancellationToken)
);
}
else
{
await Collection.InsertOneAsync(
await collection.InsertOneAsync(
entity,
cancellationToken: GetCancellationToken(cancellationToken)
);
@ -87,33 +114,38 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
public override async Task InsertManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
foreach (var entity in entities)
var entityArray = entities.ToArray();
foreach (var entity in entityArray)
{
await ApplyAbpConceptsForAddedEntityAsync(entity);
}
var dbContext = await GetDbContextAsync(GetCancellationToken(cancellationToken));
var collection = dbContext.Collection<TEntity>();
if (BulkOperationProvider != null)
{
await BulkOperationProvider.InsertManyAsync(this, entities, SessionHandle, autoSave, cancellationToken);
await BulkOperationProvider.InsertManyAsync(this, entityArray, dbContext.SessionHandle, autoSave, cancellationToken);
return;
}
if (SessionHandle != null)
if (dbContext.SessionHandle != null)
{
await Collection.InsertManyAsync(
SessionHandle,
entities,
await collection.InsertManyAsync(
dbContext.SessionHandle,
entityArray,
cancellationToken: cancellationToken);
}
else
{
await Collection.InsertManyAsync(
entities,
await collection.InsertManyAsync(
entityArray,
cancellationToken: cancellationToken);
}
}
public async override Task<TEntity> UpdateAsync(
public override async Task<TEntity> UpdateAsync(
TEntity entity,
bool autoSave = false,
CancellationToken cancellationToken = default)
@ -135,20 +167,21 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
ReplaceOneResult result;
if (SessionHandle != null)
var dbContext = await GetDbContextAsync(GetCancellationToken(cancellationToken));
var collection = dbContext.Collection<TEntity>();
if (dbContext.SessionHandle != null)
{
result = await Collection.ReplaceOneAsync(
SessionHandle,
result = await collection.ReplaceOneAsync(
dbContext.SessionHandle,
CreateEntityFilter(entity, true, oldConcurrencyStamp),
entity,
cancellationToken: GetCancellationToken(cancellationToken)
);
}
else
{
result = await Collection.ReplaceOneAsync(
result = await collection.ReplaceOneAsync(
CreateEntityFilter(entity, true, oldConcurrencyStamp),
entity,
cancellationToken: GetCancellationToken(cancellationToken)
@ -165,12 +198,13 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
public override async Task UpdateManyAsync(IEnumerable<TEntity> entities, bool autoSave = false, CancellationToken cancellationToken = default)
{
var isSoftDeleteEntity = typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity));
var entityArray = entities.ToArray();
foreach (var entity in entities)
foreach (var entity in entityArray)
{
SetModificationAuditProperties(entity);
var isSoftDeleteEntity = typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity));
if (isSoftDeleteEntity)
{
SetDeletionAuditProperties(entity);
@ -186,37 +220,40 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
SetNewConcurrencyStamp(entity);
}
cancellationToken = GetCancellationToken(cancellationToken);
var dbContext = await GetDbContextAsync(cancellationToken);
if (BulkOperationProvider != null)
{
await BulkOperationProvider.UpdateManyAsync(this, entities, SessionHandle, autoSave, cancellationToken);
await BulkOperationProvider.UpdateManyAsync(this, entityArray, dbContext.SessionHandle, autoSave, cancellationToken);
return;
}
var entitiesCount = entities.Count();
BulkWriteResult result;
List<WriteModel<TEntity>> replaceRequests = new List<WriteModel<TEntity>>();
foreach (var entity in entities)
foreach (var entity in entityArray)
{
replaceRequests.Add(new ReplaceOneModel<TEntity>(CreateEntityFilter(entity), entity));
}
if (SessionHandle != null)
var collection = dbContext.Collection<TEntity>();
if (dbContext.SessionHandle != null)
{
result = await Collection.BulkWriteAsync(SessionHandle, replaceRequests);
result = await collection.BulkWriteAsync(dbContext.SessionHandle, replaceRequests, cancellationToken: cancellationToken);
}
else
{
result = await Collection.BulkWriteAsync(replaceRequests);
result = await collection.BulkWriteAsync(replaceRequests, cancellationToken: cancellationToken);
}
if (result.MatchedCount < entitiesCount)
if (result.MatchedCount < entityArray.Length)
{
ThrowOptimisticConcurrencyException();
}
}
public async override Task DeleteAsync(
public override async Task DeleteAsync(
TEntity entity,
bool autoSave = false,
CancellationToken cancellationToken = default)
@ -224,15 +261,18 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
await ApplyAbpConceptsForDeletedEntityAsync(entity);
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
var dbContext = await GetDbContextAsync(GetCancellationToken(cancellationToken));
var collection = dbContext.Collection<TEntity>();
if (entity is ISoftDelete softDeleteEntity && !IsHardDeleted(entity))
{
softDeleteEntity.IsDeleted = true;
ReplaceOneResult result;
if (SessionHandle != null)
if (dbContext.SessionHandle != null)
{
result = await Collection.ReplaceOneAsync(
SessionHandle,
result = await collection.ReplaceOneAsync(
dbContext.SessionHandle,
CreateEntityFilter(entity, true, oldConcurrencyStamp),
entity,
cancellationToken: GetCancellationToken(cancellationToken)
@ -240,7 +280,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
}
else
{
result = await Collection.ReplaceOneAsync(
result = await collection.ReplaceOneAsync(
CreateEntityFilter(entity, true, oldConcurrencyStamp),
entity,
cancellationToken: GetCancellationToken(cancellationToken)
@ -256,17 +296,17 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
{
DeleteResult result;
if (SessionHandle != null)
if (dbContext.SessionHandle != null)
{
result = await Collection.DeleteOneAsync(
SessionHandle,
result = await collection.DeleteOneAsync(
dbContext.SessionHandle,
CreateEntityFilter(entity, true, oldConcurrencyStamp),
cancellationToken: GetCancellationToken(cancellationToken)
);
}
else
{
result = await Collection.DeleteOneAsync(
result = await collection.DeleteOneAsync(
CreateEntityFilter(entity, true, oldConcurrencyStamp),
GetCancellationToken(cancellationToken)
);
@ -284,35 +324,40 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
bool autoSave = false,
CancellationToken cancellationToken = default)
{
foreach (var entity in entities)
var entityArray = entities.ToArray();
foreach (var entity in entityArray)
{
await ApplyAbpConceptsForDeletedEntityAsync(entity);
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
SetNewConcurrencyStamp(entity);
}
var dbContext = await GetDbContextAsync(GetCancellationToken(cancellationToken));
var collection = dbContext.Collection<TEntity>();
if (BulkOperationProvider != null)
{
await BulkOperationProvider.DeleteManyAsync(this, entities, SessionHandle, autoSave, cancellationToken);
await BulkOperationProvider.DeleteManyAsync(this, entityArray, dbContext.SessionHandle, autoSave, cancellationToken);
return;
}
var entitiesCount = entities.Count();
var entitiesCount = entityArray.Count();
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
{
UpdateResult updateResult;
if (SessionHandle != null)
if (dbContext.SessionHandle != null)
{
updateResult = await Collection.UpdateManyAsync(
SessionHandle,
CreateEntitiesFilter(entities),
updateResult = await collection.UpdateManyAsync(
dbContext.SessionHandle,
CreateEntitiesFilter(entityArray),
Builders<TEntity>.Update.Set(x => ((ISoftDelete)x).IsDeleted, true)
);
}
else
{
updateResult = await Collection.UpdateManyAsync(
CreateEntitiesFilter(entities),
updateResult = await collection.UpdateManyAsync(
CreateEntitiesFilter(entityArray),
Builders<TEntity>.Update.Set(x => ((ISoftDelete)x).IsDeleted, true)
);
}
@ -325,17 +370,17 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
else
{
DeleteResult deleteResult;
if (SessionHandle != null)
if (dbContext.SessionHandle != null)
{
deleteResult = await Collection.DeleteManyAsync(
SessionHandle,
CreateEntitiesFilter(entities)
deleteResult = await collection.DeleteManyAsync(
dbContext.SessionHandle,
CreateEntitiesFilter(entityArray)
);
}
else
{
deleteResult = await Collection.DeleteManyAsync(
CreateEntitiesFilter(entities)
deleteResult = await collection.DeleteManyAsync(
CreateEntitiesFilter(entityArray)
);
}
@ -346,38 +391,44 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
}
}
public async override Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
public override async Task<List<TEntity>> GetListAsync(bool includeDetails = false, CancellationToken cancellationToken = default)
{
return await GetMongoQueryable().ToListAsync(GetCancellationToken(cancellationToken));
cancellationToken = GetCancellationToken(cancellationToken);
return await (await GetMongoQueryableAsync(cancellationToken)).ToListAsync(cancellationToken);
}
public async override Task<long> GetCountAsync(CancellationToken cancellationToken = default)
public override async Task<long> GetCountAsync(CancellationToken cancellationToken = default)
{
return await GetMongoQueryable().LongCountAsync(GetCancellationToken(cancellationToken));
cancellationToken = GetCancellationToken(cancellationToken);
return await (await GetMongoQueryableAsync(cancellationToken)).LongCountAsync(cancellationToken);
}
public async override Task<List<TEntity>> GetPagedListAsync(
public override async Task<List<TEntity>> GetPagedListAsync(
int skipCount,
int maxResultCount,
string sorting,
bool includeDetails = false,
CancellationToken cancellationToken = default)
{
return await GetMongoQueryable()
cancellationToken = GetCancellationToken(cancellationToken);
return await (await GetMongoQueryableAsync(cancellationToken))
.OrderBy(sorting)
.As<IMongoQueryable<TEntity>>()
.PageBy<TEntity, IMongoQueryable<TEntity>>(skipCount, maxResultCount)
.ToListAsync(GetCancellationToken(cancellationToken));
.ToListAsync(cancellationToken);
}
public async override Task DeleteAsync(
public override async Task DeleteAsync(
Expression<Func<TEntity, bool>> predicate,
bool autoSave = false,
CancellationToken cancellationToken = default)
{
var entities = await GetMongoQueryable()
cancellationToken = GetCancellationToken(cancellationToken);
var entities = await (await GetMongoQueryableAsync(cancellationToken))
.Where(predicate)
.ToListAsync(GetCancellationToken(cancellationToken));
.ToListAsync(cancellationToken);
foreach (var entity in entities)
{
@ -385,25 +436,49 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
}
}
[Obsolete("Use GetQueryableAsync method.")]
protected override IQueryable<TEntity> GetQueryable()
{
return GetMongoQueryable();
}
public async override Task<TEntity> FindAsync(
public override async Task<IQueryable<TEntity>> GetQueryableAsync()
{
return await GetMongoQueryableAsync();
}
public override async Task<TEntity> FindAsync(
Expression<Func<TEntity, bool>> predicate,
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
return await GetMongoQueryable()
return await (await GetMongoQueryableAsync(cancellationToken))
.Where(predicate)
.SingleOrDefaultAsync(GetCancellationToken(cancellationToken));
}
[Obsolete("Use GetMongoQueryableAsync method.")]
public virtual IMongoQueryable<TEntity> GetMongoQueryable()
{
return ApplyDataFilters(SessionHandle != null ? Collection.AsQueryable(SessionHandle) : Collection.AsQueryable());
return ApplyDataFilters(
SessionHandle != null
? Collection.AsQueryable(SessionHandle)
: Collection.AsQueryable()
);
}
public async Task<IMongoQueryable<TEntity>> GetMongoQueryableAsync(CancellationToken cancellationToken = default)
{
var dbContext = await GetDbContextAsync(cancellationToken);
var collection = dbContext.Collection<TEntity>();
return ApplyDataFilters(
dbContext.SessionHandle != null
? collection.AsQueryable(dbContext.SessionHandle)
: collection.AsQueryable()
);
}
protected virtual bool IsHardDeleted(TEntity entity)
{
var hardDeletedEntities = UnitOfWorkManager?.Current?.Items.GetOrDefault(UnitOfWorkItemNames.HardDeletedEntities) as HashSet<IEntity>;
@ -552,30 +627,19 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
throw new AbpDbConcurrencyException("Database operation expected to affect 1 row but actually affected 0 row. Data may have been modified or deleted since entities were loaded. This exception has been thrown on optimistic concurrency check.");
}
/// <summary>
/// IMongoQueryable<TEntity>
/// </summary>
/// <returns></returns>
[Obsolete("This method will be removed in future versions.")]
public QueryableExecutionModel GetExecutionModel()
{
return GetMongoQueryable().GetExecutionModel();
}
/// <summary>
/// IMongoQueryable<TEntity>
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Obsolete("This method will be removed in future versions.")]
public IAsyncCursor<TEntity> ToCursor(CancellationToken cancellationToken = new CancellationToken())
{
return GetMongoQueryable().ToCursor(cancellationToken);
}
/// <summary>
/// IMongoQueryable<TEntity>
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
[Obsolete("This method will be removed in future versions.")]
public Task<IAsyncCursor<TEntity>> ToCursorAsync(CancellationToken cancellationToken = new CancellationToken())
{
return GetMongoQueryable().ToCursorAsync(cancellationToken);
@ -616,16 +680,21 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
bool includeDetails = true,
CancellationToken cancellationToken = default)
{
if (SessionHandle != null)
cancellationToken = GetCancellationToken(cancellationToken);
var dbContext = await GetDbContextAsync(cancellationToken);
var collection = dbContext.Collection<TEntity>();
if (dbContext.SessionHandle != null)
{
return await Collection
.Find(SessionHandle, RepositoryFilterer.CreateEntityFilter(id, true))
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
return await collection
.Find(dbContext.SessionHandle, RepositoryFilterer.CreateEntityFilter(id, true))
.FirstOrDefaultAsync(cancellationToken);
}
return await Collection
return await collection
.Find(RepositoryFilterer.CreateEntityFilter(id, true))
.FirstOrDefaultAsync(GetCancellationToken(cancellationToken));
.FirstOrDefaultAsync(cancellationToken);
}
public virtual Task DeleteAsync(
@ -638,9 +707,11 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
public virtual async Task DeleteManyAsync([NotNull] IEnumerable<TKey> ids, bool autoSave = false, CancellationToken cancellationToken = default)
{
var entities = await GetMongoQueryable()
cancellationToken = GetCancellationToken(cancellationToken);
var entities = await (await GetMongoQueryableAsync(cancellationToken))
.Where(x => ids.Contains(x.Id))
.ToListAsync(GetCancellationToken(cancellationToken));
.ToListAsync(cancellationToken);
await DeleteManyAsync(entities, autoSave, cancellationToken);
}

24
framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDbCoreRepositoryExtensions.cs

@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Volo.Abp.Domain.Entities;
@ -8,24 +9,45 @@ namespace Volo.Abp.Domain.Repositories
{
public static class MongoDbCoreRepositoryExtensions
{
[Obsolete("Use GetDatabaseAsync method.")]
public static IMongoDatabase GetDatabase<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().Database;
}
public static Task<IMongoDatabase> GetDatabaseAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().GetDatabaseAsync();
}
[Obsolete("Use GetCollection method.")]
public static IMongoCollection<TEntity> GetCollection<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().Collection;
}
public static Task<IMongoCollection<TEntity>> GetCollectionAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().GetCollectionAsync();
}
[Obsolete("Use GetMongoQueryableAsync method.")]
public static IMongoQueryable<TEntity> GetMongoQueryable<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().GetMongoQueryable();
}
public static Task<IMongoQueryable<TEntity>> GetMongoQueryableAsync<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
return repository.ToMongoDbRepository().GetMongoQueryableAsync();
}
public static IMongoDbRepository<TEntity, TKey> ToMongoDbRepository<TEntity, TKey>(this IBasicRepository<TEntity, TKey> repository)
where TEntity : class, IEntity<TKey>
{
@ -38,4 +60,4 @@ namespace Volo.Abp.Domain.Repositories
return mongoDbRepository;
}
}
}
}

13
framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/IMongoDbContextProvider.cs

@ -1,8 +1,15 @@
namespace Volo.Abp.MongoDB
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Volo.Abp.MongoDB
{
public interface IMongoDbContextProvider<out TMongoDbContext>
public interface IMongoDbContextProvider<TMongoDbContext>
where TMongoDbContext : IAbpMongoDbContext
{
[Obsolete("Use CreateDbContextAsync")]
TMongoDbContext GetDbContext();
Task<TMongoDbContext> GetDbContextAsync(CancellationToken cancellationToken = default);
}
}
}

2
framework/src/Volo.Abp.MongoDB/Volo/Abp/MongoDB/MongoDbAsyncQueryableProvider.cs

@ -12,7 +12,7 @@ using Volo.Abp.DynamicProxy;
namespace Volo.Abp.MongoDB
{
public class MongoDbAsyncQueryableProvider : IAsyncQueryableProvider, ITransientDependency
public class MongoDbAsyncQueryableProvider : IAsyncQueryableProvider, ISingletonDependency
{
public bool CanExecute<T>(IQueryable<T> queryable)
{

133
framework/src/Volo.Abp.MongoDB/Volo/Abp/Uow/MongoDB/UnitOfWorkMongoDbContextProvider.cs

@ -1,28 +1,48 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using MongoDB.Bson;
using MongoDB.Driver;
using Volo.Abp.Data;
using Volo.Abp.MongoDB;
using Volo.Abp.Threading;
namespace Volo.Abp.Uow.MongoDB
{
public class UnitOfWorkMongoDbContextProvider<TMongoDbContext> : IMongoDbContextProvider<TMongoDbContext>
where TMongoDbContext : IAbpMongoDbContext
{
public ILogger<UnitOfWorkMongoDbContextProvider<TMongoDbContext>> Logger { get; set; }
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConnectionStringResolver _connectionStringResolver;
private readonly ICancellationTokenProvider _cancellationTokenProvider;
public UnitOfWorkMongoDbContextProvider(
IUnitOfWorkManager unitOfWorkManager,
IConnectionStringResolver connectionStringResolver)
IConnectionStringResolver connectionStringResolver,
ICancellationTokenProvider cancellationTokenProvider)
{
_unitOfWorkManager = unitOfWorkManager;
_connectionStringResolver = connectionStringResolver;
_cancellationTokenProvider = cancellationTokenProvider;
Logger = NullLogger<UnitOfWorkMongoDbContextProvider<TMongoDbContext>>.Instance;
}
[Obsolete("Use CreateDbContextAsync")]
public TMongoDbContext GetDbContext()
{
Logger.LogWarning(
"UnitOfWorkDbContextProvider.GetDbContext is deprecated. Use GetDbContextAsync instead! " +
"You are probably using LINQ (LINQ extensions) directly on a repository. In this case, use repository.GetQueryableAsync() method " +
"to obtain an IQueryable<T> instance and use LINQ (LINQ extensions) on this object. "
);
Logger.LogWarning(Environment.StackTrace.Truncate(2048));
var unitOfWork = _unitOfWorkManager.Current;
if (unitOfWork == null)
{
@ -48,6 +68,46 @@ namespace Volo.Abp.Uow.MongoDB
return ((MongoDbDatabaseApi<TMongoDbContext>) databaseApi).DbContext;
}
public async Task<TMongoDbContext> GetDbContextAsync(CancellationToken cancellationToken = default)
{
var unitOfWork = _unitOfWorkManager.Current;
if (unitOfWork == null)
{
throw new AbpException(
$"A {nameof(IMongoDatabase)} instance can only be created inside a unit of work!");
}
var connectionString = await _connectionStringResolver.ResolveAsync<TMongoDbContext>();
var dbContextKey = $"{typeof(TMongoDbContext).FullName}_{connectionString}";
var mongoUrl = new MongoUrl(connectionString);
var databaseName = mongoUrl.DatabaseName;
if (databaseName.IsNullOrWhiteSpace())
{
databaseName = ConnectionStringNameAttribute.GetConnStringName<TMongoDbContext>();
}
//TODO: Create only single MongoDbClient per connection string in an application (extract MongoClientCache for example).
var databaseApi = unitOfWork.FindDatabaseApi(dbContextKey);
if (databaseApi == null)
{
databaseApi = new MongoDbDatabaseApi<TMongoDbContext>(
await CreateDbContextAsync(
unitOfWork,
mongoUrl,
databaseName,
cancellationToken
)
);
unitOfWork.AddDatabaseApi(dbContextKey, databaseApi);
}
return ((MongoDbDatabaseApi<TMongoDbContext>) databaseApi).DbContext;
}
[Obsolete("Use CreateDbContextAsync")]
private TMongoDbContext CreateDbContext(IUnitOfWork unitOfWork, MongoUrl mongoUrl, string databaseName)
{
var client = new MongoClient(mongoUrl);
@ -64,7 +124,34 @@ namespace Volo.Abp.Uow.MongoDB
return dbContext;
}
public TMongoDbContext CreateDbContextWithTransaction(
private async Task<TMongoDbContext> CreateDbContextAsync(
IUnitOfWork unitOfWork,
MongoUrl mongoUrl,
string databaseName,
CancellationToken cancellationToken = default)
{
var client = new MongoClient(mongoUrl);
var database = client.GetDatabase(databaseName);
if (unitOfWork.Options.IsTransactional)
{
return await CreateDbContextWithTransactionAsync(
unitOfWork,
mongoUrl,
client,
database,
cancellationToken
);
}
var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TMongoDbContext>();
dbContext.ToAbpMongoDbContext().InitializeDatabase(database, client, null);
return dbContext;
}
[Obsolete("Use CreateDbContextWithTransactionAsync")]
private TMongoDbContext CreateDbContextWithTransaction(
IUnitOfWork unitOfWork,
MongoUrl url,
MongoClient client,
@ -99,5 +186,47 @@ namespace Volo.Abp.Uow.MongoDB
return dbContext;
}
private async Task<TMongoDbContext> CreateDbContextWithTransactionAsync(
IUnitOfWork unitOfWork,
MongoUrl url,
MongoClient client,
IMongoDatabase database,
CancellationToken cancellationToken = default)
{
var transactionApiKey = $"MongoDb_{url}";
var activeTransaction = unitOfWork.FindTransactionApi(transactionApiKey) as MongoDbTransactionApi;
var dbContext = unitOfWork.ServiceProvider.GetRequiredService<TMongoDbContext>();
if (activeTransaction?.SessionHandle == null)
{
var session = await client.StartSessionAsync(cancellationToken: GetCancellationToken(cancellationToken));
if (unitOfWork.Options.Timeout.HasValue)
{
session.AdvanceOperationTime(new BsonTimestamp(unitOfWork.Options.Timeout.Value));
}
session.StartTransaction();
unitOfWork.AddTransactionApi(
transactionApiKey,
new MongoDbTransactionApi(session)
);
dbContext.ToAbpMongoDbContext().InitializeDatabase(database, client, session);
}
else
{
dbContext.ToAbpMongoDbContext().InitializeDatabase(database, client, activeTransaction.SessionHandle);
}
return dbContext;
}
protected virtual CancellationToken GetCancellationToken(CancellationToken preferredValue = default)
{
return _cancellationTokenProvider.FallbackToProvider(preferredValue);
}
}
}

4
framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/ITenantStore.cs

@ -9,8 +9,10 @@ namespace Volo.Abp.MultiTenancy
Task<TenantConfiguration> FindAsync(Guid id);
[Obsolete("Use FindAsync method.")]
TenantConfiguration Find(string name);
[Obsolete("Use FindAsync method.")]
TenantConfiguration Find(Guid id);
}
}
}

53
framework/src/Volo.Abp.MultiTenancy/Volo/Abp/MultiTenancy/MultiTenantConnectionStringResolver.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Data;
@ -23,6 +24,58 @@ namespace Volo.Abp.MultiTenancy
_serviceProvider = serviceProvider;
}
public override async Task<string> ResolveAsync(string connectionStringName = null)
{
//No current tenant, fallback to default logic
if (_currentTenant.Id == null)
{
return await base.ResolveAsync(connectionStringName);
}
using (var serviceScope = _serviceProvider.CreateScope())
{
var tenantStore = serviceScope
.ServiceProvider
.GetRequiredService<ITenantStore>();
var tenant = await tenantStore.FindAsync(_currentTenant.Id.Value);
if (tenant?.ConnectionStrings == null)
{
return await base.ResolveAsync(connectionStringName);
}
//Requesting default connection string
if (connectionStringName == null)
{
return tenant.ConnectionStrings.Default ??
Options.ConnectionStrings.Default;
}
//Requesting specific connection string
var connString = tenant.ConnectionStrings.GetOrDefault(connectionStringName);
if (connString != null)
{
return connString;
}
/* Requested a specific connection string, but it's not specified for the tenant.
* - If it's specified in options, use it.
* - If not, use tenant's default conn string.
*/
var connStringInOptions = Options.ConnectionStrings.GetOrDefault(connectionStringName);
if (connStringInOptions != null)
{
return connStringInOptions;
}
return tenant.ConnectionStrings.Default ??
Options.ConnectionStrings.Default;
}
}
[Obsolete("Use ResolveAsync method.")]
public override string Resolve(string connectionStringName = null)
{
//No current tenant, fallback to default logic

3
framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/Modularity/EntityExtensionConfiguration.cs

@ -67,7 +67,8 @@ namespace Volo.Abp.ObjectExtending.Modularity
lookupTextPropertyName,
() => new ExtensionPropertyConfiguration(this, typeof(string), lookupTextPropertyName)
);
lookupTextPropertyInfo.DisplayName = propertyInfo.DisplayName ?? new FixedLocalizableString(propertyInfo.Name);
lookupTextPropertyInfo.DisplayName = propertyInfo.DisplayName;
}
[NotNull]

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

Loading…
Cancel
Save