Browse Source

Merge branch 'dev' into berkan/add-localization-for-trial-license

pull/10005/head
Berkan Sasmaz 5 years ago
parent
commit
69bf3e18c2
  1. 12
      .github/workflows/cancel-workflow.yml
  2. 9
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  3. 4
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
  4. 23
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/it.json
  5. 2
      docs/en/Authorization.md
  6. 13
      docs/en/CLI.md
  7. 35
      docs/en/Data-Filtering.md
  8. 8
      docs/en/Data-Transfer-Objects.md
  9. 6
      docs/en/Modules/Permission-Management.md
  10. 2
      docs/en/Modules/Setting-Management.md
  11. 2
      docs/en/Text-Templating-Scriban.md
  12. 2
      docs/en/Tutorials/Part-9.md
  13. 2
      docs/en/Tutorials/Todo/Index.md
  14. 2
      docs/en/UI/Angular/Settings.md
  15. 33
      docs/zh-Hans/Data-Filtering.md
  16. 8
      docs/zh-Hans/Data-Transfer-Objects.md
  17. 830
      docs/zh-Hans/Tutorials/Todo/Index.md
  18. 0
      docs/zh-Hans/Tutorials/Todo/run-without-iisexpress.png
  19. 0
      docs/zh-Hans/Tutorials/Todo/todo-api.png
  20. BIN
      docs/zh-Hans/Tutorials/Todo/todo-efcore-migration.png
  21. 0
      docs/zh-Hans/Tutorials/Todo/todo-list.png
  22. 0
      docs/zh-Hans/Tutorials/Todo/todo-swagger-ui-initial.png
  23. 0
      docs/zh-Hans/Tutorials/Todo/todo-ui-initial.png
  24. 822
      docs/zh-Hans/Tutorials/todo/Index.md
  25. BIN
      docs/zh-Hans/Tutorials/todo/todo-efcore-migration.png
  26. 4
      docs/zh-Hans/docs-nav.json
  27. 2
      docs/zh-Hans/docs-params.json
  28. 27
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Properties/launchSettings.json
  29. 1
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo.Abp.AspNetCore.Authentication.OAuth.csproj
  30. 1
      framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Volo.Abp.AspNetCore.Components.Server.Theming.csproj
  31. 1
      framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj
  32. 27
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Properties/launchSettings.json
  33. 1
      framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo.Abp.AspNetCore.Mvc.Client.csproj
  34. 10
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs
  35. 9
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentOutputFormatter.cs
  36. 27
      framework/src/Volo.Abp.AspNetCore.Serilog/Properties/launchSettings.json
  37. 1
      framework/src/Volo.Abp.AspNetCore.Serilog/Volo.Abp.AspNetCore.Serilog.csproj
  38. 27
      framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json
  39. 1
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj
  40. 27
      framework/src/Volo.Abp.AspNetCore.TestBase/Properties/launchSettings.json
  41. 1
      framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj
  42. 27
      framework/src/Volo.Abp.AspNetCore/Properties/launchSettings.json
  43. 1
      framework/src/Volo.Abp.AspNetCore/Volo.Abp.AspNetCore.csproj
  44. 14
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
  45. 8
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs
  46. 58
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/ProjectNameValidator.cs
  47. 9
      framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs
  48. 36
      framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs
  49. 18
      framework/src/Volo.Abp.Data/Volo/Abp/Data/ConcurrencyStampExtensions.cs
  50. 25
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/BasicAggregateRoot.cs
  51. 15
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/DomainEventRecord.cs
  52. 5
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/DomainEventEntry.cs
  53. 260
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs
  54. 32
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeReport.cs
  55. 1
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangingEventData.cs
  56. 1
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityCreatingEventData.cs
  57. 1
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityDeletingEventData.cs
  58. 22
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityEventReport.cs
  59. 1
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityUpdatingEventData.cs
  60. 16
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/IEntityChangeEventHelper.cs
  61. 41
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/NullEntityChangeEventHelper.cs
  62. 4
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/IGeneratesDomainEvents.cs
  63. 176
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  64. 11
      framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs
  65. 12
      framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs
  66. 11
      framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs
  67. 8
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs
  68. 4
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs
  69. 29
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs
  70. 6
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/IEventBus.cs
  71. 11
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs
  72. 4
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs
  73. 40
      framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/UnitOfWorkEventPublisher.cs
  74. 27
      framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Properties/launchSettings.json
  75. 1
      framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo.Abp.Http.Client.IdentityModel.Web.csproj
  76. 11
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs
  77. 11
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs
  78. 4
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs
  79. 59
      framework/src/Volo.Abp.MemoryDb/Volo/Abp/Domain/Repositories/MemoryDb/MemoryDbRepository.cs
  80. 65
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
  81. 13
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/AmbientUnitOfWork.cs
  82. 14
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs
  83. 14
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/EventOrderGenerator.cs
  84. 2
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IAmbientUnitOfWork.cs
  85. 10
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs
  86. 12
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkEventPublisher.cs
  87. 19
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/NullUnitOfWorkEventPublisher.cs
  88. 89
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs
  89. 29
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkEventRecord.cs
  90. 15
      framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs
  91. 17
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController.cs
  92. 23
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController_Tests.cs
  93. 10
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/TestUnitOfWork.cs
  94. 10
      framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectNameValidation_Tests.cs
  95. 60
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs
  96. 10
      framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/DomainEvents/DomainEvents_Tests.cs
  97. 6
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs
  98. 119
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs
  99. 16
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChangeEvents_Tests.cs
  100. 1
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/TestAppTestBase.cs

12
.github/workflows/cancel-workflow.yml

@ -0,0 +1,12 @@
name: cancel-workflow
on: [push]
jobs:
cancel:
name: 'Cancel Previous Runs'
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: styfle/cancel-workflow-action@0.6.0
with:
workflow_id: 10629,1299107,2792859,8268314
access_token: ${{ github.token }}

9
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -303,6 +303,13 @@
"ProductPrice": "Product Price",
"AdditionalDeveloper": "Additional developer",
"ThisPaymentHasBeenAlreadyUsed": "This payment has been already used",
"PaymentTimeCannotBeFutureTime": "Payment time cannot be future time"
"PaymentTimeCannotBeFutureTime": "Payment time cannot be a future time!",
"SaveAndDownload": "Save and Download",
"BillingInfo": "Billing Info",
"GenerateInvoice": "Create Invoice",
"DeleteInvoice" : "Delete Invoice",
"SuccessfullyDeleted": "Successfully deleted!",
"PaymentStateSetTo" : "Payment state set to {0}",
"ChangeState": "Change State"
}
}

4
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json

@ -377,7 +377,7 @@
"AddressValidationMessage": "Address is too long!",
"TaxNoValidationMessage": "TAX/VAT No is too long!",
"NotesValidationMessage": "Notes field is too long!",
"CheckYourBillingInfo": "You can create your invoice only once! Check your billing info before creating your invoice.",
"CheckYourBillingInfo": "You can create your invoice only once! Check your billing information before creating your invoice.",
"Volo.AbpIo.Commercial:030000": "You already used your trial period.",
"Volo.AbpIo.Commercial:030001": "There is already an organization called \"{CompanyName}\"",
"Volo.AbpIo.Commercial:030002": "You already own an organization that has a license.",
@ -391,4 +391,4 @@
"TryForFree": "Try For Free",
"TrialLicenseExpiredInfo": "Your trial license period has expired!"
}
}
}

23
abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/it.json

@ -358,6 +358,25 @@
"WeWillSendYouADownloadLink": "Un link per scaricare il libro e-book è stato spedito a {0}.<br/> Controlla nella posta in arrivo / indesiderata / spam!",
"InvalidFormInputs": "Per favore, digita le informazioni valide specificate nel modulo",
"DDDBookEmailBody": "Grazie. <br /> Per scaricare il tuo libro, <a href=\"{0}\">clicca qui</a>.",
"FreeDDDEBook": "E-Book Gratuito DDD"
"FreeDDDEBook": "E-Book Gratuito DDD",
"StartFree": "Inizia gratuitamente",
"FreeTrial": "Prova Gratuita",
"AcceptsMarketingCommunications": "Si desidero ricevere comunicazioni marketing riguardanti ABP Commercial",
"PurposeOfUsage": "Proposta di utilizzo",
"Industry": "Settore",
"Choose": "- Scegli -",
"CompanyOrganizationName": "Azienda / Nome organizzazione",
"CompanySize": "Dimensioni Azienda",
"Next": "Prossimo",
"StartTrial": "Inizia la mia prova gratuita",
"ContactUsIssues": "Contattaci per qualsiasi problema",
"TrialActivatedWarning": "Caro {0}, un utente ha diritto di avere 1 solo periodo di prova gratuito. Hai già utilizzato il tuo periodo di prova.",
"ActivationRequirement": "Ti manca un solo passo per iniziare il tuo periodo di prova gratuito.<br>Abbiamo inviato un'email a <b>{0}</b> per attivare il tuo account. Controlla la tua casella di posta e non dimenticare la cartella spam!",
"SaveAndDownload": "Salva e Scarica",
"CompanyNameValidationMessage": "Il nome dell'azienda è troppo lungo!",
"AddressValidationMessage": "L'indirizzo è troppo lungo!",
"TaxNoValidationMessage": "TAX/VAT (IVA) è troppo lungo!",
"NotesValidationMessage": "Il campo note è troppo lungo!",
"CheckYourBillingInfo": "Puoi creare una fattura solo una volta! Controlla i tuoi dati di fatturazione prima di creare una fattura."
}
}
}

2
docs/en/Authorization.md

@ -256,7 +256,7 @@ myGroup.AddPermission("Book_Creation")
#### Depending on a Global Feature
Use the `RequireFeatures` extension method on your permission definition to make the permission available only if a given feature is enabled:
Use the `RequireGlobalFeatures` extension method on your permission definition to make the permission available only if a given feature is enabled:
````csharp
myGroup.AddPermission("Book_Creation")

13
docs/en/CLI.md

@ -161,7 +161,7 @@ abp add-package <package-name> [options]
Example:
````
abp add-package Volo.Abp.MongoDB
abp add-package Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
````
* This example adds the Volo.Abp.MongoDB package to the project.
@ -170,7 +170,14 @@ abp add-package Volo.Abp.MongoDB
* `--project` or `-p`: Specifies the project (.csproj) file path. If not specified, CLI tries to find a .csproj file in the current directory.
* `--with-source-code`: Downloads the source code of the package to your solution folder and uses local project references instead of NuGet/NPM packages.
* `--add-to-solution-file`: Adds the downloaded package to your solution file, so you will also see the package when you open the solution on a IDE. (only available when `--with-source-code` is True.)
* `--add-to-solution-file`: Adds the downloaded package to your solution file, so you will also see the package when you open the solution on a IDE. (only available when `--with-source-code` is True)
> Currently only the source code of the basic theme packages([MVC](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Basic-Theme) and [Blazor](https://docs.abp.io/en/abp/latest/UI/Blazor/Basic-Theme)) can be downloaded.
> - Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic
> - Volo.Abp.AspNetCore.Components.WebAssembly.BasicTheme
> - Volo.Abp.AspNetCore.Components.Web.BasicTheme
> - Volo.Abp.AspNetCore.Components.Server.BasicTheme
### add-module
@ -454,4 +461,4 @@ abp install-libs [options]
## See Also
* [Examples for the new command](CLI-New-Command-Samples.md)
* [Examples for the new command](CLI-New-Command-Samples.md)

35
docs/en/Data-Filtering.md

@ -239,15 +239,25 @@ protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntit
### MongoDB
ABP implements data filters directly in the [repository](Repositories.md) level for the [MongoDB Integration](MongoDB.md). So, it works only if you use the repositories properly. Otherwise, you should manually filter the data.
ABP abstracts the `IMongoDbRepositoryFilterer` interface to implement data filtering for the [MongoDB Integration](MongoDB.md), it works only if you use the repositories properly. Otherwise, you should manually filter the data.
Currently, the best way to implement a data filter for the MongoDB integration is to override the `AddGlobalFilters` in the repository derived from the `MongoDbRepository` class. Example:
Currently, the best way to implement a data filter for the MongoDB integration is to create a derived class of `MongoDbRepositoryFilterer` and override `AddGlobalFilters`. Example:
````csharp
public class BookRepository : MongoDbRepository<BookStoreMongoDbContext, Book, Guid>
[ExposeServices(typeof(IMongoDbRepositoryFilterer<Book, Guid>))]
public class BookMongoDbRepositoryFilterer : MongoDbRepositoryFilterer<Book, Guid> , ITransientDependency
{
protected override void AddGlobalFilters(List<FilterDefinition<Book>> filters)
public BookMongoDbRepositoryFilterer(
IDataFilter dataFilter,
ICurrentTenant currentTenant) :
base(dataFilter, currentTenant)
{
}
public override void AddGlobalFilters(List<FilterDefinition<Book>> filters)
{
base.AddGlobalFilters(filters);
if (DataFilter.IsEnabled<IIsActive>())
{
filters.Add(Builders<Book>.Filter.Eq(e => ((IIsActive)e).IsActive, true));
@ -256,7 +266,7 @@ public class BookRepository : MongoDbRepository<BookStoreMongoDbContext, Book, G
}
````
This example implements it only for the `Book` entity. If you want to implement for all entities (those implement the `IIsActive` interface), create your own custom MongoDB repository base class and override the `AddGlobalFilters` as shown below:
This example implements it only for the `Book` entity. If you want to implement for all entities (those implement the `IIsActive` interface), create your own custom MongoDB repository filterer base class and override the `AddGlobalFilters` as shown below:
````csharp
public abstract class MyMongoRepository<TMongoDbContext, TEntity, TKey> : MongoDbRepository<TMongoDbContext, TEntity, TKey>
@ -271,6 +281,8 @@ public abstract class MyMongoRepository<TMongoDbContext, TEntity, TKey> : MongoD
protected override void AddGlobalFilters(List<FilterDefinition<TEntity>> filters)
{
base.AddGlobalFilters(filters);
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))
&& DataFilter.IsEnabled<IIsActive>())
{
@ -278,6 +290,15 @@ public abstract class MyMongoRepository<TMongoDbContext, TEntity, TKey> : MongoD
}
}
}
````
> See "Set Default Repository Classes" section of the [MongoDb Integration document](MongoDB.md) to learn how to replace default repository base with your custom class.
public class MyMongoDbModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//.......
context.Services
.Replace(ServiceDescriptor.Transient(typeof(IMongoDbRepositoryFilterer<,>),typeof(MyMongoDbRepositoryFilterer<,>)));
}
}
````

8
docs/en/Data-Transfer-Objects.md

@ -210,15 +210,15 @@ namespace AbpDemo
{
//Create the query
var query = _productRepository
.OrderBy(input.Sorting)
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
.OrderBy(input.Sorting);
//Get total count from the repository
var totalCount = await query.CountAsync();
//Get entities from the repository
List<Product> products = await query.ToListAsync();
List<Product> products = await query
.Skip(input.SkipCount)
.Take(input.MaxResultCount).ToListAsync();
//Map entities to DTOs
List<ProductDto> productDtos =

6
docs/en/Modules/Permission-Management.md

@ -28,7 +28,7 @@ In this dialog, you can grant permissions for the selected role. The tabs in the
## IPermissionManager
`IPermissionManager` is the main service provided by this module. It is used to read and change the permission values. `IPermissionManager` is typically used by the *Feature Management Dialog*. However, you can inject it if you need to set a permission value.
`IPermissionManager` is the main service provided by this module. It is used to read and change the permission values. `IPermissionManager` is typically used by the *Permission Management Dialog*. However, you can inject it if you need to set a permission value.
> If you just want to read/check permission values for the current user, use the `IAuthorizationService` or the `[Authorize]` attribute as explained in the [Authorization document](../Authorization.md).
@ -91,9 +91,9 @@ public class CustomPermissionManagementProvider : PermissionManagementProvider
}
````
`PermissionManagementProvider` base class makes the default implementation (using the `IPermissionGrantRepository`) for you. You can override base methods as you need. Every provider must have a unique name, which is `Custom` in this example (keep it short since it is saved to database for each feature value record).
`PermissionManagementProvider` base class makes the default implementation (using the `IPermissionGrantRepository`) for you. You can override base methods as you need. Every provider must have a unique name, which is `Custom` in this example (keep it short since it is saved to database for each permission value record).
Once you create your provider class, you should register it using the `FeatureManagementOptions` [options class](../Options.md):
Once you create your provider class, you should register it using the `PermissionManagementOptions` [options class](../Options.md):
````csharp
Configure<PermissionManagementOptions>(options =>

2
docs/en/Modules/Setting-Management.md

@ -99,7 +99,7 @@ public class CustomSettingProvider : SettingManagementProvider
}
````
`SettingManagementProvider` base class makes the default implementation (using the `ISettingManagementStore`) for you. You can override base methods as you need. Every provider must have a unique name, which is `Custom` in this example (keep it short since it is saved to database for each feature value record).
`SettingManagementProvider` base class makes the default implementation (using the `ISettingManagementStore`) for you. You can override base methods as you need. Every provider must have a unique name, which is `Custom` in this example (keep it short since it is saved to database for each setting value record).
Once you create your provider class, you should register it using the `SettingManagementOptions` [options class](../Options.md):

2
docs/en/Text-Templating-Scriban.md

@ -1,4 +1,4 @@
# Razor Integration
# Scriban Integration
## Installation

2
docs/en/Tutorials/Part-9.md

@ -1022,7 +1022,7 @@ Create a new Razor Component Page, `/Pages/Authors.razor`, in the `Acme.BookStor
</Modal>
````
* This code is similar to the `Books.razor`, except it doesn't inherit from the `BlazorisePageBase`, but uses its own implementation.
* This code is similar to the `Books.razor`, except it doesn't inherit from the `AbpCrudPageBase`, but uses its own implementation.
* Injects the `IAuthorAppService` to consume the server side HTTP APIs from the UI. We can directly inject application service interfaces and use just like regular method calls by the help of [Dynamic C# HTTP API Client Proxy System](../API/Dynamic-CSharp-API-Clients.md), which performs REST API calls for us. See the `Authors` class below to see the usage.
* Injects the `IAuthorizationService` to check [permissions](../Authorization.md).
* Injects the `IObjectMapper` for [object to object mapping](../Object-To-Object-Mapping.md).

2
docs/en/Tutorials/Todo/Index.md

@ -618,7 +618,7 @@ Open the `Index.razor` file in the `Pages` folder of the *TodoApp.Blazor* projec
### Index.razor.css
As the final touch, open the `Index.razor.css` file in the `Pages` folder of the *TodoApp.Web* project and replace with the following content:
As the final touch, open the `Index.razor.css` file in the `Pages` folder of the *TodoApp.Blazor* project and replace with the following content:
````css
#TodoList{

2
docs/en/UI/Angular/Settings.md

@ -2,7 +2,7 @@
You can get settings on the client-side using the [config state service](./Config-State.md) if they are allowed by their setting definition on the server-side.
> This document only explains how settings work in the Angular UI projects. See the [settings document](../../../Settings.md) to understand the ABP setting system.
> This document only explains how settings work in the Angular UI projects. See the [settings document](../../Settings.md) to understand the ABP setting system.
## Before Use

33
docs/zh-Hans/Data-Filtering.md

@ -188,15 +188,25 @@ protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntit
### MongoDB
ABP直接在[仓储](Repositories.md)级别实现数据过滤用于[MongoDB 集成](MongoDB.md). 所以只有正确的使用仓储,它才会工作. 否则你需要手动过滤数据.
ABP抽象了 `IMongoDbRepositoryFilterer` 接口为[MongoDB 集成](MongoDB.md)实现数据过滤, 只有正确的使用仓储,它才会工作. 否则你需要手动过滤数据.
目前为MongoDB集成实现数据过滤的最佳方法是重写派生自 `MongoDbRepository` 仓储的 `AddGlobalFilters` 方法:
目前为MongoDB集成实现数据过滤的最佳方法是重写派生自 `MongoDbRepositoryFilterer` 基类的 `AddGlobalFilters` 方法:
````csharp
public class BookRepository : MongoDbRepository<BookStoreMongoDbContext, Book, Guid>
[ExposeServices(typeof(IMongoDbRepositoryFilterer<Book, Guid>))]
public class BookMongoDbRepositoryFilterer : MongoDbRepositoryFilterer<Book, Guid> , ITransientDependency
{
protected override void AddGlobalFilters(List<FilterDefinition<Book>> filters)
public BookMongoDbRepositoryFilterer(
IDataFilter dataFilter,
ICurrentTenant currentTenant) :
base(dataFilter, currentTenant)
{
}
public override void AddGlobalFilters(List<FilterDefinition<Book>> filters)
{
base.AddGlobalFilters(filters);
if (DataFilter.IsEnabled<IIsActive>())
{
filters.Add(Builders<Book>.Filter.Eq(e => ((IIsActive)e).IsActive, true));
@ -205,7 +215,7 @@ public class BookRepository : MongoDbRepository<BookStoreMongoDbContext, Book, G
}
````
示例中仅为 `Book` 实体实现了过滤. 如果你想要为所有的实体实现过滤 (实现了 `IIsActive` 接口的实体),可以创建自己的MongoDB仓储基类并重 写 `AddGlobalFilters` 方法. 如下所示:
示例中仅为 `Book` 实体实现了过滤. 如果你想要为所有的实体实现过滤 (实现了 `IIsActive` 接口的实体),可以创建自己的 `MongoDbRepositoryFilterer` 基类并重 写 `AddGlobalFilters` 方法. 如下所示:
````csharp
public abstract class MyMongoRepository<TMongoDbContext, TEntity, TKey> : MongoDbRepository<TMongoDbContext, TEntity, TKey>
@ -220,6 +230,8 @@ public abstract class MyMongoRepository<TMongoDbContext, TEntity, TKey> : MongoD
protected override void AddGlobalFilters(List<FilterDefinition<TEntity>> filters)
{
base.AddGlobalFilters(filters);
if (typeof(IIsActive).IsAssignableFrom(typeof(TEntity))
&& DataFilter.IsEnabled<IIsActive>())
{
@ -227,6 +239,17 @@ public abstract class MyMongoRepository<TMongoDbContext, TEntity, TKey> : MongoD
}
}
}
public class MyMongoDbModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
//.......
context.Services
.Replace(ServiceDescriptor.Transient(typeof(IMongoDbRepositoryFilterer<,>),typeof(MyMongoDbRepositoryFilterer<,>)));
}
}
````
> 参阅[MongoDb集成文档](MongoDB.md)的 "设置默认仓储" 部分了解如何使用自定义类替换默认仓储.

8
docs/zh-Hans/Data-Transfer-Objects.md

@ -210,15 +210,15 @@ namespace AbpDemo
{
//Create the query
var query = _productRepository
.OrderBy(input.Sorting)
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
.OrderBy(input.Sorting);
//Get total count from the repository
var totalCount = await query.CountAsync();
//Get entities from the repository
List<Product> products = await query.ToListAsync();
List<Product> products = await query
.Skip(input.SkipCount)
.Take(input.MaxResultCount);.ToListAsync();
//Map entities to DTOs
List<ProductDto> productDtos =

830
docs/zh-Hans/Tutorials/Todo/Index.md

@ -0,0 +1,830 @@
# 快速入门
````json
//[doc-params]
{
"UI": ["MVC", "Blazor", "BlazorServer", "NG"],
"DB": ["EF", "Mongo"]
}
````
这是一个由单个部分组成的快速入门教程, 旨在使用ABP框架构建一个简单的待办事项应用程序. 这是最终应用程序的屏幕截图:
![待办事项列表](todo-list.png)
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/TodoApp)找到已完成的项目源代码.
## 先决条件
* 一个集成开发环境 (比如: [Visual Studio](https://visualstudio.microsoft.com/vs/)) 它需要支持 [.NET 5.0+](https://dotnet.microsoft.com/download/dotnet) 的开发.
{{if DB=="Mongo"}}
* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/)
{{end}}
{{if UI=="NG"}}
* [Node v14.x](https://nodejs.org/)
{{end}}
## 创建新的解决方案
我们将使用[ABP CLI](../../CLI.md) 创建带有ABP框架的新解决方案. 你可以在命令行终端中运行以下命令来安装它:
````bash
dotnet tool install -g Volo.Abp.Cli
````
然后创建一个空文件夹, 打开命令行终端并在终端中执行以下命令:
````bash
abp new TodoApp{{if UI=="Blazor"}} -u blazor{{else if UI=="BlazorServer"}} -u blazor-server{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}}
````
{{if UI=="NG"}}
这将创建一个名为*TodoApp*的新解决方案, 其中包含`angular`和`aspnet core`文件夹. 一旦解决方案就绪, 请在你最喜欢的 IDE 中打开 ASP.NET Core 解决方案.
{{else}}
这将创建一个名为*TodoApp*的新解决方案. 一旦解决方案就绪, 请在你最喜欢的 IDE 中打开它.
{{end}}
### 创建数据库
如果你使用的是Visual Studio, 请右键单击`TodoApp.DbMigrator`项目, 选择*设置为启动项目*, 然后按*Ctrl+F5*运行它而不进行调试. 它将创建初始数据库并生成初始数据.
{{if DB=="EF"}}
> 一些IDE (例如Rider) 可能会在第一次运行时遇到问题, 因为 *DbMigrator* 添加初始迁移并重新编译项目. 在这种情况下, 在 `.DbMigrator` 项目文件夹中打开命令行终端并执行 `dotnet run` 命令.
{{end}}
### 运行应用程序
{{if UI=="MVC" || UI=="BlazorServer"}}
最好在开始开发之前运行一下应用程序. 确保 {{if UI=="BlazorServer"}}`TodoApp.Blazor`{{else}}`TodoApp.Web`{{end}} 是启动项目, 然后运行应用程序(Visual Studio中是Ctrl+F5)来查看初始UI:
{{else if UI=="Blazor"}}
最好在开始开发之前运行一下应用程序. 解决方案中有两个主要应用程序
* `TodoApp.HttpApi.Host` 承载服务器端的 HTTP API.
* `TodoApp.Blazor` 是客户端的Blazor WebAssembly应用程序.
确保 `TodoApp.HttpApi.Host` 是启动项目, 然后运行应用程序(Visual Studio中的Ctrl+F5)打开[Swagger UI](https://swagger.io/tools/swagger-ui/)来查看服务器端的 HTTP API:
![todo-swagger-ui-initial](todo-swagger-ui-initial.png)
你可以使用此 UI 探索和测试你的 HTTP API. 现在, 我们可以将 `TodoApp.Blazor` 设置为启动项目并运行它来打开实际的 Blazor 应用程序UI:
{{else if UI=="NG"}}
最好在开始开发之前运行一下应用程序. 解决方案中有两个主要应用程序;
* `TodoApp.HttpApi.Host` (在.NET解决方案中)承载服务器端HTTP API.
* `Angular` 文件夹包含 Angular 应用程序.
确保 `TodoApp.HttpApi.Host` 是启动项目, 然后运行应用程序(Visual Studio中的Ctrl+F5)打开[Swagger UI](https://swagger.io/tools/swagger-ui/)来查看服务器端的 HTTP API:
![todo-swagger-ui-initial](todo-swagger-ui-initial.png)
你可以使用此 UI 探索和测试你的 HTTP API. 如果该功能正常, 我们可以运行Angular 客户端应用程序.
首先, 运行以下命令来还原NPM包:
````bash
npm install
````
安装所有软件包需要一些时间. 然后可以使用以下命令运行应用程序:
````bash
npm start
````
此命令需要时间, 但最终在你的默认浏览器中运行并打开应用程序:
{{end}}
![todo-ui-initial](todo-ui-initial.png)
你可以单击 *登录* 按钮, 以`admin`作为用户名和`1q2w3E*` 作为密码登录到应用程序.
一切就绪. 我们可以开始编程!
## 领域层
此应用程序只有一个 [实体](../../Entities.md), 接下来我们开始创建它. 在 *TodoApp.Domain* 项目中创建一个新的 `TodoItem` 类:
````csharp
using System;
using Volo.Abp.Domain.Entities;
namespace TodoApp
{
public class TodoItem : BasicAggregateRoot<Guid>
{
public string Text { get; set; }
}
}
````
`BasicAggregateRoot` 是创建根实体的最简单的基础类. `Guid` 是这里实体的主键 (`Id`).
## 数据库集成
{{if DB=="EF"}}
下一步是设置 [Entity Framework Core](../../Entity-Framework-Core.md)配置
### 映射配置
打开在 *TodoApp.EntityFrameworkCore* 项目中 `EntityFrameworkCore` 文件夹中的 `TodoAppDbContext` 类, 并向该类添加新的 `DbSet` 属性:
````csharp
public DbSet<TodoItem> TodoItems { get; set; }
````
然后在 `TodoAppDbContext` 类中定位到 `OnModelCreating` 方法, 并为 `TodoItem` 实体添加映射代码:
````csharp
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
/* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
...
/* Configure your own tables/entities inside here */
builder.Entity<TodoItem>(b =>
{
b.ToTable("TodoItems");
});
}
````
我们已经将 `TodoItem` 实体映射到数据库中的 `TodoItems` 表.
### Code First 迁移
解决方案快速模版已经配置为使用Entity Framework Core的 [Code First 迁移](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations). 由于我们已经更改了数据库映射配置, 因此我们应该创建一个新的迁移并将更改应用于数据库.
在 *TodoApp.EntityFrameworkCore* 项目目录中打开一个命令行终端并输入以下命令:
````bash
dotnet ef migrations add Added_TodoItem
````
这将向项目添加一个新的迁移类:
![todo-efcore-migration](todo-efcore-migration.png)
你可以在同一命令行终端中使用以下命令将更改应用于数据库:
````bash
dotnet ef database update
````
> 如果你使用的是Visual Studio, 则可能希望在 *包管理器控制台 (PMC)* 中使用 `Add-Migration Added_TodoItem` 和 `Update-Database` 命令. 在这种情况下, 请确保 {{if UI=="MVC"}}`TodoApp.Web`{{else if UI=="BlazorServer"}}`TodoApp.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`TodoApp.HttpApi.Host`{{end}} 是启动项目, 并且 `TodoApp.EntityFrameworkCore` 是PMC中的 *默认项目*.
{{else if DB=="Mongo"}}
下一步是设置 [MongoDB](../../MongoDB.md) 配置. 打开 *TodoApp.MongoDB* 项目中 `MongoDb` 文件夹的`TodoAppMongoDbContext` 类并做出以下更改;
1. 向类添加新属性:
````csharp
public IMongoCollection<TodoItem> TodoItems => Collection<TodoItem>();
````
2. 在 `CreateModel` 方法中添加以下代码:
````csharp
modelBuilder.Entity<TodoItem>(b =>
{
b.CollectionName = "TodoItems";
});
````
{{end}}
现在, 我们可以使用ABP仓库保存和检索待办事项, 如我们在下一节所做的那样.
## 应用层
[应用程序服务](../../Application-Services.md) 用于执行应用程序的用例. 我们需要执行以下用例:
* 获取待办事项列表
* 创建新的待办事项
* 删除现有的待办事项
### 应用服务接口
我们可以先从为应用程序服务定义接口开始. 在 *TodoApp.Application.Contracts* 项目中创建新的 `ITodoAppService` 接口, 如下所示:
````csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace TodoApp
{
public interface ITodoAppService : IApplicationService
{
Task<List<TodoItemDto>> GetListAsync();
Task<TodoItemDto> CreateAsync(string text);
Task DeleteAsync(Guid id);
}
}
````
### 数据传输对象
`GetListAsync` 和`CreateAsync` 方法返回 `TodoItemDto`. `应用程序服务` 通常获取并返回 DTO([数据传输对象](../../Data-Transfer-Objects.md)) 而不是实体. 因此, 我们应该在这里定义DTO类. 在 *TodoApp.Application.Contracts* 项目中创建新的 `TodoItemDto` 类:
````csharp
using System;
namespace TodoApp
{
public class TodoItemDto
{
public Guid Id { get; set; }
public string Text { get; set; }
}
}
````
这是一个非常简单的DTO类, 它与我们的 `TodoItem` 实体相对应. 接下来, 我们准备实现 `ITodoAppService`接口.
### 应用服务实现
在 *TodoApp.Application* 项目中创建 `TodoAppService` 类, 如下所示:
````csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace TodoApp
{
public class TodoAppService : ApplicationService, ITodoAppService
{
private readonly IRepository<TodoItem, Guid> _todoItemRepository;
public TodoAppService(IRepository<TodoItem, Guid> todoItemRepository)
{
_todoItemRepository = todoItemRepository;
}
// TODO: Implement the methods here...
}
}
````
该类继承自ABP框架的`ApplicationService`类, 并实现了之前定义的 `ITodoAppService`接口. ABP为实体提供默认的泛型 [仓储](../../Repositories.md). 我们可以使用它们来执行基本的数据库操作. 此类中 [注入](../../Dependency-Injection.md) 的 `IRepository<TodoItem, Guid>`, 它就是 `TodoItem` 实体的默认存储库. 我们将使用它来实现之前描述的用例.
#### 获取待办事项
让我们先实现 `GetListAsync` 方法:
````csharp
public async Task<List<TodoItemDto>> GetListAsync()
{
var items = await _todoItemRepository.GetListAsync();
return items
.Select(item => new TodoItemDto
{
Id = item.Id,
Text = item.Text
}).ToList();
}
````
我们只是从数据库中获取完整的`TodoItem`列表, 将它们映射到`TodoItemDto`对象并作为结果返回.
#### 创建一个新的待办事项
下一个我们可以实现方法是 `CreateAsync` , 如下所示:
````csharp
public async Task<TodoItemDto> CreateAsync(string text)
{
var todoItem = await _todoItemRepository.InsertAsync(
new TodoItem {Text = text}
);
return new TodoItemDto
{
Id = todoItem.Id,
Text = todoItem.Text
};
}
````
仓储的 `InsertAsync` 方法将给定的`TodoItem`插入数据库, 并返回相同的`TodoItem` 对象. 它还设置了`Id`, 因此我们可以在返回对象上使用它. 我们只是通过从新的 `TodoItem` 实体创建和返回 `TodoItemDto`.
#### 删除待办事项
最后, 我们来实现 `DeleteAsync` 方法, 代码如下:
````csharp
public async Task DeleteAsync(Guid id)
{
await _todoItemRepository.DeleteAsync(id);
}
````
至此, 应用程序服务已准备好了让UI层来使用.
## 用户界面层
现在是在UI上显示待办事项的时候了! 在开始编写代码之前, 最好记住我们正在尝试构建的内容. 这里是示例程序的最终用户界面的截图:
![待办事项列表](todo-list.png)
> **我们将在本教程中保持最简洁的UI端, 以使本教程简单且重点突出. 请参阅 [web应用程序开发教程](../Part-1.md) 来了解构建实际应用各个方面.**
{{if UI=="MVC"}}
### Index.cshtml.cs
打开在 *TodoApp.Web* 项目的 `Pages` 文件夹中`Index.cshtml.cs`文件, 并用以下代码块替换它的默认内容:
````csharp
using System.Collections.Generic;
using System.Threading.Tasks;
namespace TodoApp.Web.Pages
{
public class IndexModel : TodoAppPageModel
{
public List<TodoItemDto> TodoItems { get; set; }
private readonly ITodoAppService _todoAppService;
public IndexModel(ITodoAppService todoAppService)
{
_todoAppService = todoAppService;
}
public async Task OnGetAsync()
{
TodoItems = await _todoAppService.GetListAsync();
}
}
}
````
此类使用 `ITodoAppService` 获取待办事项列表并将它赋值给 `TodoItems` 属性. 我们将用它来渲染razor页面上的待办事项目列表.
### Index.cshtml
打开 *TodoApp.Web* 项目中`Pages`文件夹下的`Index.cshtml`, 并替换为以下内容:
````xml
@page
@model TodoApp.Web.Pages.IndexModel
@section styles {
<abp-style src="/Pages/Index.css" />
}
@section scripts {
<abp-script src="/Pages/Index.js" />
}
<div class="container">
<abp-card>
<abp-card-header>
<abp-card-title>
TODO LIST
</abp-card-title>
</abp-card-header>
<abp-card-body>
<!-- FORM FOR NEW TODO ITEMS -->
<form id="NewItemForm" class="form-inline">
<input id="NewItemText"
type="text"
class="form-control mr-2"
placeholder="enter text...">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<!-- TODO ITEMS LIST -->
<ul id="TodoList">
@foreach (var todoItem in Model.TodoItems)
{
<li data-id="@todoItem.Id">
<i class="fa fa-trash-o"></i> @todoItem.Text
</li>
}
</ul>
</abp-card-body>
</abp-card>
</div>
````
我们使用 ABP 的 [卡片标签助手](../../UI/AspNetCore/Tag-Helpers/Cards.md) 来创建一个简单的卡片视图. 你可以直接使用标准引导HTML结构, 但ABP [标签助手](../../UI/AspNetCore/Tag-Helpers/Index.md) 使它更容易并且更安全.
此页面导入一个 CSS 和 JavaScript 文件, 所以我们需要创建它们.
### Index.js
打开 *TodoApp.Web* 项目中`Pages`文件夹下的`Index.js`, 并替换为以下内容:
````js
$(function () {
// DELETING ITEMS /////////////////////////////////////////
$('#TodoList').on('click', 'li i', function(){
var $li = $(this).parent();
var id = $li.attr('data-id');
todoApp.todo.delete(id).then(function(){
$li.remove();
abp.notify.info('Deleted the todo item.');
});
});
// CREATING NEW ITEMS /////////////////////////////////////
$('#NewItemForm').submit(function(e){
e.preventDefault();
var todoText = $('#NewItemText').val();
todoApp.todo.create(todoText).then(function(result){
$('<li data-id="' + result.id + '">')
.html('<i class="fa fa-trash-o"></i> ' + result.text)
.appendTo($('#TodoList'));
$('#NewItemText').val('');
});
});
});
````
在第一部分中, 我们注册了在待办事项旁边的删除图标的点击事件, 来删除服务器上的相关项目并在UI上显示通知. 此外, 我们会从 DOM 中移除已删除的项目, 所以我们不需要刷新页面.
在第二部分中, 我们在服务器上创建一个新的待办事项. 如果成功, 我们将操纵DOM来将新的 `<li>` 元素插入到待办事项列表. 这样我们不需要在创建新的待办事项后刷新整个页面.
这里有趣的部分是我们如何与服务器通信. 请参阅 *动态JavaScript代理和自动API控制器* 部分来了解其工作原理. 但是现在让我们继续并完成这个应用程序.
### Index.css
最后, 请打开 *TodoApp.Web* 项目的 `Pages` 文件夹中的 `Index.css` 文件, 并替换为以下内容:
````css
#TodoList{
list-style: none;
margin: 0;
padding: 0;
}
#TodoList li {
padding: 5px;
margin: 5px 0px;
border: 1px solid #cccccc;
background-color: #f5f5f5;
}
#TodoList li i
{
opacity: 0.5;
}
#TodoList li i:hover
{
opacity: 1;
color: #ff0000;
cursor: pointer;
}
````
这是待办事项页面的简单样式. 我们相信你可以做得更好 :)
现在, 你可以再次运行应用程序并看到结果.
### 动态JavaScript代理和自动API控制器
在 `Index.js` 文件中, 我们使用了 `todoApp.todo.delete(...)` 和 `todoApp.todo.create(...)` 方法来与服务器通信. 这些函数是由ABP框架动态创建的, 这要归功于 [动态JavaScript客户端代理](../../UI/AspNetCore/Dynamic-JavaScript-Proxies.md) 系统. 它们执行 HTTP API 到服务器调用并返回一个Promise对象, 这样你就可以使用 `then` 函数注册一个回调, 像之前所做的那样.
但是, 你可能会注意到我们还没有创建任何API控制器, 所以服务器如何处理这些请求? 这个问题为我们引出了ABP 框架的 [自动API控制器](../../API/Auto-API-Controllers.md) 功能. 它通过约定自动将应用程序服务转换为 API 控制器.
如果你在应用程序URL中输入 `/swagger` 来打开 [Swagger UI](https://swagger.io/tools/swagger-ui/) , 就会看到Todo API:
![todo-api](todo-api.png)
{{else if UI=="Blazor" || UI=="BlazorServer"}}
### Index.razor.cs
打开在*TodoApp.Blazor*项目的`Pages`文件夹中`Index.razor.cs`文件, 并替换为一下内容:
````csharp
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace TodoApp.Blazor.Pages
{
public partial class Index
{
[Inject]
private ITodoAppService TodoAppService { get; set; }
private List<TodoItemDto> TodoItems { get; set; } = new List<TodoItemDto>();
private string NewTodoText { get; set; }
protected async override Task OnInitializedAsync()
{
TodoItems = await TodoAppService.GetListAsync();
}
private async Task Create()
{
var result = await TodoAppService.CreateAsync(NewTodoText);
TodoItems.Add(result);
NewTodoText = null;
}
private async Task Delete(TodoItemDto todoItem)
{
await TodoAppService.DeleteAsync(todoItem.Id);
await Notify.Info("Deleted the todo item.");
TodoItems.Remove(todoItem);
}
}
}
````
此类使用 `ITodoAppService` 来执行待办事项的操作. 它在创建和删除操作后操纵 `TodoItems` 列表. 这样, 我们不需要从服务器刷新整个待办事项列表.
{{if UI=="Blazor"}}
请参阅下面的 *动态C#代理和自动API控制器* 部分, 来了解我们如何在浏览器上运行的 Blazor 应用程序中使用注入和应用服务接口的! 但是现在让我们继续并完成这个应用程序.
{{end # Blazor}}
### Index.razor
打开在 *TodoApp.Blazor* 项目的`Pages`文件夹中 `Index.razor`文件, 并替换为以下代码块内容:
````xml
@page "/"
@inherits TodoAppComponentBase
<div class="container">
<Card>
<CardHeader>
<CardTitle>
TODO LIST
</CardTitle>
</CardHeader>
<CardBody>
<!-- FORM FOR NEW TODO ITEMS -->
<form id="NewItemForm"
@onsubmit:preventDefault
@onsubmit="() => Create()"
class="form-inline">
<input type="text"
@bind-value="@NewTodoText"
class="form-control mr-2"
placeholder="enter text...">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<!-- TODO ITEMS LIST -->
<ul id="TodoList">
@foreach (var todoItem in TodoItems)
{
<li data-id="@todoItem.Id">
<i class="far fa-trash-alt"
@onclick="() => Delete(todoItem)"
></i> @todoItem.Text
</li>
}
</ul>
</CardBody>
</Card>
</div>
````
### Index.razor.css
最后, 请打开 *TodoApp.Blazor* 项目的 `Pages` 文件夹中的 `Index.razor.css`文件, 并替换为以下内容:
````css
#TodoList{
list-style: none;
margin: 0;
padding: 0;
}
#TodoList li {
padding: 5px;
margin: 5px 0px;
border: 1px solid #cccccc;
background-color: #f5f5f5;
}
#TodoList li i
{
opacity: 0.5;
}
#TodoList li i:hover
{
opacity: 1;
color: #ff0000;
cursor: pointer;
}
````
这是待办事项页面的简单样式. 我们相信你可以做得更好 :)
现在, 你可以再次运行应用程序来查看结果.
{{if UI=="Blazor"}}
### 动态C#代理和自动API控制器
在 `Index.razor.cs` 文件中, 我们已经注入(使用 `[Inject]` 特性)并像使用本地服务一样使用 `ITodoAppService`. 请记住, Blazor应用程序是在浏览器上运行, 但此应用程序服务的实现却是在服务器上运行的.
这个神奇的过程是由ABP框架的 [动态C#客户端代理](../../API/Dynamic-CSharp-API-Clients.md) 系统完成. 它使用标准的`HttpClient` 并执行HTTP API到远程服务器请求. 它还为我们处理所有标准任务, 包括授权、JSON序列化和异常处理.
但是, 你可能会问我们还没有创建任何API控制器, 那么服务器如何处理这些请求呢? 这个问题为我们引出了ABP 框架的 [自动API控制器](../../API/Auto-API-Controllers.md) 功能. 它通过约定自动将应用程序服务转换为 API 控制器.
如果你运行 `TodoApp.HttpApi.Host` 应用程序, 你可以看到Todo API:
![todo-api](todo-api.png)
{{end # Blazor}}
{{else if UI=="NG"}}
### 服务代理生成
ABP提供了一个便捷的功能来自动创建客户端服务, 以方便地使用由服务器提供的HTTP API.
你首先需要运行 `TodoApp.HttpApi.Host` 项目, 因为代理生成器从服务器应用程序读取API定义.
> **请注意**: IIS Express有一个问题: 它不允许从另一个进程连接到应用程序. 如果你使用的是Visual Studio, 请在 “运行” 按钮下拉列表中选择`TodoApp.HttpApi.Host` 来替代IIS Express, 如下图所示:
![运行时不使用 iisexpress](run-without-iisexpress.png)
当启动 `TodoApp.HttpApi.Host` 项目后, 在`angular`文件夹中打开一个命令行终端并输入以下命令:
````bash
abp generate-proxy
````
如果一切顺利, 它应该生成如下输出:
````bash
CREATE src/app/proxy/generate-proxy.json (170978 bytes)
CREATE src/app/proxy/README.md (1000 bytes)
CREATE src/app/proxy/todo.service.ts (794 bytes)
CREATE src/app/proxy/models.ts (66 bytes)
CREATE src/app/proxy/index.ts (58 bytes)
````
然后, 我们可以使用 `todoService` 来使用服务器端HTTP API, 就像我们将在下一节中做的那样.
### home.component.ts
打开 `/angular/src/app/home/home.component.ts` 文件, 并用以下代码块替换其内容:
````js
import { ToasterService } from '@abp/ng.theme.shared';
import { Component, OnInit } from '@angular/core';
import { TodoItemDto, TodoService } from '@proxy';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
todoItems: TodoItemDto[];
newTodoText: string;
constructor(
private todoService: TodoService,
private toasterService: ToasterService)
{ }
ngOnInit(): void {
this.todoService.getList().subscribe(response => {
this.todoItems = response;
});
}
create(): void{
this.todoService.create(this.newTodoText).subscribe((result) => {
this.todoItems = this.todoItems.concat(result);
this.newTodoText = null;
});
}
delete(id: string): void {
this.todoService.delete(id).subscribe(() => {
this.todoItems = this.todoItems.filter(item => item.id !== id);
this.toasterService.info('Deleted the todo item.');
});
}
}
````
我们已经实现了使用 `todoService` 来获取待办事项目列表, 并将返回值赋值给 `todoItems` 数组. 我们还添加了 `create` 和 `delete` 方法. 这些方法将在视图端使用.
### home.component.html
打开 `/angular/src/app/home/home.component.html` 文件, 并用以下代码块替换其内容:
````html
<div class="container">
<div class="card">
<div class="card-header">
<div class="card-title">TODO LIST</div>
</div>
<div class="card-body">
<!-- FORM FOR NEW TODO ITEMS -->
<form class="form-inline" (ngSubmit)="create()">
<input
name="NewTodoText"
type="text"
[(ngModel)]="newTodoText"
class="form-control mr-2"
placeholder="enter text..."
/>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<!-- TODO ITEMS LIST -->
<ul id="TodoList">
<li *ngFor="let todoItem of todoItems">
<i class="fa fa-trash-o" (click)="delete(todoItem.id)"></i> {%{{{ todoItem.text }}}%}
</li>
</ul>
</div>
</div>
</div>
````
### home.component.scss
最后, 打开`/angular/src/app/home/home.component.scss` 文件, 并用以下代码块替换其内容:
````css
#TodoList{
list-style: none;
margin: 0;
padding: 0;
}
#TodoList li {
padding: 5px;
margin: 5px 0px;
border: 1px solid #cccccc;
background-color: #f5f5f5;
}
#TodoList li i
{
opacity: 0.5;
}
#TodoList li i:hover
{
opacity: 1;
color: #ff0000;
cursor: pointer;
}
````
这是待办事项页面的简单样式. 我们相信你可以做得更好 :)
现在, 你可以再次运行应用程序来查看结果.
{{end}}
## 总结
在这个教程中, 我们已经建立了一个非常简单的应用程序来熟悉 ABP 框架. 如果你想要构建一个实际场景的应用程序, 请查看 [web 应用程序开发教程](../Part-1.md), 该教程涵盖了真实的 Web 应用程序开发的所有方面.
## 源代码
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/TodoApp)找到已完成的项目源代码.
## 另请参见
* [Web应用程序开发教程](../Part-1.md)

0
docs/zh-Hans/Tutorials/todo/run-without-iisexpress.png → docs/zh-Hans/Tutorials/Todo/run-without-iisexpress.png

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

0
docs/zh-Hans/Tutorials/todo/todo-api.png → docs/zh-Hans/Tutorials/Todo/todo-api.png

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/zh-Hans/Tutorials/Todo/todo-efcore-migration.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

0
docs/zh-Hans/Tutorials/todo/todo-list.png → docs/zh-Hans/Tutorials/Todo/todo-list.png

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

0
docs/zh-Hans/Tutorials/todo/todo-swagger-ui-initial.png → docs/zh-Hans/Tutorials/Todo/todo-swagger-ui-initial.png

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

0
docs/zh-Hans/Tutorials/todo/todo-ui-initial.png → docs/zh-Hans/Tutorials/Todo/todo-ui-initial.png

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

822
docs/zh-Hans/Tutorials/todo/Index.md

@ -1,822 +0,0 @@
# 快速入门
````json
//[doc-params]
{
"UI": ["MVC", "Blazor", "BlazorServer", "NG"],
"DB": ["EF", "Mongo"]
}
````
这是一个单独的部分,使用ABP框架构建简单待办事项应用程序的快速入门教程.这是一个最终应用的截图:
![todo-list](todo-list.png)
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/TodoApp)找到已完成的项目源代码.
## 先决条件
* 一个集成开发环境 (比如: [Visual Studio](https://visualstudio.microsoft.com/vs/)) 它需要支持 [.NET 5.0+](https://dotnet.microsoft.com/download/dotnet) 的开发.
{{if DB=="Mongo"}}
* [MongoDB Server 4.0+](https://docs.mongodb.com/manual/administration/install-community/)
{{end}}
{{if UI=="NG"}}
* [Node v14.x](https://nodejs.org/)
{{end}}
## 创建新的解决方案
我们将使用[ABP CLI](../../CLI.md) 创建带有ABP框架的新解决方案. 你可以在命令行终端中运行以下命令来安装它:
````bash
dotnet tool install -g Volo.Abp.Cli
````
然后创建一个空文件夹,打开命令行终端并以打开的文件夹为路径在终端中执行以下命令:
````bash
abp new TodoApp{{if UI=="Blazor"}} -u blazor{{else if UI=="BlazorServer"}} -u blazor-server{{else if UI=="NG"}} -u angular{{end}}{{if DB=="Mongo"}} -d mongodb{{end}}
````
{{if UI=="NG"}}
这将创建一个名为*TodoApp*的新解决方案,其中包含`angular`和`aspnet core`文件夹.解决方案就绪后,在你的IDE中打开ASP.NET Core 解决方案.
{{else}}
这将创建一个名为*TodoApp*的新解决方案.解决方案准备好后,在你的IDE中打开它.
{{end}}
### 创建数据库
如果你使用的是visual studio,请右键单击`TodoApp.DbMigrator`项目,选择*设置为启动项目*,然后按*Ctrl+F5*运行它而不进行调试.它将创建初始数据库并播种初始数据.
{{if DB=="EF"}}
> 由于*DbMigrator*添加初始迁移并重新编译项目,一些ide(例如Rider)在第一次运行时可能会出现问题.在这种情况下,请打开`.DbMigrator`项目文件夹中的命令行终端,然后执行`dotnet run`命令.
{{end}}
### 运行应用程序
{{if UI=="MVC" || UI=="BlazorServer"}}
最好在开始开发之前运行一下应用程序.确保 {{if UI=="BlazorServer"}}`TodoApp.Blazor`{{else}}`TodoApp.Web`{{end}} 是启动项目,然后运行应用程序(Visual Studio中是Ctrl+F5)以查看查看初始UI:
{{else if UI=="Blazor"}}
最好在开始开发之前运行一下应用程序.该解决方案有两个主要应用:
* `TodoApp.HttpApi.Host` 托管服务器端的 HTTP API.
* `TodoApp.Blazor` 客户端托管的 Blazor WebAssembly 应用.
确保 `TodoApp.HttpApi.Host` 是启动项目,然后运行应用程序(Visual Studio中的Ctrl+F5)以查看[Swagger UI](https://swagger.io/tools/swagger-ui/)上server-side 的 HTTP API:
![todo-swagger-ui-initial](todo-swagger-ui-initial.png)
你可以使用此UI查看和测试你的HTTP API.现在,我们可以将 `TodoApp.Blazor` 设置为启动项目,并运行它来打开实际的Blazor应用程序UI:
{{else if UI=="NG"}}
最好在开始开发之前运行应用程序.该解决方案有两个主要应用:
* `TodoApp.HttpApi.Host` (在.NET解决方案中)承载服务器端HTTP API.
* `angular` 文件夹包含 Angular 应用程序.
确保`TodoApp.HttpApi.Host`project是启动项目,然后运行应用程序(visual studio中是Ctrl+F5)以查看[Swagger UI](https://swagger.io/tools/swagger-ui/)上server-side HTTP API:
![todo-swagger-ui-initial](todo-swagger-ui-initial.png)
你可以使用这个UI查看和测试你的HTTP API.如果可以的话,我们可以运行Angular客户端应用程序.
首先,运行以下命令来还原NPM包:
````bash
npm install
````
安装所有软件包需要一些时间.然后可以使用以下命令运行应用程序:
````bash
npm start
````
此命令需要一点时间,但最终会在默认浏览器中运行并打开应用程序:
{{end}}
![todo-ui-initial](todo-ui-initial.png)
你可以单击 *登录* 按钮,以`admin`作为用户名,`1q2w3E*` 作为密码登录到应用程序.
一切就绪.我们可以开始编码了!
## 领域层
此应用程序只有一个 [实体](../../Entities.md) 我们开始创建它. 在*TodoApp.Domain*项目中创建一个新的 `TodoItem` 类:
````csharp
using System;
using Volo.Abp.Domain.Entities;
namespace TodoApp
{
public class TodoItem : BasicAggregateRoot<Guid>
{
public string Text { get; set; }
}
}
````
`BasicAggregateRoot` 是创建根实体的最简单的基类之一,`Guid` 是这里实体的主键 (`Id`).
## Database Integration 数据库集成
{{if DB=="EF"}}
下一步是配置 [Entity Framework Core](../../Entity-Framework-Core.md).
### Mapping Configuration 映射配置
在 *TodoApp.EntityFrameworkCore* 项目的 `EntityFrameworkCore` 文件夹中打开 `TodoAppDbContext` 类,并向该类添加一个新的 `DbSet` 属性:
````csharp
public DbSet<TodoItem> TodoItems { get; set; }
````
然后在同一文件夹中打开 `TodoAppDbContextModelCreatingExtensions` 类,并为 `TodoItem` 类添加映射配置,如下所示:
````csharp
public static void ConfigureTodoApp(this ModelBuilder builder)
{
Check.NotNull(builder, nameof(builder));
builder.Entity<TodoItem>(b =>
{
b.ToTable("TodoItems");
});
}
````
我们已经将 `TodoItem` 实体映射到数据库中的 `TodoItems` 表.
### 代码优先迁移
解决方案启动模版已经配置为使用Entity Framework Core [Code First 迁移](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations).由于我们已经更改了数据库映射配置,因此我们应该创建一个新的迁移并将更改应用于数据库.
在 *TodoApp.EntityFrameworkCore.DbMigrations* 项目的目录中打开一个命令行终端,然后键入以下命令:
````bash
dotnet ef migrations add Added_TodoItem
````
这将向项目添加一个新的迁移类:
![todo-efcore-migration](todo-efcore-migration.png)
你可以在同一命令行终端中使用以下命令将更改应用于数据库:
````bash
dotnet ef database update
````
> 如果你使用的是Visual Studio,*则可能希望在包管理器控制台 (PMC)* 中使用 `Add-Migration Added_TodoItem` 和 `Update-Database` 命令.在这种情况下,请确保 {{if UI=="MVC"}}`TodoApp.Web`{{else if UI=="BlazorServer"}}`TodoApp.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`TodoApp.HttpApi.Host`{{end}} 是启动项目,并且 `TodoApp.EntityFrameworkCore.DbMigrations` 是PMC中的 *默认项目*.
{{else if DB=="Mongo"}}
下一步是设置 [MongoDB](../MongoDB.md) 配置.在 *TodoApp.MongoDB* 项目的 `MongoDb` 文件夹中打开 `TodoAppMongoDbContext` 类,并进行以下更改;
1. 向类添加新属性:
````csharp
public IMongoCollection<TodoItem> TodoItems => Collection<TodoItem>();
````
2. 在 `CreateModel` 方法中添加以下代码:
````csharp
modelBuilder.Entity<TodoItem>(b =>
{
b.CollectionName = "TodoItems";
});
````
{{end}}
现在,我们可以使用ABP仓储来保存和检索待办事项列表项了,就像我们将在下一节中做的那样.
## 应用层
[应用服务](../../Application-Services.md) 用于执行应用程序的用例.我们需要执行以下用例;
* 获取待办事项列表
* 创建新的待办事项
* 删除现有的待办事项
### 应用服务接口
我们可以从为应用程序服务定义一个接口开始.在 *TodoApp.Application.Contracts* 项目中创建一个新的 `ITodoAppService` 接口,如下所示:
````csharp
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
namespace TodoApp
{
public interface ITodoAppService : IApplicationService
{
Task<List<TodoItemDto>> GetListAsync();
Task<TodoItemDto> CreateAsync(string text);
Task DeleteAsync(Guid id);
}
}
````
### 数据传输对象
`GetListAsync` 和 `CreateAsync` 方法返回 `TodoItemDto`. 通常应用服务获取或返回的是 DTO ([数据传输对象](../../Data-Transfer-Objects.md)) 而不是直接获取或返回实体对象. 所以,我们应该在此定义 DTO(数据传输对象) 类. 在 *TodoApp.Application.Contracts* 项目中创建一个名为 `TodoItemDto` 的类:
````csharp
using System;
namespace TodoApp
{
public class TodoItemDto
{
public Guid Id { get; set; }
public string Text { get; set; }
}
}
````
这是一个非常简单的DTO类,与我们的 `TodoItem` 实体相对应.我们接下来实现 `ITodoAppService` 接口.
### 应用服务实现
在 *TodoApp.Application* 项目内部创建一个 `TodoAppService` 类,如下所示:
````csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace TodoApp
{
public class TodoAppService : ApplicationService, ITodoAppService
{
private readonly IRepository<TodoItem, Guid> _todoItemRepository;
public TodoAppService(IRepository<TodoItem, Guid> todoItemRepository)
{
_todoItemRepository = todoItemRepository;
}
// TODO: Implement the methods here...
}
}
````
该类继承自ABP框架的 `ApplicationService` 类,并实现了之前定义的 `ITodoAppService`接口.ABP为实体提供默认的通用的 [仓储](../repositories.md).我们可以使用它们来执行基本的数据库操作. 这个类 [注入](../Dependency-Injection.md) `IRepository<TodoItem, Guid>` 它是 `TodoItem` 实体的默认存储库.我们将使用它来实现之前描述的用例.
#### 获取所有待办事项列表
让我们开始实现 `GetListAsync` 方法:
````csharp
public async Task<List<TodoItemDto>> GetListAsync()
{
var items = await _todoItemRepository.GetListAsync();
return items
.Select(item => new TodoItemDto
{
Id = item.Id,
Text = item.Text
}).ToList();
}
````
我们只是简单的从数据库中获取完整的 `TodoItem` 列表,将它们映射到 `TodoItemDto` 对象并作为结果返回.
#### 创建一个新的待办事项
下一个方法是 `CreateAsync`,我们可以像如下所示来实现:
````csharp
public async Task<TodoItemDto> CreateAsync(string text)
{
var todoItem = await _todoItemRepository.InsertAsync(
new TodoItem {Text = text}
);
return new TodoItemDto
{
Id = todoItem.Id,
Text = todoItem.Text
};
}
````
仓储的 `InsertAsync` 方法将给定的 `TodoItem` 插入数据库,并返回相同的 `TodoItem` 对象.它还设置了 `Id`,因此我们可以在返回对象上使用它.我们只是通过从新的 `TodoItem` 实体创建和返回 `TodoItemDto`.
#### 删除待办事项
最后,我们可以将 `DeleteAsync` 实现为以下代码块:
````csharp
public async Task DeleteAsync(Guid id)
{
await _todoItemRepository.DeleteAsync(id);
}
````
至此,应用程序服务已准备好了让UI层来使用.
## 用户界面层
是时候在UI上显示待办事项了!在开始编写代码之前,最好记住我们正在尝试构建的内容.这是最终UI的示例屏幕截图:
![todo-list](todo-list.png)
> **我们将在本教程中保持最简洁轻量的UI端,以使本教程简单且重点突出.请参阅[web应用程序开发教程](../Part-1.md) 以构建包含各个方面的真实页面.**
{{if UI=="MVC"}}
### Index.cshtml.cs
在 *TodoApp.Web* 项目的 `Pages` 文件夹中打开 `Index.cshtml.cs` 文件,并用以下代码块替换它的默认内容:
````csharp
using System.Collections.Generic;
using System.Threading.Tasks;
namespace TodoApp.Web.Pages
{
public class IndexModel : TodoAppPageModel
{
public List<TodoItemDto> TodoItems { get; set; }
private readonly ITodoAppService _todoAppService;
public IndexModel(ITodoAppService todoAppService)
{
_todoAppService = todoAppService;
}
public async Task OnGetAsync()
{
TodoItems = await _todoAppService.GetListAsync();
}
}
}
````
此类使用 `ITodoAppService` 获取待办事项目列表并将它赋值给 `TodoItems` 属性.我们将用它来渲染razor页面上的待办事项目列表.
### Index.cshtml
在 *TodoApp.Web* 项目的 `Pages` 文件夹中打开 `Index.cshtml` 文件,并替换为以下内容:
````xml
@page
@model TodoApp.Web.Pages.IndexModel
@section styles {
<abp-style src="/Pages/Index.css" />
}
@section scripts {
<abp-script src="/Pages/Index.js" />
}
<div class="container">
<abp-card>
<abp-card-header>
<abp-card-title>
TODO LIST
</abp-card-title>
</abp-card-header>
<abp-card-body>
<!-- FORM FOR NEW TODO ITEMS -->
<form id="NewItemForm" class="form-inline">
<input id="NewItemText"
type="text"
class="form-control mr-2"
placeholder="enter text...">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<!-- TODO ITEMS LIST -->
<ul id="TodoList">
@foreach (var todoItem in Model.TodoItems)
{
<li data-id="@todoItem.Id">
<i class="fa fa-trash-o"></i> @todoItem.Text
</li>
}
</ul>
</abp-card-body>
</abp-card>
</div>
````
我们使用ABP的 [卡片标签助手](../../UI/AspNetCore/Tag-Helpers/Cards.md) 创建一个简单的卡片视图.你可以直接使用标准的bootstrap HTML, 不过ABP的[标签助手](../../UI/AspNetCore/Tag-Helpers/Index.md) 使它更简单并且类型安全.
这个页面要导入一个CSS和一个JavaScript文件,所以我们也应该创建它们.
### Index.js
在 *TodoApp.Web* 项目的 `Pages` 文件夹中打开 `Index.js` 文件,并替换为以下内容:
````js
$(function () {
// DELETING ITEMS /////////////////////////////////////////
$('#TodoList').on('click', 'li i', function(){
var $li = $(this).parent();
var id = $li.attr('data-id');
todoApp.todo.delete(id).then(function(){
$li.remove();
abp.notify.info('Deleted the todo item.');
});
});
// CREATING NEW ITEMS /////////////////////////////////////
$('#NewItemForm').submit(function(e){
e.preventDefault();
var todoText = $('#NewItemText').val();
todoApp.todo.create(todoText).then(function(result){
$('<li data-id="' + result.id + '">')
.html('<i class="fa fa-trash-o"></i> ' + result.text)
.appendTo($('#TodoList'));
$('#NewItemText').val('');
});
});
});
````
在第一部分中,我们在待办事项目附近注册点击删除图标所要触发的事件,删除服务器上的相关待办事项并在UI上显示通知.另外,我们从DOM中删除已删除的项,因此我们不需要刷新页面.
在第二部分中,我们在服务器上创建一个新的待办事项.如果成功,我们将操纵DOM以将新的 `<li>` 元素插入todo列表.这样,创建新的待办事项后就不用刷新整个页面了.
这里有趣的部分是我们如何与服务器通信.请参阅 *动态JavaScript代理和自动API控制器* 部分,以了解其工作原理.但是现在让我们继续并完成这个应用程序.
### Index.css
来到最后要做的工作,请打开 *TodoApp.Web* 项目的 `Pages` 文件夹中的 `Index.css` 文件,并替换为以下内容:
````css
#TodoList{
list-style: none;
margin: 0;
padding: 0;
}
#TodoList li {
padding: 5px;
margin: 5px 0px;
border: 1px solid #cccccc;
background-color: #f5f5f5;
}
#TodoList li i
{
opacity: 0.5;
}
#TodoList li i:hover
{
opacity: 1;
color: #ff0000;
cursor: pointer;
}
````
这是待办事项页面的简单样式.我们相信你可以做得更好 :)
现在,你可以再次运行应用程序以查看结果.
### Dynamic JavaScript Proxies & Auto API Controllers 动态JavaScript代理和自动应用编程接口控制器
在 `Index.js` 文件中,我们使用了 `todoApp.todo.delete(...)` 和 `todoApp.todo.create(...)` 函数与服务器通信.这些函数是由ABP框架动态创建的,这要归功于 [动态JavaScript客户端代理](../UI/AspNetCore/Dynamic-JavaScript-Proxies.md) 系统.他们对服务器执行HTTP API调用并返回承诺,因此你可以像上面所做的那样注册对 `then` 函数的回调.
但是,你可能会问我们还没有创建任何API控制器,那么服务器如何处理这些请求呢? 这个问题为我们引出了ABP框架的 [自动API控制器](../API/Auto-API-Controllers.md) 功能.它通过约定自动将应用程序服务转换为API Controllers(API控制器).
如果通过在应用程序中输入 `/swagger` URL来打开 [swagger UI](https://swagger.io/tools/ Swagger-ui/),则可以看到Todo API:
![todo-api](todo-api.png)
{{else if UI=="Blazor" || UI=="BlazorServer"}}
### Index.razor.cs
在 *TodoApp.Blazor* 项目的 `Pages` 文件夹中打开 `Index.razor.cs` 文件,并用以下代码块替换内容:
````csharp
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace TodoApp.Blazor.Pages
{
public partial class Index
{
[Inject]
private ITodoAppService TodoAppService { get; set; }
private List<TodoItemDto> TodoItems { get; set; } = new List<TodoItemDto>();
private string NewTodoText { get; set; }
protected async override Task OnInitializedAsync()
{
TodoItems = await TodoAppService.GetListAsync();
}
private async Task Create()
{
var result = await TodoAppService.CreateAsync(NewTodoText);
TodoItems.Add(result);
NewTodoText = null;
}
private async Task Delete(TodoItemDto todoItem)
{
await TodoAppService.DeleteAsync(todoItem.Id);
await Notify.Info("Deleted the todo item.");
TodoItems.Remove(todoItem);
}
}
}
````
这个类使用 `ITodoAppService` 对待办事项执行操作.它在创建和删除操作后操纵 `TodoItems` 列表.这样,我们不需要从服务器刷新整个todo列表.
{{if UI=="Blazor"}}
请参阅下面的 *动态C # 代理和自动API控制器* 部分,了解如何从浏览器上运行的Blazor应用程序注入和使用应用程序服务接口!但是现在,让我们继续并完成这个应用.
{{end # Blazor}}
### Index.razor
在 *TodoApp.Blazor* 项目的 `Pages` 文件夹中打开 `Index.razor` 文件,并用以下代码块替换内容:
````xml
@page "/"
@inherits TodoAppComponentBase
<div class="container">
<Card>
<CardHeader>
<CardTitle>
TODO LIST
</CardTitle>
</CardHeader>
<CardBody>
<!-- FORM FOR NEW TODO ITEMS -->
<form id="NewItemForm"
@onsubmit:preventDefault
@onsubmit="() => Create()"
class="form-inline">
<input type="text"
@bind-value="@NewTodoText"
class="form-control mr-2"
placeholder="enter text...">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<!-- TODO ITEMS LIST -->
<ul id="TodoList">
@foreach (var todoItem in TodoItems)
{
<li data-id="@todoItem.Id">
<i class="far fa-trash-alt"
@onclick="() => Delete(todoItem)"
></i> @todoItem.Text
</li>
}
</ul>
</CardBody>
</Card>
</div>
````
### Index.razor.css
最后,请在 *TodoApp.Web* 项目的 `Pages` 文件夹中打开 `Index.razor.css` 文件,并替换为以下内容:
````css
#TodoList{
list-style: none;
margin: 0;
padding: 0;
}
#TodoList li {
padding: 5px;
margin: 5px 0px;
border: 1px solid #cccccc;
background-color: #f5f5f5;
}
#TodoList li i
{
opacity: 0.5;
}
#TodoList li i:hover
{
opacity: 1;
color: #ff0000;
cursor: pointer;
}
````
这是待办事项页面的简单样式.我们相信你可以做得更好 :)
现在,你可以再次运行应用程序以查看结果.
{{if UI=="Blazor"}}
### Dynamic C# Proxies & Auto API Controllers 动态C#代理和自动应用编程接口控制器
在 `Index.razor.cs` 文件中,我们已经注入(使用 `[Inject]` 特性)并像使用本地服务一样使用 `ITodoAppService`.请记住,当此应用程序服务的实例在服务器上运行的同时,Blazor应用程序正在浏览器上运行.
这个神奇的过程是由ABP框架的 [动态C#客户端代理](../API/Dynamic-CSharp-API-Clients.md) 系统完成的.它使用标准的 `HttpClient` 并向远程服务器执行HTTP API请求.它还为我们处理所有标准任务,包括授权,JSON序列化和异常处理.
但是,你可能会问我们还没有创建任何API控制器,那么服务器如何处理这些请求呢? 这个问题为我们引出了ABP框架的 [自动API控制器](../API/Auto-API-Controllers.md) 功能.它通过约定自动将应用程序服务转换为API控制器.
如果你运行 `TodoApp.HttpApi.Host` 应用程序,你可以看到Todo API:
![todo-api](todo-api.png)
{{end # Blazor}}
{{else if UI=="NG"}}
### 服务代理生成
ABP提供了一个方便的功能,可以自动创建客户端服务,轻松使用服务器提供的HTTP APIs.
你首先需要运行 `TodoApp.HttpApi.Host` 项目,因为代理生成器从服务器应用程序读取API定义.
> **请注意**: IIS Express有问题; 它不允许从另一个进程连接到应用程序.如果你使用的是Visual Studio,请在 “运行” 按钮下拉列表中选择 `TodoApp.HttpApi.Host` 而不是IIS Express,如下图所示:
![run-without-iisexpress](run-without-iisexpress.png)
运行 `TodoApp.HttpApi.Host` 项目后,在 `angular` 文件夹中打开命令行终端,然后键入以下命令:
````bash
abp generate-proxy
````
如果一切顺利,应该生成如下所示的输出:
````bash
CREATE src/app/proxy/generate-proxy.json (170978 bytes)
CREATE src/app/proxy/README.md (1000 bytes)
CREATE src/app/proxy/todo.service.ts (794 bytes)
CREATE src/app/proxy/models.ts (66 bytes)
CREATE src/app/proxy/index.ts (58 bytes)
````
然后,我们可以使用 `todoService` 来使用服务器端HTTP APIs,就像我们将在下一节中做的那样.
### home.component.ts
打开 `/angular/src/app/home/home.component.ts` 文件,并用以下代码块替换其内容:
````js
import { ToasterService } from '@abp/ng.theme.shared';
import { Component, OnInit } from '@angular/core';
import { TodoItemDto, TodoService } from '@proxy';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
todoItems: TodoItemDto[];
newTodoText: string;
constructor(
private todoService: TodoService,
private toasterService: ToasterService)
{ }
ngOnInit(): void {
this.todoService.getList().subscribe(response => {
this.todoItems = response;
});
}
create(): void{
this.todoService.create(this.newTodoText).subscribe((result) => {
this.todoItems = this.todoItems.concat(result);
this.newTodoText = null;
});
}
delete(id: string): void {
this.todoService.delete(id).subscribe(() => {
this.todoItems = this.todoItems.filter(item => item.id !== id);
this.toasterService.info('Deleted the todo item.');
});
}
}
````
我们已经实现了使用 `todoService` 来获取待办事项目列表,并将返回值赋值给 `todoItems` 数组.我们还添加了 `create`和 `delete` 方法.这些方法将在视图端使用.
### home.component.html
打开 `/angular/src/app/home/home.component.html` 文件,并用以下代码块替换其内容:
````html
<div class="container">
<div class="card">
<div class="card-header">
<div class="card-title">TODO LIST</div>
</div>
<div class="card-body">
<!-- FORM FOR NEW TODO ITEMS -->
<form class="form-inline" (ngSubmit)="create()">
<input
name="NewTodoText"
type="text"
[(ngModel)]="newTodoText"
class="form-control mr-2"
placeholder="enter text..."
/>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<!-- TODO ITEMS LIST -->
<ul id="TodoList">
<li *ngFor="let todoItem of todoItems">
<i class="fa fa-trash-o" (click)="delete(todoItem.id)"></i> {%{{{ todoItem.text }}}%}
</li>
</ul>
</div>
</div>
</div>
````
### home.component.scss
最后的最后,打开 `/angular/src/app/home/home.component.scss` 文件,并用以下代码块替换其内容:
````css
#TodoList{
list-style: none;
margin: 0;
padding: 0;
}
#TodoList li {
padding: 5px;
margin: 5px 0px;
border: 1px solid #cccccc;
background-color: #f5f5f5;
}
#TodoList li i
{
opacity: 0.5;
}
#TodoList li i:hover
{
opacity: 1;
color: #ff0000;
cursor: pointer;
}
````
这是待办事项页面的简单样式.我们相信你可以做得更好 :)
现在,你可以再次运行应用程序以查看结果.
{{end}}
## 总结
在本教程中,我们构建了一个非常简单的应用程序来抢先探究ABP框架的新特性.如果你想构建一个实际场景的应用程序,请查看 [应用程序开发教程](../Part-1.md),其中涵盖了实际web应用程序开发的所有方面.
## 源代码
你可以在[这里](https://github.com/abpframework/abp-samples/tree/master/TodoApp)获取到完整的应用程序源代码.
## 另请参见
* [Web应用程序开发教程](../Part-1.md)

BIN
docs/zh-Hans/Tutorials/todo/todo-efcore-migration.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

4
docs/zh-Hans/docs-nav.json

@ -1,5 +1,9 @@
{
"items": [
{
"text": "快速入门",
"path": "Tutorials/Todo/Index.md"
},
{
"text": "入门",
"items": [

2
docs/zh-Hans/docs-params.json

@ -5,6 +5,8 @@
"displayName": "UI",
"values": {
"MVC": "MVC / Razor Pages",
"Blazor": "Blazor WebAssembly",
"BlazorServer": "Blazor Server",
"NG": "Angular"
}
},

27
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53601/",
"sslPort": 44330
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Authentication.OAuth": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

1
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Volo.Abp.AspNetCore.Authentication.OAuth.csproj

@ -13,6 +13,7 @@
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<RootNamespace />
</PropertyGroup>

1
framework/src/Volo.Abp.AspNetCore.Components.Server.Theming/Volo.Abp.AspNetCore.Components.Server.Theming.csproj

@ -7,6 +7,7 @@
<TargetFramework>net5.0</TargetFramework>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
</PropertyGroup>
<ItemGroup>

1
framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj

@ -7,6 +7,7 @@
<TargetFramework>net5.0</TargetFramework>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<RootNamespace />
</PropertyGroup>

27
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53603/",
"sslPort": 44305
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Mvc.Client": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

1
framework/src/Volo.Abp.AspNetCore.Mvc.Client/Volo.Abp.AspNetCore.Mvc.Client.csproj

@ -13,6 +13,7 @@
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<RootNamespace />
</PropertyGroup>

10
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs

@ -108,14 +108,14 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
if (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase))
{
postedFiles.Add(new RemoteStreamContent(file.OpenReadStream())
{
ContentType = file.ContentType,
FileName = file.FileName
});
postedFiles.Add(new RemoteStreamContent(file.OpenReadStream(), file.FileName, file.ContentType, file.Length));
}
}
}
else if (bindingContext.IsTopLevelObject)
{
postedFiles.Add(new RemoteStreamContent(request.Body, null, request.ContentType, request.ContentLength));
}
}
}
}

9
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentOutputFormatter.cs

@ -33,14 +33,9 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = contentDisposition.ToString();
}
using (var stream = remoteStream.GetStream())
using (remoteStream)
{
if (stream.CanSeek)
{
stream.Position = 0;
}
await stream.CopyToAsync(context.HttpContext.Response.Body);
await remoteStream.GetStream().CopyToAsync(context.HttpContext.Response.Body);
}
}
}

27
framework/src/Volo.Abp.AspNetCore.Serilog/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53606/",
"sslPort": 44319
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.Serilog": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

1
framework/src/Volo.Abp.AspNetCore.Serilog/Volo.Abp.AspNetCore.Serilog.csproj

@ -13,6 +13,7 @@
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<RootNamespace />
</PropertyGroup>

27
framework/src/Volo.Abp.AspNetCore.SignalR/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53611/",
"sslPort": 44331
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.SignalR": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

1
framework/src/Volo.Abp.AspNetCore.SignalR/Volo.Abp.AspNetCore.SignalR.csproj

@ -13,6 +13,7 @@
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<RootNamespace />
</PropertyGroup>

27
framework/src/Volo.Abp.AspNetCore.TestBase/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53599/",
"sslPort": 44358
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore.TestBase": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

1
framework/src/Volo.Abp.AspNetCore.TestBase/Volo.Abp.AspNetCore.TestBase.csproj

@ -13,6 +13,7 @@
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<RootNamespace />
</PropertyGroup>

27
framework/src/Volo.Abp.AspNetCore/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53596/",
"sslPort": 44300
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.AspNetCore": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

1
framework/src/Volo.Abp.AspNetCore/Volo.Abp.AspNetCore.csproj

@ -13,6 +13,7 @@
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<RootNamespace />
</PropertyGroup>

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

@ -47,20 +47,12 @@ namespace Volo.Abp.Cli.Commands
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
{
var projectName = NamespaceHelper.NormalizeNamespace(commandLineArgs.Target);
if (projectName == null)
if (string.IsNullOrWhiteSpace(projectName))
{
throw new CliUsageException(
"Project name is missing!" +
Environment.NewLine + Environment.NewLine +
GetUsageInfo()
);
throw new CliUsageException("Project name is missing!" + Environment.NewLine + Environment.NewLine + GetUsageInfo());
}
if (!ProjectNameValidator.IsValid(projectName))
{
throw new CliUsageException("The project name is invalid! Please specify a different name.");
}
ProjectNameValidator.Validate(projectName);
Logger.LogInformation("Creating your project...");
Logger.LogInformation("Project name: " + projectName);

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

@ -244,7 +244,7 @@ namespace Volo.Abp.Cli.ProjectModification
foreach (var projectToRemove in projectsToRemove)
{
if (IsReferencedByAnotherModuleProject(moduleDirectory, projectsToRemove, projectToRemove))
if (IsReferencedByAnotherProject(solutionDirectory, projectsToRemove, projectToRemove))
{
continue;
}
@ -253,10 +253,10 @@ namespace Volo.Abp.Cli.ProjectModification
}
}
private bool IsReferencedByAnotherModuleProject(string moduleDirectory, List<string> projectsToRemove, string projectToRemove)
private bool IsReferencedByAnotherProject(string solutionDirectory, List<string> projectsToRemove, string projectToRemove)
{
var moduleProjects = Directory.GetFiles(moduleDirectory, "*.csproj", SearchOption.AllDirectories);
var projectsToKeep = moduleProjects.Where(mp => !projectsToRemove.Contains(Path.GetFileName(mp).RemovePostFix(".csproj"))).ToList();
var projects = Directory.GetFiles(solutionDirectory, "*.csproj", SearchOption.AllDirectories);
var projectsToKeep = projects.Where(mp => !projectsToRemove.Contains(Path.GetFileName(mp).RemovePostFix(".csproj"))).ToList();
return projectsToKeep.Select(File.ReadAllText).Any(content => content.Contains($"\"{projectToRemove}\""));
}

58
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/ProjectNameValidator.cs

@ -22,70 +22,50 @@ namespace Volo.Abp.Cli.Utils
"Blazor"
};
private static bool HasParentDirectoryString(string projectName)
private static void ValidateParentDirectoryString(string projectName)
{
return projectName.Contains("..");
if (projectName.Contains(".."))
{
throw new CliUsageException("Project name cannot contain \"..\"! Specify a different name.");
}
}
private static bool HasSurrogateOrControlChar(string projectName)
private static void ValidateSurrogateOrControlChar(string projectName)
{
return projectName.Any(chr => char.IsControl(chr) || char.IsSurrogate(chr));
if (projectName.Any(chr => char.IsControl(chr) || char.IsSurrogate(chr)))
{
throw new CliUsageException("Project name cannot contain surrogate or control characters! Specify a different name.");
}
}
private static bool IsIllegalProjectName(string projectName)
private static void ValidateIllegalProjectName(string projectName)
{
foreach (var illegalProjectName in IllegalProjectNames)
{
if (projectName.Equals(illegalProjectName, StringComparison.OrdinalIgnoreCase))
{
return true;
throw new CliUsageException("Project name cannot be \"" + illegalProjectName + "\"! Specify a different name.");
}
}
return false;
}
private static bool HasIllegalKeywords(string projectName)
private static void ValidateIllegalKeywords(string projectName)
{
foreach (var illegalKeyword in IllegalKeywords)
{
if (projectName.Contains(illegalKeyword))
{
return true;
throw new CliUsageException("Project name cannot contain the word \"" + illegalKeyword + "\". Specify a different name.");
}
}
return false;
}
public static bool IsValid(string projectName)
public static void Validate(string projectName)
{
if (projectName == null)
{
throw new CliUsageException("Project name cannot be empty!");
}
if (HasSurrogateOrControlChar(projectName))
{
return false;
}
if (HasParentDirectoryString(projectName))
{
return false;
}
if (IsIllegalProjectName(projectName))
{
return false;
}
if (HasIllegalKeywords(projectName))
{
return false;
}
return true;
ValidateSurrogateOrControlChar(projectName);
ValidateParentDirectoryString(projectName);
ValidateIllegalProjectName(projectName);
ValidateIllegalKeywords(projectName);
}
}
}

9
framework/src/Volo.Abp.Core/Volo/Abp/Content/IRemoteStreamContent.cs

@ -1,15 +1,16 @@
using System.IO;
using System;
using System.IO;
namespace Volo.Abp.Content
{
public interface IRemoteStreamContent
public interface IRemoteStreamContent : IDisposable
{
string FileName { get; }
string ContentType { get; }
long? ContentLength { get; }
string FileName { get; }
Stream GetStream();
}
}

36
framework/src/Volo.Abp.Core/Volo/Abp/Content/RemoteStreamContent.cs

@ -5,28 +5,40 @@ namespace Volo.Abp.Content
public class RemoteStreamContent : IRemoteStreamContent
{
private readonly Stream _stream;
private readonly bool _disposeStream;
private bool _disposed;
public RemoteStreamContent(Stream stream)
public virtual string FileName { get; }
public virtual string ContentType { get; } = "application/octet-stream";
public virtual long? ContentLength { get; }
public RemoteStreamContent(Stream stream, string fileName = null, string contentType = null, long? readOnlyLength = null, bool disposeStream = true)
{
_stream = stream;
}
public RemoteStreamContent(Stream stream, string fileName)
: this(stream)
{
FileName = fileName;
ContentType = "application/octet-stream";
if (contentType != null)
{
ContentType = contentType;
}
ContentLength = readOnlyLength ?? (_stream.CanSeek ? _stream.Length - _stream.Position : null);
_disposeStream = disposeStream;
}
public virtual string ContentType { get; set; }
public virtual long? ContentLength => GetStream().Length;
public virtual string FileName { get; set; }
public virtual Stream GetStream()
{
return _stream;
}
public virtual void Dispose()
{
if (!_disposed && _disposeStream)
{
_disposed = true;
_stream?.Dispose();
}
}
}
}

18
framework/src/Volo.Abp.Data/Volo/Abp/Data/ConcurrencyStampExtensions.cs

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using Volo.Abp.Domain.Entities;
using JetBrains.Annotations;
namespace Volo.Abp.Data
{
public static class ConcurrencyStampExtensions
{
public static void SetConcurrencyStampIfNotNull(this IHasConcurrencyStamp entity, [CanBeNull] string concurrencyStamp)
{
if (!concurrencyStamp.IsNullOrEmpty())
{
entity.ConcurrencyStamp = concurrencyStamp;
}
}
}
}

25
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/BasicAggregateRoot.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Volo.Abp.Uow;
namespace Volo.Abp.Domain.Entities
{
@ -9,15 +10,15 @@ namespace Volo.Abp.Domain.Entities
IAggregateRoot,
IGeneratesDomainEvents
{
private readonly ICollection<object> _distributedEvents = new Collection<object>();
private readonly ICollection<object> _localEvents = new Collection<object>();
private readonly ICollection<DomainEventRecord> _distributedEvents = new Collection<DomainEventRecord>();
private readonly ICollection<DomainEventRecord> _localEvents = new Collection<DomainEventRecord>();
public virtual IEnumerable<object> GetLocalEvents()
public virtual IEnumerable<DomainEventRecord> GetLocalEvents()
{
return _localEvents;
}
public virtual IEnumerable<object> GetDistributedEvents()
public virtual IEnumerable<DomainEventRecord> GetDistributedEvents()
{
return _distributedEvents;
}
@ -34,12 +35,12 @@ namespace Volo.Abp.Domain.Entities
protected virtual void AddLocalEvent(object eventData)
{
_localEvents.Add(eventData);
_localEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext()));
}
protected virtual void AddDistributedEvent(object eventData)
{
_distributedEvents.Add(eventData);
_distributedEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext()));
}
}
@ -48,8 +49,8 @@ namespace Volo.Abp.Domain.Entities
IAggregateRoot<TKey>,
IGeneratesDomainEvents
{
private readonly ICollection<object> _distributedEvents = new Collection<object>();
private readonly ICollection<object> _localEvents = new Collection<object>();
private readonly ICollection<DomainEventRecord> _distributedEvents = new Collection<DomainEventRecord>();
private readonly ICollection<DomainEventRecord> _localEvents = new Collection<DomainEventRecord>();
protected BasicAggregateRoot()
{
@ -62,12 +63,12 @@ namespace Volo.Abp.Domain.Entities
}
public virtual IEnumerable<object> GetLocalEvents()
public virtual IEnumerable<DomainEventRecord> GetLocalEvents()
{
return _localEvents;
}
public virtual IEnumerable<object> GetDistributedEvents()
public virtual IEnumerable<DomainEventRecord> GetDistributedEvents()
{
return _distributedEvents;
}
@ -84,12 +85,12 @@ namespace Volo.Abp.Domain.Entities
protected virtual void AddLocalEvent(object eventData)
{
_localEvents.Add(eventData);
_localEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext()));
}
protected virtual void AddDistributedEvent(object eventData)
{
_distributedEvents.Add(eventData);
_distributedEvents.Add(new DomainEventRecord(eventData, EventOrderGenerator.GetNext()));
}
}
}

15
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/DomainEventRecord.cs

@ -0,0 +1,15 @@
namespace Volo.Abp.Domain.Entities
{
public class DomainEventRecord
{
public object EventData { get; }
public long EventOrder { get; }
public DomainEventRecord(object eventData, long eventOrder)
{
EventData = eventData;
EventOrder = eventOrder;
}
}
}

5
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/DomainEventEntry.cs

@ -8,11 +8,14 @@ namespace Volo.Abp.Domain.Entities.Events
public object SourceEntity { get; }
public object EventData { get; }
public long EventOrder { get; }
public DomainEventEntry(object sourceEntity, object eventData)
public DomainEventEntry(object sourceEntity, object eventData, long eventOrder)
{
SourceEntity = sourceEntity;
EventData = eventData;
EventOrder = eventOrder;
}
}
}

260
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeEventHelper.cs

@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Volo.Abp.Auditing;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Entities.Events.Distributed;
using Volo.Abp.DynamicProxy;
@ -21,6 +18,8 @@ namespace Volo.Abp.Domain.Entities.Events
/// </summary>
public class EntityChangeEventHelper : IEntityChangeEventHelper, ITransientDependency
{
private const string UnitOfWorkEventRecordEntityPropName = "_Abp_Entity";
public ILogger<EntityChangeEventHelper> Logger { get; set; }
public ILocalEventBus LocalEventBus { get; set; }
public IDistributedEventBus DistributedEventBus { get; set; }
@ -43,37 +42,25 @@ namespace Volo.Abp.Domain.Entities.Events
Logger = NullLogger<EntityChangeEventHelper>.Instance;
}
public async Task TriggerEventsAsync(EntityChangeReport changeReport)
public virtual void PublishEntityCreatingEvent(object entity)
{
await TriggerEventsInternalAsync(changeReport);
if (changeReport.IsEmpty() || UnitOfWorkManager.Current == null)
{
return;
}
await UnitOfWorkManager.Current.SaveChangesAsync();
}
public virtual async Task TriggerEntityCreatingEventAsync(object entity)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
LocalEventBus,
#pragma warning disable 618
typeof(EntityCreatingEventData<>),
#pragma warning restore 618
entity,
entity,
true
entity
);
}
public virtual async Task TriggerEntityCreatedEventOnUowCompletedAsync(object entity)
public virtual void PublishEntityCreatedEvent(object entity)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
LocalEventBus,
typeof(EntityCreatedEventData<>),
entity,
entity,
false
entity
);
if (ShouldPublishDistributedEventForEntity(entity))
@ -81,12 +68,11 @@ namespace Volo.Abp.Domain.Entities.Events
var eto = EntityToEtoMapper.Map(entity);
if (eto != null)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
DistributedEventBus,
typeof(EntityCreatedEto<>),
eto,
entity,
false
entity
);
}
}
@ -103,25 +89,25 @@ namespace Volo.Abp.Domain.Entities.Events
);
}
public virtual async Task TriggerEntityUpdatingEventAsync(object entity)
public virtual void PublishEntityUpdatingEvent(object entity)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
LocalEventBus,
#pragma warning disable 618
typeof(EntityUpdatingEventData<>),
#pragma warning restore 618
entity,
entity,
true
entity
);
}
public virtual async Task TriggerEntityUpdatedEventOnUowCompletedAsync(object entity)
public virtual void PublishEntityUpdatedEvent(object entity)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
LocalEventBus,
typeof(EntityUpdatedEventData<>),
entity,
entity,
false
entity
);
if (ShouldPublishDistributedEventForEntity(entity))
@ -129,36 +115,35 @@ namespace Volo.Abp.Domain.Entities.Events
var eto = EntityToEtoMapper.Map(entity);
if (eto != null)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
DistributedEventBus,
typeof(EntityUpdatedEto<>),
eto,
entity,
false
entity
);
}
}
}
public virtual async Task TriggerEntityDeletingEventAsync(object entity)
public virtual void PublishEntityDeletingEvent(object entity)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
LocalEventBus,
#pragma warning disable 618
typeof(EntityDeletingEventData<>),
#pragma warning restore 618
entity,
entity,
true
entity
);
}
public virtual async Task TriggerEntityDeletedEventOnUowCompletedAsync(object entity)
public virtual void PublishEntityDeletedEvent(object entity)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
LocalEventBus,
typeof(EntityDeletedEventData<>),
entity,
entity,
false
entity
);
if (ShouldPublishDistributedEventForEntity(entity))
@ -166,183 +151,78 @@ namespace Volo.Abp.Domain.Entities.Events
var eto = EntityToEtoMapper.Map(entity);
if (eto != null)
{
await TriggerEventWithEntity(
TriggerEventWithEntity(
DistributedEventBus,
typeof(EntityDeletedEto<>),
eto,
entity,
false
entity
);
}
}
}
protected virtual async Task TriggerEventsInternalAsync(EntityChangeReport changeReport)
{
await TriggerEntityChangeEvents(changeReport.ChangedEntities);
await TriggerLocalEvents(changeReport.DomainEvents);
await TriggerDistributedEvents(changeReport.DistributedEvents);
}
protected virtual async Task TriggerEntityChangeEvents(List<EntityChangeEntry> changedEntities)
{
foreach (var changedEntity in changedEntities)
{
switch (changedEntity.ChangeType)
{
case EntityChangeType.Created:
await TriggerEntityCreatingEventAsync(changedEntity.Entity);
await TriggerEntityCreatedEventOnUowCompletedAsync(changedEntity.Entity);
break;
case EntityChangeType.Updated:
await TriggerEntityUpdatingEventAsync(changedEntity.Entity);
await TriggerEntityUpdatedEventOnUowCompletedAsync(changedEntity.Entity);
break;
case EntityChangeType.Deleted:
await TriggerEntityDeletingEventAsync(changedEntity.Entity);
await TriggerEntityDeletedEventOnUowCompletedAsync(changedEntity.Entity);
break;
default:
throw new AbpException("Unknown EntityChangeType: " + changedEntity.ChangeType);
}
}
}
protected virtual async Task TriggerLocalEvents(List<DomainEventEntry> localEvents)
{
foreach (var localEvent in localEvents)
{
await LocalEventBus.PublishAsync(localEvent.EventData.GetType(), localEvent.EventData);
}
}
protected virtual async Task TriggerDistributedEvents(List<DomainEventEntry> distributedEvents)
{
foreach (var distributedEvent in distributedEvents)
{
await DistributedEventBus.PublishAsync(distributedEvent.EventData.GetType(),
distributedEvent.EventData);
}
}
protected virtual async Task TriggerEventWithEntity(
protected virtual void TriggerEventWithEntity(
IEventBus eventPublisher,
Type genericEventType,
object entityOrEto,
object originalEntity,
bool triggerInCurrentUnitOfWork)
object originalEntity)
{
var entityType = ProxyHelper.UnProxy(entityOrEto).GetType();
var eventType = genericEventType.MakeGenericType(entityType);
var eventData = Activator.CreateInstance(eventType, entityOrEto);
var currentUow = UnitOfWorkManager.Current;
if (triggerInCurrentUnitOfWork || currentUow == null)
if (currentUow == null)
{
await eventPublisher.PublishAsync(
eventType,
Activator.CreateInstance(eventType, entityOrEto)
);
Logger.LogWarning("UnitOfWorkManager.Current is null! Can not publish the event.");
return;
}
var eventList = GetEventList(currentUow);
var isFirstEvent = !eventList.Any();
eventList.AddUniqueEvent(eventPublisher, eventType, entityOrEto, originalEntity);
/* Register to OnCompleted if this is the first item.
* Other items will already be in the list once the UOW completes.
var eventRecord = new UnitOfWorkEventRecord(eventType, eventData, EventOrderGenerator.GetNext())
{
Properties =
{
{ UnitOfWorkEventRecordEntityPropName, originalEntity },
}
};
/* We are trying to eliminate same events for the same entity.
* In this way, for example, we don't trigger update event for an entity multiple times
* even if it is updated multiple times in the current UOW.
*/
if (isFirstEvent)
if (eventPublisher == DistributedEventBus)
{
currentUow.OnCompleted(
async () =>
{
foreach (var eventEntry in eventList)
{
try
{
await eventEntry.EventBus.PublishAsync(
eventEntry.EventType,
Activator.CreateInstance(eventEntry.EventType, eventEntry.EntityOrEto)
);
}
catch (Exception ex)
{
Logger.LogError(
$"Caught an exception while publishing the event '{eventType.FullName}' for the entity '{entityOrEto}'");
Logger.LogException(ex);
}
}
}
currentUow.AddOrReplaceDistributedEvent(
eventRecord,
otherRecord => IsSameEntityEventRecord(eventRecord, otherRecord)
);
}
}
private EntityChangeEventList GetEventList(IUnitOfWork currentUow)
{
return (EntityChangeEventList) currentUow.Items.GetOrAdd(
"AbpEntityChangeEventList",
() => new EntityChangeEventList()
);
}
private class EntityChangeEventList : List<EntityChangeEventEntry>
{
public void AddUniqueEvent(IEventBus eventBus, Type eventType, object entityOrEto, object originalEntity)
else
{
var newEntry = new EntityChangeEventEntry(eventBus, eventType, entityOrEto, originalEntity);
//Latest "same" event overrides the previous events.
for (var i = 0; i < Count; i++)
{
if (this[i].IsSameEvent(newEntry))
{
this[i] = newEntry;
return;
}
}
//If this is a "new" event, add to the end
Add(newEntry);
currentUow.AddOrReplaceLocalEvent(
eventRecord,
otherRecord => IsSameEntityEventRecord(eventRecord, otherRecord)
);
}
}
private class EntityChangeEventEntry
public bool IsSameEntityEventRecord(UnitOfWorkEventRecord record1, UnitOfWorkEventRecord record2)
{
public IEventBus EventBus { get; }
public Type EventType { get; }
public object EntityOrEto { get; }
public object OriginalEntity { get; }
public EntityChangeEventEntry(IEventBus eventBus, Type eventType, object entityOrEto, object originalEntity)
if (record1.EventType != record2.EventType)
{
EventType = eventType;
EntityOrEto = entityOrEto;
OriginalEntity = originalEntity;
EventBus = eventBus;
return false;
}
public bool IsSameEvent(EntityChangeEventEntry otherEntry)
{
if (EventBus != otherEntry.EventBus || EventType != otherEntry.EventType)
{
return false;
}
var originalEntityRef = OriginalEntity as IEntity;
var otherOriginalEntityRef = otherEntry.OriginalEntity as IEntity;
if (originalEntityRef == null || otherOriginalEntityRef == null)
{
return false;
}
var record1OriginalEntity = record1.Properties.GetOrDefault(UnitOfWorkEventRecordEntityPropName) as IEntity;
var record2OriginalEntity = record2.Properties.GetOrDefault(UnitOfWorkEventRecordEntityPropName) as IEntity;
return EntityHelper.EntityEquals(originalEntityRef, otherOriginalEntityRef);
if (record1OriginalEntity == null || record2OriginalEntity == null)
{
return false;
}
return EntityHelper.EntityEquals(record1OriginalEntity, record2OriginalEntity);
}
}
}

32
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangeReport.cs

@ -1,32 +0,0 @@
using System.Collections.Generic;
namespace Volo.Abp.Domain.Entities.Events
{
public class EntityChangeReport
{
public List<EntityChangeEntry> ChangedEntities { get; }
public List<DomainEventEntry> DomainEvents { get; }
public List<DomainEventEntry> DistributedEvents { get; }
public EntityChangeReport()
{
ChangedEntities = new List<EntityChangeEntry>();
DomainEvents = new List<DomainEventEntry>();
DistributedEvents = new List<DomainEventEntry>();
}
public bool IsEmpty()
{
return ChangedEntities.Count <= 0 &&
DomainEvents.Count <= 0 &&
DistributedEvents.Count <= 0;
}
public override string ToString()
{
return $"[EntityChangeReport] ChangedEntities: {ChangedEntities.Count}, DomainEvents: {DomainEvents.Count}, DistributedEvents: {DistributedEvents.Count}";
}
}
}

1
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityChangingEventData.cs

@ -8,6 +8,7 @@ namespace Volo.Abp.Domain.Entities.Events
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
[Serializable]
[Obsolete("This event is no longer needed and identical to EntityChangedEventData. Please use EntityChangedEventData instead.")]
public class EntityChangingEventData<TEntity> : EntityEventData<TEntity>
{
/// <summary>

1
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityCreatingEventData.cs

@ -7,6 +7,7 @@ namespace Volo.Abp.Domain.Entities.Events
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
[Serializable]
[Obsolete("This event is no longer needed and identical to EntityCreatedEventData. Please use EntityCreatedEventData instead.")]
public class EntityCreatingEventData<TEntity> : EntityChangingEventData<TEntity>
{
/// <summary>

1
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityDeletingEventData.cs

@ -7,6 +7,7 @@ namespace Volo.Abp.Domain.Entities.Events
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
[Serializable]
[Obsolete("This event is no longer needed and identical to EntityDeleteEventData. Please use EntityDeleteEventData instead.")]
public class EntityDeletingEventData<TEntity> : EntityChangingEventData<TEntity>
{
/// <summary>

22
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityEventReport.cs

@ -0,0 +1,22 @@
using System.Collections.Generic;
namespace Volo.Abp.Domain.Entities.Events
{
public class EntityEventReport
{
public List<DomainEventEntry> DomainEvents { get; }
public List<DomainEventEntry> DistributedEvents { get; }
public EntityEventReport()
{
DomainEvents = new List<DomainEventEntry>();
DistributedEvents = new List<DomainEventEntry>();
}
public override string ToString()
{
return $"[{nameof(EntityEventReport)}] DomainEvents: {DomainEvents.Count}, DistributedEvents: {DistributedEvents.Count}";
}
}
}

1
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/EntityUpdatingEventData.cs

@ -7,6 +7,7 @@ namespace Volo.Abp.Domain.Entities.Events
/// </summary>
/// <typeparam name="TEntity">Entity type</typeparam>
[Serializable]
[Obsolete("This event is no longer needed and identical to EntityUpdatedEventData. Please use EntityUpdatedEventData instead.")]
public class EntityUpdatingEventData<TEntity> : EntityChangingEventData<TEntity>
{
/// <summary>

16
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/IEntityChangeEventHelper.cs

@ -1,5 +1,3 @@
using System.Threading.Tasks;
namespace Volo.Abp.Domain.Entities.Events
{
/// <summary>
@ -7,15 +5,13 @@ namespace Volo.Abp.Domain.Entities.Events
/// </summary>
public interface IEntityChangeEventHelper
{
Task TriggerEventsAsync(EntityChangeReport changeReport);
Task TriggerEntityCreatingEventAsync(object entity);
Task TriggerEntityCreatedEventOnUowCompletedAsync(object entity);
void PublishEntityCreatingEvent(object entity);
void PublishEntityCreatedEvent(object entity);
Task TriggerEntityUpdatingEventAsync(object entity);
Task TriggerEntityUpdatedEventOnUowCompletedAsync(object entity);
void PublishEntityUpdatingEvent(object entity);
void PublishEntityUpdatedEvent(object entity);
Task TriggerEntityDeletingEventAsync(object entity);
Task TriggerEntityDeletedEventOnUowCompletedAsync(object entity);
void PublishEntityDeletingEvent(object entity);
void PublishEntityDeletedEvent(object entity);
}
}

41
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/NullEntityChangeEventHelper.cs

@ -1,5 +1,3 @@
using System.Threading.Tasks;
namespace Volo.Abp.Domain.Entities.Events
{
/// <summary>
@ -14,57 +12,30 @@ namespace Volo.Abp.Domain.Entities.Events
private NullEntityChangeEventHelper()
{
}
public Task TriggerEntityCreatingEventAsync(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEntityCreatedEventAsync(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEntityCreatedEventOnUowCompletedAsync(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEntityUpdatingEventAsync(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEntityUpdatedEventAsync(object entity)
public void PublishEntityCreatingEvent(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEntityUpdatedEventOnUowCompletedAsync(object entity)
public void PublishEntityCreatedEvent(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEntityDeletingEventAsync(object entity)
public void PublishEntityUpdatingEvent(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEntityDeletedEventAsync(object entity)
public void PublishEntityUpdatedEvent(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEntityDeletedEventOnUowCompletedAsync(object entity)
public void PublishEntityDeletingEvent(object entity)
{
return Task.CompletedTask;
}
public Task TriggerEventsAsync(EntityChangeReport changeReport)
public void PublishEntityDeletedEvent(object entity)
{
return Task.CompletedTask;
}
}
}

4
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/IGeneratesDomainEvents.cs

@ -6,9 +6,9 @@ namespace Volo.Abp.Domain.Entities
public interface IGeneratesDomainEvents
{
IEnumerable<object> GetLocalEvents();
IEnumerable<DomainEventRecord> GetLocalEvents();
IEnumerable<object> GetDistributedEvents();
IEnumerable<DomainEventRecord> GetDistributedEvents();
void ClearLocalEvents();

176
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

@ -22,6 +22,8 @@ using Volo.Abp.Domain.Repositories;
using Volo.Abp.EntityFrameworkCore.EntityHistory;
using Volo.Abp.EntityFrameworkCore.Modeling;
using Volo.Abp.EntityFrameworkCore.ValueConverters;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Guids;
using Volo.Abp.MultiTenancy;
using Volo.Abp.ObjectExtending;
@ -59,6 +61,10 @@ namespace Volo.Abp.EntityFrameworkCore
public IUnitOfWorkManager UnitOfWorkManager => LazyServiceProvider.LazyGetRequiredService<IUnitOfWorkManager>();
public IClock Clock => LazyServiceProvider.LazyGetRequiredService<IClock>();
public IDistributedEventBus DistributedEventBus => LazyServiceProvider.LazyGetRequiredService<IDistributedEventBus>();
public ILocalEventBus LocalEventBus => LazyServiceProvider.LazyGetRequiredService<ILocalEventBus>();
public ILogger<AbpDbContext<TDbContext>> Logger => LazyServiceProvider.LazyGetService<ILogger<AbpDbContext<TDbContext>>>(NullLogger<AbpDbContext<TDbContext>>.Instance);
@ -150,20 +156,21 @@ namespace Volo.Abp.EntityFrameworkCore
try
{
var auditLog = AuditingManager?.Current?.Log;
List<EntityChangeInfo> entityChangeList = null;
if (auditLog != null)
{
entityChangeList = EntityHistoryHelper.CreateChangeList(ChangeTracker.Entries().ToList());
}
var changeReport = ApplyAbpConcepts();
ApplyAbpConcepts();
var eventReport = CreateEventReport();
var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
await EntityChangeEventHelper.TriggerEventsAsync(changeReport);
if (auditLog != null)
PublishEntityEvents(eventReport);
if (entityChangeList != null)
{
EntityHistoryHelper.UpdateChangeList(entityChangeList);
auditLog.EntityChanges.AddRange(entityChangeList);
@ -182,6 +189,23 @@ namespace Volo.Abp.EntityFrameworkCore
}
}
private void PublishEntityEvents(EntityEventReport changeReport)
{
foreach (var localEvent in changeReport.DomainEvents)
{
UnitOfWorkManager.Current?.AddOrReplaceLocalEvent(
new UnitOfWorkEventRecord(localEvent.EventData.GetType(), localEvent.EventData, localEvent.EventOrder)
);
}
foreach (var distributedEvent in changeReport.DistributedEvents)
{
UnitOfWorkManager.Current?.AddOrReplaceDistributedEvent(
new UnitOfWorkEventRecord(distributedEvent.EventData.GetType(), distributedEvent.EventData, distributedEvent.EventOrder)
);
}
}
/// <summary>
/// This method will call the DbContext <see cref="SaveChangesAsync(bool, CancellationToken)"/> method directly of EF Core, which doesn't apply concepts of abp.
/// </summary>
@ -202,11 +226,18 @@ namespace Volo.Abp.EntityFrameworkCore
ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;
ChangeTracker.Tracked += ChangeTracker_Tracked;
ChangeTracker.StateChanged += ChangeTracker_StateChanged;
}
protected virtual void ChangeTracker_Tracked(object sender, EntityTrackedEventArgs e)
{
FillExtraPropertiesForTrackedEntities(e);
PublishEventsForTrackedEntity(e.Entry);
}
protected virtual void ChangeTracker_StateChanged(object sender, EntityStateChangedEventArgs e)
{
PublishEventsForTrackedEntity(e.Entry);
}
protected virtual void FillExtraPropertiesForTrackedEntities(EntityTrackedEventArgs e)
@ -254,37 +285,107 @@ namespace Volo.Abp.EntityFrameworkCore
}
}
}
protected virtual EntityChangeReport ApplyAbpConcepts()
private void PublishEventsForTrackedEntity(EntityEntry entry)
{
var changeReport = new EntityChangeReport();
switch (entry.State)
{
case EntityState.Added:
EntityChangeEventHelper.PublishEntityCreatingEvent(entry.Entity);
EntityChangeEventHelper.PublishEntityCreatedEvent(entry.Entity);
break;
case EntityState.Modified:
if (entry.Properties.Any(x => x.IsModified && x.Metadata.ValueGenerated == ValueGenerated.Never))
{
if (entry.Entity is ISoftDelete && entry.Entity.As<ISoftDelete>().IsDeleted)
{
EntityChangeEventHelper.PublishEntityDeletingEvent(entry.Entity);
EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity);
}
else
{
EntityChangeEventHelper.PublishEntityUpdatingEvent(entry.Entity);
EntityChangeEventHelper.PublishEntityUpdatedEvent(entry.Entity);
}
}
break;
case EntityState.Deleted:
EntityChangeEventHelper.PublishEntityDeletingEvent(entry.Entity);
EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity);
break;
}
}
protected virtual void ApplyAbpConcepts()
{
foreach (var entry in ChangeTracker.Entries().ToList())
{
ApplyAbpConcepts(entry, changeReport);
ApplyAbpConcepts(entry);
}
return changeReport;
}
protected virtual EntityEventReport CreateEventReport()
{
var eventReport = new EntityEventReport();
foreach (var entry in ChangeTracker.Entries().ToList())
{
var generatesDomainEventsEntity = entry.Entity as IGeneratesDomainEvents;
if (generatesDomainEventsEntity == null)
{
continue;
}
protected virtual void ApplyAbpConcepts(EntityEntry entry, EntityChangeReport changeReport)
var localEvents = generatesDomainEventsEntity.GetLocalEvents()?.ToArray();
if (localEvents != null && localEvents.Any())
{
eventReport.DomainEvents.AddRange(
localEvents.Select(
eventRecord => new DomainEventEntry(
entry.Entity,
eventRecord.EventData,
eventRecord.EventOrder
)
)
);
generatesDomainEventsEntity.ClearLocalEvents();
}
var distributedEvents = generatesDomainEventsEntity.GetDistributedEvents()?.ToArray();
if (distributedEvents != null && distributedEvents.Any())
{
eventReport.DistributedEvents.AddRange(
distributedEvents.Select(
eventRecord => new DomainEventEntry(
entry.Entity,
eventRecord.EventData,
eventRecord.EventOrder)
)
);
generatesDomainEventsEntity.ClearDistributedEvents();
}
}
return eventReport;
}
protected virtual void ApplyAbpConcepts(EntityEntry entry)
{
switch (entry.State)
{
case EntityState.Added:
ApplyAbpConceptsForAddedEntity(entry, changeReport);
ApplyAbpConceptsForAddedEntity(entry);
break;
case EntityState.Modified:
ApplyAbpConceptsForModifiedEntity(entry, changeReport);
ApplyAbpConceptsForModifiedEntity(entry);
break;
case EntityState.Deleted:
ApplyAbpConceptsForDeletedEntity(entry, changeReport);
ApplyAbpConceptsForDeletedEntity(entry);
break;
}
HandleExtraPropertiesOnSave(entry);
AddDomainEvents(changeReport, entry.Entity);
}
protected virtual void HandleExtraPropertiesOnSave(EntityEntry entry)
@ -361,15 +462,14 @@ namespace Volo.Abp.EntityFrameworkCore
}
}
protected virtual void ApplyAbpConceptsForAddedEntity(EntityEntry entry, EntityChangeReport changeReport)
protected virtual void ApplyAbpConceptsForAddedEntity(EntityEntry entry)
{
CheckAndSetId(entry);
SetConcurrencyStampIfNull(entry);
SetCreationAuditProperties(entry);
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Created));
}
protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, EntityChangeReport changeReport)
protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry)
{
if (entry.State == EntityState.Modified && entry.Properties.Any(x => x.IsModified && x.Metadata.ValueGenerated == ValueGenerated.Never))
{
@ -379,24 +479,17 @@ namespace Volo.Abp.EntityFrameworkCore
if (entry.Entity is ISoftDelete && entry.Entity.As<ISoftDelete>().IsDeleted)
{
SetDeletionAuditProperties(entry);
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted));
}
else
{
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Updated));
}
}
}
protected virtual void ApplyAbpConceptsForDeletedEntity(EntityEntry entry, EntityChangeReport changeReport)
protected virtual void ApplyAbpConceptsForDeletedEntity(EntityEntry entry)
{
if (TryCancelDeletionForSoftDelete(entry))
{
UpdateConcurrencyStamp(entry);
SetDeletionAuditProperties(entry);
}
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted));
}
protected virtual bool IsHardDeleted(EntityEntry entry)
@ -410,29 +503,6 @@ namespace Volo.Abp.EntityFrameworkCore
return hardDeletedEntities.Contains(entry.Entity);
}
protected virtual void AddDomainEvents(EntityChangeReport changeReport, object entityAsObj)
{
var generatesDomainEventsEntity = entityAsObj as IGeneratesDomainEvents;
if (generatesDomainEventsEntity == null)
{
return;
}
var localEvents = generatesDomainEventsEntity.GetLocalEvents()?.ToArray();
if (localEvents != null && localEvents.Any())
{
changeReport.DomainEvents.AddRange(localEvents.Select(eventData => new DomainEventEntry(entityAsObj, eventData)));
generatesDomainEventsEntity.ClearLocalEvents();
}
var distributedEvents = generatesDomainEventsEntity.GetDistributedEvents()?.ToArray();
if (distributedEvents != null && distributedEvents.Any())
{
changeReport.DistributedEvents.AddRange(distributedEvents.Select(eventData => new DomainEventEntry(entityAsObj, eventData)));
generatesDomainEventsEntity.ClearDistributedEvents();
}
}
protected virtual void UpdateConcurrencyStamp(EntityEntry entry)
{
var entity = entry.Entity as IHasConcurrencyStamp;

11
framework/src/Volo.Abp.EventBus.Kafka/Volo/Abp/EventBus/Kafka/KafkaDistributedEventBus.cs

@ -12,6 +12,7 @@ using Volo.Abp.EventBus.Distributed;
using Volo.Abp.Kafka;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Uow;
namespace Volo.Abp.EventBus.Kafka
{
@ -33,6 +34,7 @@ namespace Volo.Abp.EventBus.Kafka
public KafkaDistributedEventBus(
IServiceScopeFactory serviceScopeFactory,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IOptions<AbpKafkaEventBusOptions> abpKafkaEventBusOptions,
IKafkaMessageConsumerFactory messageConsumerFactory,
IOptions<AbpDistributedEventBusOptions> abpDistributedEventBusOptions,
@ -40,7 +42,7 @@ namespace Volo.Abp.EventBus.Kafka
IProducerPool producerPool,
IEventErrorHandler errorHandler,
IOptions<AbpEventBusOptions> abpEventBusOptions)
: base(serviceScopeFactory, currentTenant, errorHandler)
: base(serviceScopeFactory, currentTenant, unitOfWorkManager, errorHandler)
{
AbpKafkaEventBusOptions = abpKafkaEventBusOptions.Value;
AbpDistributedEventBusOptions = abpDistributedEventBusOptions.Value;
@ -165,11 +167,16 @@ namespace Volo.Abp.EventBus.Kafka
GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear());
}
public override async Task PublishAsync(Type eventType, object eventData)
protected override async Task PublishToEventBusAsync(Type eventType, object eventData)
{
await PublishAsync(eventType, eventData, new Headers {{"messageId", Serializer.Serialize(Guid.NewGuid())}}, null);
}
protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord)
{
unitOfWork.AddOrReplaceDistributedEvent(eventRecord);
}
public virtual async Task PublishAsync(Type eventType, object eventData, Headers headers, Dictionary<string, object> headersArguments)
{
await PublishAsync(AbpKafkaEventBusOptions.TopicName, eventType, eventData, headers, headersArguments);

12
framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs

@ -13,6 +13,7 @@ using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy;
using Volo.Abp.RabbitMQ;
using Volo.Abp.Threading;
using Volo.Abp.Uow;
namespace Volo.Abp.EventBus.RabbitMq
{
@ -44,9 +45,10 @@ namespace Volo.Abp.EventBus.RabbitMq
IOptions<AbpDistributedEventBusOptions> distributedEventBusOptions,
IRabbitMqMessageConsumerFactory messageConsumerFactory,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IEventErrorHandler errorHandler,
IOptions<AbpEventBusOptions> abpEventBusOptions)
: base(serviceScopeFactory, currentTenant, errorHandler)
: base(serviceScopeFactory, currentTenant, unitOfWorkManager, errorHandler)
{
ConnectionPool = connectionPool;
Serializer = serializer;
@ -189,14 +191,18 @@ namespace Volo.Abp.EventBus.RabbitMq
GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear());
}
public override async Task PublishAsync(Type eventType, object eventData)
protected override async Task PublishToEventBusAsync(Type eventType, object eventData)
{
await PublishAsync(eventType, eventData, null);
}
public Task PublishAsync(Type eventType, object eventData, IBasicProperties properties, Dictionary<string, object> headersArguments = null)
protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord)
{
unitOfWork.AddOrReplaceDistributedEvent(eventRecord);
}
public Task PublishAsync(Type eventType, object eventData, IBasicProperties properties, Dictionary<string, object> headersArguments = null)
{
var eventName = EventNameAttribute.GetNameOrDefault(eventType);
var body = Serializer.Serialize(eventData);

11
framework/src/Volo.Abp.EventBus.Rebus/Volo/Abp/EventBus/Rebus/RebusDistributedEventBus.cs

@ -10,6 +10,7 @@ using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Uow;
namespace Volo.Abp.EventBus.Rebus
{
@ -28,11 +29,12 @@ namespace Volo.Abp.EventBus.Rebus
public RebusDistributedEventBus(
IServiceScopeFactory serviceScopeFactory,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IBus rebus,
IOptions<AbpDistributedEventBusOptions> abpDistributedEventBusOptions,
IOptions<AbpRebusEventBusOptions> abpEventBusRebusOptions,
IEventErrorHandler errorHandler) :
base(serviceScopeFactory, currentTenant, errorHandler)
base(serviceScopeFactory, currentTenant, unitOfWorkManager, errorHandler)
{
Rebus = rebus;
AbpRebusEventBusOptions = abpEventBusRebusOptions.Value;
@ -125,11 +127,16 @@ namespace Volo.Abp.EventBus.Rebus
return Subscribe(typeof(TEvent), handler);
}
public override async Task PublishAsync(Type eventType, object eventData)
protected override async Task PublishToEventBusAsync(Type eventType, object eventData)
{
await AbpRebusEventBusOptions.Publish(Rebus, eventType, eventData);
}
protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord)
{
unitOfWork.AddOrReplaceDistributedEvent(eventRecord);
}
private List<IEventHandlerFactory> GetOrCreateHandlerFactories(Type eventType)
{
return HandlerFactories.GetOrAdd(

8
framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/LocalDistributedEventBus.cs

@ -122,15 +122,15 @@ namespace Volo.Abp.EventBus.Distributed
_localEventBus.UnsubscribeAll(eventType);
}
public Task PublishAsync<TEvent>(TEvent eventData)
public Task PublishAsync<TEvent>(TEvent eventData, bool onUnitOfWorkComplete = true)
where TEvent : class
{
return _localEventBus.PublishAsync(eventData);
return _localEventBus.PublishAsync(eventData, onUnitOfWorkComplete);
}
public Task PublishAsync(Type eventType, object eventData)
public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true)
{
return _localEventBus.PublishAsync(eventType, eventData);
return _localEventBus.PublishAsync(eventType, eventData, onUnitOfWorkComplete);
}
}
}

4
framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Distributed/NullDistributedEventBus.cs

@ -77,12 +77,12 @@ namespace Volo.Abp.EventBus.Distributed
}
public Task PublishAsync<TEvent>(TEvent eventData) where TEvent : class
public Task PublishAsync<TEvent>(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class
{
return Task.CompletedTask;
}
public Task PublishAsync(Type eventType, object eventData)
public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true)
{
return Task.CompletedTask;
}

29
framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/EventBusBase.cs

@ -10,6 +10,7 @@ using Volo.Abp.Collections;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Reflection;
using Volo.Abp.Uow;
namespace Volo.Abp.EventBus
{
@ -19,15 +20,19 @@ namespace Volo.Abp.EventBus
protected ICurrentTenant CurrentTenant { get; }
protected IUnitOfWorkManager UnitOfWorkManager { get; }
protected IEventErrorHandler ErrorHandler { get; }
protected EventBusBase(
IServiceScopeFactory serviceScopeFactory,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IEventErrorHandler errorHandler)
{
ServiceScopeFactory = serviceScopeFactory;
CurrentTenant = currentTenant;
UnitOfWorkManager = unitOfWorkManager;
ErrorHandler = errorHandler;
}
@ -87,13 +92,29 @@ namespace Volo.Abp.EventBus
public abstract void UnsubscribeAll(Type eventType);
/// <inheritdoc/>
public virtual Task PublishAsync<TEvent>(TEvent eventData) where TEvent : class
public Task PublishAsync<TEvent>(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class
{
return PublishAsync(typeof(TEvent), eventData);
return PublishAsync(typeof(TEvent), eventData, onUnitOfWorkComplete);
}
/// <inheritdoc/>
public abstract Task PublishAsync(Type eventType, object eventData);
public async Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true)
{
if (onUnitOfWorkComplete && UnitOfWorkManager.Current != null)
{
AddToUnitOfWork(
UnitOfWorkManager.Current,
new UnitOfWorkEventRecord(eventType, eventData, EventOrderGenerator.GetNext())
);
return;
}
await PublishToEventBusAsync(eventType, eventData);
}
protected abstract Task PublishToEventBusAsync(Type eventType, object eventData);
protected abstract void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord);
public virtual async Task TriggerHandlersAsync(Type eventType, object eventData, Action<EventExecutionErrorContext> onErrorAction = null)
{
@ -133,7 +154,7 @@ namespace Volo.Abp.EventBus
var baseEventType = eventType.GetGenericTypeDefinition().MakeGenericType(baseArg);
var constructorArgs = ((IEventDataWithInheritableGenericArgument)eventData).GetConstructorArgs();
var baseEventData = Activator.CreateInstance(baseEventType, constructorArgs);
await PublishAsync(baseEventType, baseEventData);
await PublishToEventBusAsync(baseEventType, baseEventData);
}
}
}

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

@ -10,8 +10,9 @@ namespace Volo.Abp.EventBus
/// </summary>
/// <typeparam name="TEvent">Event type</typeparam>
/// <param name="eventData">Related data for the event</param>
/// <param name="onUnitOfWorkComplete">True, to publish the event at the end of the current unit of work, if available</param>
/// <returns>The task to handle async operation</returns>
Task PublishAsync<TEvent>(TEvent eventData)
Task PublishAsync<TEvent>(TEvent eventData, bool onUnitOfWorkComplete = true)
where TEvent : class;
/// <summary>
@ -19,8 +20,9 @@ namespace Volo.Abp.EventBus
/// </summary>
/// <param name="eventType">Event type</param>
/// <param name="eventData">Related data for the event</param>
/// <param name="onUnitOfWorkComplete">True, to publish the event at the end of the current unit of work, if available</param>
/// <returns>The task to handle async operation</returns>
Task PublishAsync(Type eventType, object eventData);
Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true);
/// <summary>
/// Registers to an event.

11
framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/LocalEventBus.cs

@ -12,6 +12,7 @@ using Volo.Abp.DependencyInjection;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Threading;
using Volo.Abp.Json;
using Volo.Abp.Uow;
namespace Volo.Abp.EventBus.Local
{
@ -34,8 +35,9 @@ namespace Volo.Abp.EventBus.Local
IOptions<AbpLocalEventBusOptions> options,
IServiceScopeFactory serviceScopeFactory,
ICurrentTenant currentTenant,
IUnitOfWorkManager unitOfWorkManager,
IEventErrorHandler errorHandler)
: base(serviceScopeFactory, currentTenant, errorHandler)
: base(serviceScopeFactory, currentTenant, unitOfWorkManager, errorHandler)
{
Options = options.Value;
Logger = NullLogger<LocalEventBus>.Instance;
@ -120,11 +122,16 @@ namespace Volo.Abp.EventBus.Local
GetOrCreateHandlerFactories(eventType).Locking(factories => factories.Clear());
}
public override async Task PublishAsync(Type eventType, object eventData)
protected override async Task PublishToEventBusAsync(Type eventType, object eventData)
{
await PublishAsync(new LocalEventMessage(Guid.NewGuid(), eventData, eventType));
}
protected override void AddToUnitOfWork(IUnitOfWork unitOfWork, UnitOfWorkEventRecord eventRecord)
{
unitOfWork.AddOrReplaceLocalEvent(eventRecord);
}
public virtual async Task PublishAsync(LocalEventMessage localEventMessage)
{
await TriggerHandlersAsync(localEventMessage.EventType, localEventMessage.EventData, errorContext =>

4
framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/Local/NullLocalEventBus.cs

@ -77,12 +77,12 @@ namespace Volo.Abp.EventBus.Local
}
public Task PublishAsync<TEvent>(TEvent eventData) where TEvent : class
public Task PublishAsync<TEvent>(TEvent eventData, bool onUnitOfWorkComplete = true) where TEvent : class
{
return Task.CompletedTask;
}
public Task PublishAsync(Type eventType, object eventData)
public Task PublishAsync(Type eventType, object eventData, bool onUnitOfWorkComplete = true)
{
return Task.CompletedTask;
}

40
framework/src/Volo.Abp.EventBus/Volo/Abp/EventBus/UnitOfWorkEventPublisher.cs

@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Uow;
namespace Volo.Abp.EventBus
{
[Dependency(ReplaceServices = true)]
public class UnitOfWorkEventPublisher : IUnitOfWorkEventPublisher, ITransientDependency
{
private readonly ILocalEventBus _localEventBus;
private readonly IDistributedEventBus _distributedEventBus;
public UnitOfWorkEventPublisher(
ILocalEventBus localEventBus,
IDistributedEventBus distributedEventBus)
{
_localEventBus = localEventBus;
_distributedEventBus = distributedEventBus;
}
public async Task PublishLocalEventsAsync(IEnumerable<UnitOfWorkEventRecord> localEvents)
{
foreach (var localEvent in localEvents)
{
await _localEventBus.PublishAsync(localEvent.EventType, localEvent.EventData, onUnitOfWorkComplete: false);
}
}
public async Task PublishDistributedEventsAsync(IEnumerable<UnitOfWorkEventRecord> distributedEvents)
{
foreach (var distributedEvent in distributedEvents)
{
await _distributedEventBus.PublishAsync(distributedEvent.EventType, distributedEvent.EventData, onUnitOfWorkComplete: false);
}
}
}
}

27
framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Properties/launchSettings.json

@ -1,27 +0,0 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53610/",
"sslPort": 44304
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Volo.Abp.Http.Client.IdentityModel.Web": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

1
framework/src/Volo.Abp.Http.Client.IdentityModel.Web/Volo.Abp.Http.Client.IdentityModel.Web.csproj

@ -13,6 +13,7 @@
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
<IsPackable>true</IsPackable>
<OutputType>Library</OutputType>
<NoDefaultLaunchSettingsFile>true</NoDefaultLaunchSettingsFile>
<RootNamespace />
</PropertyGroup>

11
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/DynamicHttpProxyInterceptor.cs

@ -113,12 +113,11 @@ namespace Volo.Abp.Http.Client.DynamicProxying
/* returning a class that holds a reference to response
* content just to be sure that GC does not dispose of
* it before we finish doing our work with the stream */
return (T)(object)new RemoteStreamContent(await responseContent.ReadAsStreamAsync())
{
ContentType = responseContent.Headers.ContentType?.ToString(),
FileName = responseContent.Headers?.ContentDisposition?.FileNameStar ??
RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString()
};
return (T) (object) new RemoteStreamContent(
await responseContent.ReadAsStreamAsync(),
responseContent.Headers?.ContentDisposition?.FileNameStar ?? RemoveQuotes(responseContent.Headers?.ContentDisposition?.FileName).ToString(),
responseContent.Headers?.ContentType?.ToString(),
responseContent.Headers?.ContentLength);
}
var stringContent = await responseContent.ReadAsStringAsync();

11
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs

@ -82,16 +82,12 @@ namespace Volo.Abp.Http.Client.DynamicProxying
if (value is IRemoteStreamContent remoteStreamContent)
{
var stream = remoteStreamContent.GetStream();
if (stream.CanSeek)
{
stream.Position = 0;
}
var streamContent = new StreamContent(stream);
if (!remoteStreamContent.ContentType.IsNullOrWhiteSpace())
{
streamContent.Headers.ContentType = new MediaTypeHeaderValue(remoteStreamContent.ContentType);
}
streamContent.Headers.ContentLength = remoteStreamContent.ContentLength;
formData.Add(streamContent, parameter.Name, remoteStreamContent.FileName ?? parameter.Name);
}
else if (value is IEnumerable<IRemoteStreamContent> remoteStreamContents)
@ -99,15 +95,12 @@ namespace Volo.Abp.Http.Client.DynamicProxying
foreach (var content in remoteStreamContents)
{
var stream = content.GetStream();
if (stream.CanSeek)
{
stream.Position = 0;
}
var streamContent = new StreamContent(stream);
if (!content.ContentType.IsNullOrWhiteSpace())
{
streamContent.Headers.ContentType = new MediaTypeHeaderValue(content.ContentType);
}
streamContent.Headers.ContentLength = content.ContentLength;
formData.Add(streamContent, parameter.Name, content.FileName ?? parameter.Name);
}
}

4
framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/UrlBuilder.cs

@ -132,14 +132,14 @@ namespace Volo.Abp.Http.Client.DynamicProxying
return true;
}
private static string ConvertValueToString([NotNull] object value)
private static string ConvertValueToString([CanBeNull] object value)
{
if (value is DateTime dateTimeValue)
{
return dateTimeValue.ToUniversalTime().ToString("O");
}
return value.ToString();
return value?.ToString();
}
}
}

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

@ -12,6 +12,7 @@ using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Guids;
using Volo.Abp.MemoryDb;
using Volo.Abp.Uow;
namespace Volo.Abp.Domain.Repositories.MemoryDb
{
@ -65,7 +66,7 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
return ApplyDataFilters((await GetCollectionAsync()).AsQueryable());
}
protected virtual async Task TriggerDomainEventsAsync(object entity)
protected virtual void TriggerDomainEvents(object entity)
{
var generatesDomainEventsEntity = entity as IGeneratesDomainEvents;
if (generatesDomainEventsEntity == null)
@ -78,7 +79,13 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
{
foreach (var localEvent in localEvents)
{
await LocalEventBus.PublishAsync(localEvent.GetType(), localEvent);
UnitOfWorkManager.Current?.AddOrReplaceLocalEvent(
new UnitOfWorkEventRecord(
localEvent.EventData.GetType(),
localEvent.EventData,
localEvent.EventOrder
)
);
}
generatesDomainEventsEntity.ClearLocalEvents();
@ -89,7 +96,13 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
{
foreach (var distributedEvent in distributedEvents)
{
await DistributedEventBus.PublishAsync(distributedEvent.GetType(), distributedEvent);
UnitOfWorkManager.Current?.AddOrReplaceDistributedEvent(
new UnitOfWorkEventRecord(
distributedEvent.EventData.GetType(),
distributedEvent.EventData,
distributedEvent.EventOrder
)
);
}
generatesDomainEventsEntity.ClearDistributedEvents();
@ -143,37 +156,37 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
AuditPropertySetter.SetDeletionProperties(entity);
}
protected virtual async Task TriggerEntityCreateEvents(TEntity entity)
protected virtual void TriggerEntityCreateEvents(TEntity entity)
{
await EntityChangeEventHelper.TriggerEntityCreatedEventOnUowCompletedAsync(entity);
await EntityChangeEventHelper.TriggerEntityCreatingEventAsync(entity);
EntityChangeEventHelper.PublishEntityCreatingEvent(entity);
EntityChangeEventHelper.PublishEntityCreatedEvent(entity);
}
protected virtual async Task TriggerEntityUpdateEventsAsync(TEntity entity)
protected virtual void TriggerEntityUpdateEvents(TEntity entity)
{
await EntityChangeEventHelper.TriggerEntityUpdatedEventOnUowCompletedAsync(entity);
await EntityChangeEventHelper.TriggerEntityUpdatingEventAsync(entity);
EntityChangeEventHelper.PublishEntityUpdatingEvent(entity);
EntityChangeEventHelper.PublishEntityUpdatedEvent(entity);
}
protected virtual async Task TriggerEntityDeleteEventsAsync(TEntity entity)
protected virtual void TriggerEntityDeleteEvents(TEntity entity)
{
await EntityChangeEventHelper.TriggerEntityDeletedEventOnUowCompletedAsync(entity);
await EntityChangeEventHelper.TriggerEntityDeletingEventAsync(entity);
EntityChangeEventHelper.PublishEntityDeletingEvent(entity);
EntityChangeEventHelper.PublishEntityDeletedEvent(entity);
}
protected virtual async Task ApplyAbpConceptsForAddedEntityAsync(TEntity entity)
protected virtual void ApplyAbpConceptsForAddedEntity(TEntity entity)
{
CheckAndSetId(entity);
SetCreationAuditProperties(entity);
await TriggerEntityCreateEvents(entity);
await TriggerDomainEventsAsync(entity);
TriggerEntityCreateEvents(entity);
TriggerDomainEvents(entity);
}
protected virtual async Task ApplyAbpConceptsForDeletedEntityAsync(TEntity entity)
protected virtual void ApplyAbpConceptsForDeletedEntity(TEntity entity)
{
SetDeletionAuditProperties(entity);
await TriggerEntityDeleteEventsAsync(entity);
await TriggerDomainEventsAsync(entity);
TriggerEntityDeleteEvents(entity);
TriggerDomainEvents(entity);
}
public override async Task<TEntity> FindAsync(
@ -199,7 +212,7 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
bool autoSave = false,
CancellationToken cancellationToken = default)
{
await ApplyAbpConceptsForAddedEntityAsync(entity);
ApplyAbpConceptsForAddedEntity(entity);
(await GetCollectionAsync()).Add(entity);
@ -216,14 +229,14 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
if (entity is ISoftDelete softDeleteEntity && softDeleteEntity.IsDeleted)
{
SetDeletionAuditProperties(entity);
await TriggerEntityDeleteEventsAsync(entity);
TriggerEntityDeleteEvents(entity);
}
else
{
await TriggerEntityUpdateEventsAsync(entity);
TriggerEntityUpdateEvents(entity);
}
await TriggerDomainEventsAsync(entity);
TriggerDomainEvents(entity);
(await GetCollectionAsync()).Update(entity);
@ -235,7 +248,7 @@ namespace Volo.Abp.Domain.Repositories.MemoryDb
bool autoSave = false,
CancellationToken cancellationToken = default)
{
await ApplyAbpConceptsForDeletedEntityAsync(entity);
ApplyAbpConceptsForDeletedEntity(entity);
if (entity is ISoftDelete softDeleteEntity && !IsHardDeleted(entity))
{

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

@ -17,6 +17,7 @@ using Volo.Abp.EventBus.Local;
using Volo.Abp.Guids;
using Volo.Abp.MongoDB;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Uow;
namespace Volo.Abp.Domain.Repositories.MongoDB
{
@ -182,14 +183,14 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
if (entity is ISoftDelete softDeleteEntity && softDeleteEntity.IsDeleted)
{
SetDeletionAuditProperties(entity);
await TriggerEntityDeleteEventsAsync(entity);
TriggerEntityDeleteEvents(entity);
}
else
{
await TriggerEntityUpdateEventsAsync(entity);
TriggerEntityUpdateEvents(entity);
}
await TriggerDomainEventsAsync(entity);
TriggerDomainEvents(entity);
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
ReplaceOneResult result;
@ -235,14 +236,14 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
if (isSoftDeleteEntity)
{
SetDeletionAuditProperties(entity);
await TriggerEntityDeleteEventsAsync(entity);
TriggerEntityDeleteEvents(entity);
}
else
{
await TriggerEntityUpdateEventsAsync(entity);
TriggerEntityUpdateEvents(entity);
}
await TriggerDomainEventsAsync(entity);
TriggerDomainEvents(entity);
SetNewConcurrencyStamp(entity);
}
@ -295,7 +296,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)) && !IsHardDeleted(entity))
{
((ISoftDelete)entity).IsDeleted = true;
await ApplyAbpConceptsForDeletedEntityAsync(entity);
ApplyAbpConceptsForDeletedEntity(entity);
ReplaceOneResult result;
@ -324,7 +325,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
}
else
{
await ApplyAbpConceptsForDeletedEntityAsync(entity);
ApplyAbpConceptsForDeletedEntity(entity);
DeleteResult result;
@ -374,7 +375,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
hardDeletedEntities.Add(entity);
}
await ApplyAbpConceptsForDeletedEntityAsync(entity);
ApplyAbpConceptsForDeletedEntity(entity);
}
var dbContext = await GetDbContextAsync(cancellationToken);
@ -573,33 +574,33 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
{
CheckAndSetId(entity);
SetCreationAuditProperties(entity);
await TriggerEntityCreateEvents(entity);
await TriggerDomainEventsAsync(entity);
TriggerEntityCreateEvents(entity);
TriggerDomainEvents(entity);
}
private async Task TriggerEntityCreateEvents(TEntity entity)
private void TriggerEntityCreateEvents(TEntity entity)
{
await EntityChangeEventHelper.TriggerEntityCreatedEventOnUowCompletedAsync(entity);
await EntityChangeEventHelper.TriggerEntityCreatingEventAsync(entity);
EntityChangeEventHelper.PublishEntityCreatingEvent(entity);
EntityChangeEventHelper.PublishEntityCreatedEvent(entity);
}
protected virtual async Task TriggerEntityUpdateEventsAsync(TEntity entity)
protected virtual void TriggerEntityUpdateEvents(TEntity entity)
{
await EntityChangeEventHelper.TriggerEntityUpdatedEventOnUowCompletedAsync(entity);
await EntityChangeEventHelper.TriggerEntityUpdatingEventAsync(entity);
EntityChangeEventHelper.PublishEntityUpdatingEvent(entity);
EntityChangeEventHelper.PublishEntityUpdatedEvent(entity);
}
protected virtual async Task ApplyAbpConceptsForDeletedEntityAsync(TEntity entity)
protected virtual void ApplyAbpConceptsForDeletedEntity(TEntity entity)
{
SetDeletionAuditProperties(entity);
await TriggerEntityDeleteEventsAsync(entity);
await TriggerDomainEventsAsync(entity);
TriggerEntityDeleteEvents(entity);
TriggerDomainEvents(entity);
}
protected virtual async Task TriggerEntityDeleteEventsAsync(TEntity entity)
protected virtual void TriggerEntityDeleteEvents(TEntity entity)
{
await EntityChangeEventHelper.TriggerEntityDeletedEventOnUowCompletedAsync(entity);
await EntityChangeEventHelper.TriggerEntityDeletingEventAsync(entity);
EntityChangeEventHelper.PublishEntityDeletingEvent(entity);
EntityChangeEventHelper.PublishEntityDeletedEvent(entity);
}
protected virtual void CheckAndSetId(TEntity entity)
@ -639,7 +640,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
AuditPropertySetter.SetDeletionProperties(entity);
}
protected virtual async Task TriggerDomainEventsAsync(object entity)
protected virtual void TriggerDomainEvents(object entity)
{
var generatesDomainEventsEntity = entity as IGeneratesDomainEvents;
if (generatesDomainEventsEntity == null)
@ -652,7 +653,13 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
{
foreach (var localEvent in localEvents)
{
await LocalEventBus.PublishAsync(localEvent.GetType(), localEvent);
UnitOfWorkManager.Current?.AddOrReplaceLocalEvent(
new UnitOfWorkEventRecord(
localEvent.EventData.GetType(),
localEvent.EventData,
localEvent.EventOrder
)
);
}
generatesDomainEventsEntity.ClearLocalEvents();
@ -663,7 +670,13 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
{
foreach (var distributedEvent in distributedEvents)
{
await DistributedEventBus.PublishAsync(distributedEvent.GetType(), distributedEvent);
UnitOfWorkManager.Current?.AddOrReplaceDistributedEvent(
new UnitOfWorkEventRecord(
distributedEvent.EventData.GetType(),
distributedEvent.EventData,
distributedEvent.EventOrder
)
);
}
generatesDomainEventsEntity.ClearDistributedEvents();

13
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/AmbientUnitOfWork.cs

@ -19,5 +19,18 @@ namespace Volo.Abp.Uow
{
_currentUow.Value = unitOfWork;
}
public IUnitOfWork GetCurrentByChecking()
{
var uow = UnitOfWork;
//Skip reserved unit of work
while (uow != null && (uow.IsReserved || uow.IsDisposed || uow.IsCompleted))
{
uow = uow.Outer;
}
return uow;
}
}
}

14
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/ChildUnitOfWork.cs

@ -76,6 +76,20 @@ namespace Volo.Abp.Uow
_parent.OnCompleted(handler);
}
public void AddOrReplaceLocalEvent(
UnitOfWorkEventRecord eventRecord,
Predicate<UnitOfWorkEventRecord> replacementSelector = null)
{
_parent.AddOrReplaceLocalEvent(eventRecord, replacementSelector);
}
public void AddOrReplaceDistributedEvent(
UnitOfWorkEventRecord eventRecord,
Predicate<UnitOfWorkEventRecord> replacementSelector = null)
{
_parent.AddOrReplaceDistributedEvent(eventRecord, replacementSelector);
}
public IDatabaseApi FindDatabaseApi(string key)
{
return _parent.FindDatabaseApi(key);

14
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/EventOrderGenerator.cs

@ -0,0 +1,14 @@
using System.Threading;
namespace Volo.Abp.Uow
{
public static class EventOrderGenerator
{
private static long _lastOrder;
public static long GetNext()
{
return Interlocked.Increment(ref _lastOrder);
}
}
}

2
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IAmbientUnitOfWork.cs

@ -2,6 +2,6 @@
{
public interface IAmbientUnitOfWork : IUnitOfWorkAccessor
{
IUnitOfWork GetCurrentByChecking();
}
}

10
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWork.cs

@ -42,5 +42,15 @@ namespace Volo.Abp.Uow
Task RollbackAsync(CancellationToken cancellationToken = default);
void OnCompleted(Func<Task> handler);
void AddOrReplaceLocalEvent(
UnitOfWorkEventRecord eventRecord,
Predicate<UnitOfWorkEventRecord> replacementSelector = null
);
void AddOrReplaceDistributedEvent(
UnitOfWorkEventRecord eventRecord,
Predicate<UnitOfWorkEventRecord> replacementSelector = null
);
}
}

12
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/IUnitOfWorkEventPublisher.cs

@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Volo.Abp.Uow
{
public interface IUnitOfWorkEventPublisher
{
Task PublishLocalEventsAsync(IEnumerable<UnitOfWorkEventRecord> localEvents);
Task PublishDistributedEventsAsync(IEnumerable<UnitOfWorkEventRecord> distributedEvents);
}
}

19
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/NullUnitOfWorkEventPublisher.cs

@ -0,0 +1,19 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
namespace Volo.Abp.Uow
{
public class NullUnitOfWorkEventPublisher : IUnitOfWorkEventPublisher, ISingletonDependency
{
public Task PublishLocalEventsAsync(IEnumerable<UnitOfWorkEventRecord> localEvents)
{
return Task.CompletedTask;
}
public Task PublishDistributedEventsAsync(IEnumerable<UnitOfWorkEventRecord> distributedEvents)
{
return Task.CompletedTask;
}
}
}

89
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWork.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@ -33,11 +34,14 @@ namespace Volo.Abp.Uow
public string ReservationName { get; set; }
protected List<Func<Task>> CompletedHandlers { get; } = new List<Func<Task>>();
protected List<UnitOfWorkEventRecord> DistributedEvents { get; } = new List<UnitOfWorkEventRecord>();
protected List<UnitOfWorkEventRecord> LocalEvents { get; } = new List<UnitOfWorkEventRecord>();
public event EventHandler<UnitOfWorkFailedEventArgs> Failed;
public event EventHandler<UnitOfWorkEventArgs> Disposed;
public IServiceProvider ServiceProvider { get; }
protected IUnitOfWorkEventPublisher UnitOfWorkEventPublisher { get; }
[NotNull]
public Dictionary<string, object> Items { get; }
@ -50,9 +54,13 @@ namespace Volo.Abp.Uow
private bool _isCompleting;
private bool _isRolledback;
public UnitOfWork(IServiceProvider serviceProvider, IOptions<AbpUnitOfWorkDefaultOptions> options)
public UnitOfWork(
IServiceProvider serviceProvider,
IUnitOfWorkEventPublisher unitOfWorkEventPublisher,
IOptions<AbpUnitOfWorkDefaultOptions> options)
{
ServiceProvider = serviceProvider;
UnitOfWorkEventPublisher = unitOfWorkEventPublisher;
_defaultOptions = options.Value;
_databaseApis = new Dictionary<string, IDatabaseApi>();
@ -103,12 +111,12 @@ namespace Volo.Abp.Uow
}
}
public IReadOnlyList<IDatabaseApi> GetAllActiveDatabaseApis()
public virtual IReadOnlyList<IDatabaseApi> GetAllActiveDatabaseApis()
{
return _databaseApis.Values.ToImmutableList();
}
public IReadOnlyList<ITransactionApi> GetAllActiveTransactionApis()
public virtual IReadOnlyList<ITransactionApi> GetAllActiveTransactionApis()
{
return _transactionApis.Values.ToImmutableList();
}
@ -126,6 +134,30 @@ namespace Volo.Abp.Uow
{
_isCompleting = true;
await SaveChangesAsync(cancellationToken);
while (LocalEvents.Any() || DistributedEvents.Any())
{
if (LocalEvents.Any())
{
var localEventsToBePublished = LocalEvents.OrderBy(e => e.EventOrder).ToArray();
LocalEvents.Clear();
await UnitOfWorkEventPublisher.PublishLocalEventsAsync(
localEventsToBePublished
);
}
if (DistributedEvents.Any())
{
var distributedEventsToBePublished = DistributedEvents.OrderBy(e => e.EventOrder).ToArray();
DistributedEvents.Clear();
await UnitOfWorkEventPublisher.PublishDistributedEventsAsync(
distributedEventsToBePublished
);
}
await SaveChangesAsync(cancellationToken);
}
await CommitTransactionsAsync();
IsCompleted = true;
await OnCompletedAsync();
@ -149,12 +181,12 @@ namespace Volo.Abp.Uow
await RollbackAllAsync(cancellationToken);
}
public IDatabaseApi FindDatabaseApi(string key)
public virtual IDatabaseApi FindDatabaseApi(string key)
{
return _databaseApis.GetOrDefault(key);
}
public void AddDatabaseApi(string key, IDatabaseApi api)
public virtual void AddDatabaseApi(string key, IDatabaseApi api)
{
Check.NotNull(key, nameof(key));
Check.NotNull(api, nameof(api));
@ -167,7 +199,7 @@ namespace Volo.Abp.Uow
_databaseApis.Add(key, api);
}
public IDatabaseApi GetOrAddDatabaseApi(string key, Func<IDatabaseApi> factory)
public virtual IDatabaseApi GetOrAddDatabaseApi(string key, Func<IDatabaseApi> factory)
{
Check.NotNull(key, nameof(key));
Check.NotNull(factory, nameof(factory));
@ -175,14 +207,14 @@ namespace Volo.Abp.Uow
return _databaseApis.GetOrAdd(key, factory);
}
public ITransactionApi FindTransactionApi(string key)
public virtual ITransactionApi FindTransactionApi(string key)
{
Check.NotNull(key, nameof(key));
return _transactionApis.GetOrDefault(key);
}
public void AddTransactionApi(string key, ITransactionApi api)
public virtual void AddTransactionApi(string key, ITransactionApi api)
{
Check.NotNull(key, nameof(key));
Check.NotNull(api, nameof(api));
@ -195,7 +227,7 @@ namespace Volo.Abp.Uow
_transactionApis.Add(key, api);
}
public ITransactionApi GetOrAddTransactionApi(string key, Func<ITransactionApi> factory)
public virtual ITransactionApi GetOrAddTransactionApi(string key, Func<ITransactionApi> factory)
{
Check.NotNull(key, nameof(key));
Check.NotNull(factory, nameof(factory));
@ -203,11 +235,48 @@ namespace Volo.Abp.Uow
return _transactionApis.GetOrAdd(key, factory);
}
public void OnCompleted(Func<Task> handler)
public virtual void OnCompleted(Func<Task> handler)
{
CompletedHandlers.Add(handler);
}
public virtual void AddOrReplaceLocalEvent(
UnitOfWorkEventRecord eventRecord,
Predicate<UnitOfWorkEventRecord> replacementSelector = null)
{
AddOrReplaceEvent(LocalEvents, eventRecord, replacementSelector);
}
public virtual void AddOrReplaceDistributedEvent(
UnitOfWorkEventRecord eventRecord,
Predicate<UnitOfWorkEventRecord> replacementSelector = null)
{
AddOrReplaceEvent(DistributedEvents, eventRecord, replacementSelector);
}
public virtual void AddOrReplaceEvent(
List<UnitOfWorkEventRecord> eventRecords,
UnitOfWorkEventRecord eventRecord,
Predicate<UnitOfWorkEventRecord> replacementSelector = null)
{
if (replacementSelector == null)
{
eventRecords.Add(eventRecord);
}
else
{
var foundIndex = eventRecords.FindIndex(replacementSelector);
if (foundIndex < 0)
{
eventRecords.Add(eventRecord);
}
else
{
eventRecords[foundIndex] = eventRecord;
}
}
}
protected virtual async Task OnCompletedAsync()
{
foreach (var handler in CompletedHandlers)

29
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkEventRecord.cs

@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.Uow
{
public class UnitOfWorkEventRecord
{
public object EventData { get; }
public Type EventType { get; }
public long EventOrder { get; }
/// <summary>
/// Extra properties can be used if needed.
/// </summary>
public Dictionary<string, object> Properties { get; } = new Dictionary<string, object>();
public UnitOfWorkEventRecord(
Type eventType,
object eventData,
long eventOrder)
{
EventType = eventType;
EventData = eventData;
EventOrder = eventOrder;
}
}
}

15
framework/src/Volo.Abp.Uow/Volo/Abp/Uow/UnitOfWorkManager.cs

@ -10,7 +10,7 @@ namespace Volo.Abp.Uow
[Obsolete("This will be removed in next versions.")]
public static AsyncLocal<bool> DisableObsoleteDbContextCreationWarning { get; } = new AsyncLocal<bool>();
public IUnitOfWork Current => GetCurrentUnitOfWork();
public IUnitOfWork Current => _ambientUnitOfWork.GetCurrentByChecking();
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IAmbientUnitOfWork _ambientUnitOfWork;
@ -86,19 +86,6 @@ namespace Volo.Abp.Uow
return true;
}
private IUnitOfWork GetCurrentUnitOfWork()
{
var uow = _ambientUnitOfWork.UnitOfWork;
//Skip reserved unit of work
while (uow != null && (uow.IsReserved || uow.IsDisposed || uow.IsCompleted))
{
uow = uow.Outer;
}
return uow;
}
private IUnitOfWork CreateNewUnitOfWork()
{
var scope = _serviceScopeFactory.CreateScope();

17
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController.cs

@ -16,11 +16,8 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("DownloadAsync"));
return new RemoteStreamContent(memoryStream, "download.rtf")
{
ContentType = "application/rtf"
};
memoryStream.Position = 0;
return new RemoteStreamContent(memoryStream, "download.rtf", "application/rtf");
}
[HttpPost]
@ -32,5 +29,15 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
return await reader.ReadToEndAsync() + ":" + file.ContentType + ":" + file.FileName;
}
}
[HttpPost]
[Route("Upload-Raw")]
public async Task<string> UploadRawAsync(IRemoteStreamContent file)
{
using (var reader = new StreamReader(file.GetStream()))
{
return await reader.ReadToEndAsync() + ":" + file.ContentType;
}
}
}
}

23
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController_Tests.cs

@ -30,13 +30,32 @@ namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
var streamContent = new StreamContent(memoryStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/rtf");
requestMessage.Content = new MultipartFormDataContent {{streamContent, "file", "upload.rtf"}};
requestMessage.Content = new MultipartFormDataContent {{ streamContent, "file", "upload.rtf" }};
var response = await Client.SendAsync(requestMessage);
(await response.Content.ReadAsStringAsync()).ShouldBe("UploadAsync:application/rtf:upload.rtf");
}
}
[Fact]
public async Task UploadRawAsync()
{
using (var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/remote-stream-content-test/upload-raw"))
{
var memoryStream = new MemoryStream();
var text = @"{ ""hello"": ""world"" }";
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes(text));
memoryStream.Position = 0;
var streamContent = new StreamContent(memoryStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
requestMessage.Content = streamContent;
var response = await Client.SendAsync(requestMessage);
(await response.Content.ReadAsStringAsync()).ShouldBe($"{text}:application/json");
}
}
}
}

10
framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/Uow/TestUnitOfWork.cs

@ -13,8 +13,14 @@ namespace Volo.Abp.AspNetCore.Mvc.Uow
{
private readonly TestUnitOfWorkConfig _config;
public TestUnitOfWork(IServiceProvider serviceProvider, IOptions<AbpUnitOfWorkDefaultOptions> options, TestUnitOfWorkConfig config)
: base(serviceProvider, options)
public TestUnitOfWork(
IServiceProvider serviceProvider,
IUnitOfWorkEventPublisher unitOfWorkEventPublisher,
IOptions<AbpUnitOfWorkDefaultOptions> options, TestUnitOfWorkConfig config)
: base(
serviceProvider,
unitOfWorkEventPublisher,
options)
{
_config = config;
}

10
framework/test/Volo.Abp.Cli.Core.Tests/Volo/Abp/Cli/ProjectNameValidation_Tests.cs

@ -18,7 +18,7 @@ namespace Volo.Abp.Cli
}
[Fact]
public async Task IllegalProjectName_Test()
public async Task Illegal_ProjectName_Test()
{
var illegalProjectNames = new[]
{
@ -39,21 +39,19 @@ namespace Volo.Abp.Cli
}
[Fact]
public async Task ParentDirectoryContain_Test()
public async Task Parent_Directory_Char_Contains_Test()
{
var args = new CommandLineArgs("new", "Test..Test");
await _newCommand.ExecuteAsync(args).ShouldThrowAsync<CliUsageException>();
}
[Fact]
public async Task Has_Illegel_Keyword_Test()
public async Task Contains_Illegal_Keyword_Test()
{
var illegalKeywords = new[]
{
"Acme.Blazor",
"MyBlazor",
"Blazor"
};
foreach (var illegalKeyword in illegalKeywords)

60
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs

@ -49,7 +49,7 @@ namespace Volo.Abp.Http.DynamicProxying
{
var people = await _peopleAppService.GetListAsync(new PagedAndSortedResultRequestDto());
people.TotalCount.ShouldBeGreaterThan(0);
people.Items.Count.ShouldBe((int) people.TotalCount);
people.Items.Count.ShouldBe((int)people.TotalCount);
}
[Fact]
@ -90,8 +90,7 @@ namespace Volo.Abp.Http.DynamicProxying
{
Name = uniquePersonName,
Age = 42
}
);
});
person.ShouldNotBeNull();
person.Id.ShouldNotBe(Guid.Empty);
@ -110,8 +109,7 @@ namespace Volo.Abp.Http.DynamicProxying
var person = await _peopleAppService.CreateAsync(new PersonDto
{
Age = 42
}
);
});
});
}
@ -195,10 +193,22 @@ namespace Volo.Abp.Http.DynamicProxying
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("UploadAsync"));
memoryStream.Position = 0;
var result = await _peopleAppService.UploadAsync(new RemoteStreamContent(memoryStream, "upload.rtf")
{
ContentType = "application/rtf"
});
var result = await _peopleAppService.UploadAsync(new RemoteStreamContent(memoryStream, "upload.rtf", "application/rtf"));
result.ShouldBe("UploadAsync:application/rtf:upload.rtf");
}
[Fact]
public async Task UploadPartialAsync()
{
var memoryStream = new MemoryStream();
var rawData = new byte[16];
var text = Encoding.UTF8.GetBytes("UploadAsync");
await memoryStream.WriteAsync(rawData);
await memoryStream.WriteAsync(text);
memoryStream.Position = rawData.Length;
var result = await _peopleAppService.UploadAsync(new RemoteStreamContent(memoryStream, "upload.rtf", "application/rtf"));
result.ShouldBe("UploadAsync:application/rtf:upload.rtf");
}
@ -215,15 +225,8 @@ namespace Volo.Abp.Http.DynamicProxying
var result = await _peopleAppService.UploadMultipleAsync(new List<IRemoteStreamContent>()
{
new RemoteStreamContent(memoryStream, "File1.rtf")
{
ContentType = "application/rtf"
},
new RemoteStreamContent(memoryStream2, "File2.rtf")
{
ContentType = "application/rtf2"
}
new RemoteStreamContent(memoryStream, "File1.rtf", "application/rtf"),
new RemoteStreamContent(memoryStream2, "File2.rtf", "application/rtf2")
});
result.ShouldBe("File1:application/rtf:File1.rtfFile2:application/rtf2:File2.rtf");
}
@ -237,10 +240,7 @@ namespace Volo.Abp.Http.DynamicProxying
var result = await _peopleAppService.CreateFileAsync(new CreateFileInput()
{
Name = "123.rtf",
Content = new RemoteStreamContent(memoryStream, "create.rtf")
{
ContentType = "application/rtf"
}
Content = new RemoteStreamContent(memoryStream, "create.rtf", "application/rtf")
});
result.ShouldBe("123.rtf:CreateFileAsync:application/rtf:create.rtf");
}
@ -265,23 +265,13 @@ namespace Volo.Abp.Http.DynamicProxying
Name = "123.rtf",
Contents = new List<IRemoteStreamContent>()
{
new RemoteStreamContent(memoryStream, "1-1.rtf")
{
ContentType = "application/rtf"
},
new RemoteStreamContent(memoryStream2, "1-2.rtf")
{
ContentType = "application/rtf2"
}
new RemoteStreamContent(memoryStream, "1-1.rtf", "application/rtf"),
new RemoteStreamContent(memoryStream2, "1-2.rtf", "application/rtf2")
},
Inner = new CreateFileInput()
{
Name = "789.rtf",
Content = new RemoteStreamContent(memoryStream3, "i-789.rtf")
{
ContentType = "application/rtf3"
}
Content = new RemoteStreamContent(memoryStream3, "i-789.rtf", "application/rtf3")
}
});
result.ShouldBe("123.rtf:File1:application/rtf:1-1.rtf123.rtf:File2:application/rtf2:1-2.rtf789.rtf:File3:application/rtf3:i-789.rtf");

10
framework/test/Volo.Abp.MemoryDb.Tests/Volo/Abp/MemoryDb/DomainEvents/DomainEvents_Tests.cs

@ -1,9 +1,15 @@
using Volo.Abp.TestApp.Testing;
using System.Threading.Tasks;
using Volo.Abp.TestApp.Testing;
using Xunit;
namespace Volo.Abp.MemoryDb.DomainEvents
{
public class DomainEvents_Tests : DomainEvents_Tests<AbpMemoryDbTestModule>
{
[Fact(Skip = "MemoryDB doesn't support transactions.")]
public override Task Should_Rollback_Uow_If_Event_Handler_Throws_Exception()
{
return base.Should_Rollback_Uow_If_Event_Handler_Throws_Exception();
}
}
}

6
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs

@ -72,11 +72,9 @@ namespace Volo.Abp.TestApp.Application
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("DownloadAsync"));
memoryStream.Position = 0;
return new RemoteStreamContent(memoryStream, "download.rtf")
{
ContentType = "application/rtf"
};
return new RemoteStreamContent(memoryStream, "download.rtf", "application/rtf");
}
public async Task<string> UploadAsync(IRemoteStreamContent streamContent)

119
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/DomainEvents_Tests.cs

@ -2,11 +2,13 @@
using System.Linq;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.EventBus.Distributed;
using Volo.Abp.EventBus.Local;
using Volo.Abp.Modularity;
using Volo.Abp.TestApp.Domain;
using Volo.Abp.Uow;
using Xunit;
namespace Volo.Abp.TestApp.Testing
@ -25,6 +27,113 @@ namespace Volo.Abp.TestApp.Testing
DistributedEventBus = GetRequiredService<IDistributedEventBus>();
}
[Fact]
public virtual async Task Should_Publish_Events_In_Order()
{
bool testPersonCreateHandled = false;
bool douglesUpdateHandled = false;
bool douglesNameChangeHandled = false;
bool customEventHandled = false;
bool customEvent2Handled = false;
LocalEventBus.Subscribe<EntityCreatedEventData<Person>>(data =>
{
data.Entity.Name.ShouldBe("TestPerson1");
testPersonCreateHandled = true;
douglesUpdateHandled.ShouldBeFalse();
douglesNameChangeHandled.ShouldBeFalse();
customEventHandled.ShouldBeFalse();
customEvent2Handled.ShouldBeFalse();
return Task.CompletedTask;
});
LocalEventBus.Subscribe<MyCustomEventData>(data =>
{
data.Value.ShouldBe("42");
customEventHandled = true;
testPersonCreateHandled.ShouldBeTrue();
douglesUpdateHandled.ShouldBeFalse();
douglesNameChangeHandled.ShouldBeFalse();
customEvent2Handled.ShouldBeFalse();
return Task.CompletedTask;
});
LocalEventBus.Subscribe<PersonNameChangedEvent>(data =>
{
data.OldName.ShouldBe("Douglas");
data.Person.Name.ShouldBe("Douglas-Updated");
douglesNameChangeHandled = true;
testPersonCreateHandled.ShouldBeTrue();
customEventHandled.ShouldBeTrue();
douglesUpdateHandled.ShouldBeFalse();
customEvent2Handled.ShouldBeFalse();
return Task.CompletedTask;
});
LocalEventBus.Subscribe<EntityUpdatedEventData<Person>>(data =>
{
data.Entity.Name.ShouldBe("Douglas-Updated");
douglesUpdateHandled = true;
testPersonCreateHandled.ShouldBeTrue();
customEventHandled.ShouldBeTrue();
douglesNameChangeHandled.ShouldBeTrue();
customEvent2Handled.ShouldBeFalse();
return Task.CompletedTask;
});
LocalEventBus.Subscribe<MyCustomEventData2>(data =>
{
data.Value.ShouldBe("44");
customEvent2Handled = true;
testPersonCreateHandled.ShouldBeTrue();
customEventHandled.ShouldBeTrue();
douglesUpdateHandled.ShouldBeTrue();
douglesNameChangeHandled.ShouldBeTrue();
return Task.CompletedTask;
});
await WithUnitOfWorkAsync(new AbpUnitOfWorkOptions{IsTransactional = true}, async () =>
{
await PersonRepository.InsertAsync(
new Person(Guid.NewGuid(), "TestPerson1", 42)
);
await LocalEventBus.PublishAsync(new MyCustomEventData { Value = "42" });
var douglas = await PersonRepository.GetAsync(TestDataBuilder.UserDouglasId);
douglas.ChangeName("Douglas-Updated");
await PersonRepository.UpdateAsync(douglas);
await LocalEventBus.PublishAsync(new MyCustomEventData2 { Value = "44" });
});
}
[Fact]
public virtual async Task Should_Rollback_Uow_If_Event_Handler_Throws_Exception()
{
(await PersonRepository.FindAsync(x => x.Name == "TestPerson1")).ShouldBeNull();
LocalEventBus.Subscribe<EntityCreatedEventData<Person>>(data =>
{
data.Entity.Name.ShouldBe("TestPerson1");
throw new ApplicationException("Just to rollback the UOW");
});
var exception = await Assert.ThrowsAsync<ApplicationException>(async () =>
{
await WithUnitOfWorkAsync(new AbpUnitOfWorkOptions{IsTransactional = true}, async () =>
{
await PersonRepository.InsertAsync(
new Person(Guid.NewGuid(), "TestPerson1", 42)
);
});
});
exception.Message.ShouldBe("Just to rollback the UOW");
(await PersonRepository.FindAsync(x => x.Name == "TestPerson1")).ShouldBeNull();
}
[Fact]
public async Task Should_Trigger_Domain_Events_For_Aggregate_Root()
{
@ -63,5 +172,15 @@ namespace Volo.Abp.TestApp.Testing
isLocalEventTriggered.ShouldBeTrue();
isDistributedEventTriggered.ShouldBeTrue();
}
private class MyCustomEventData
{
public string Value { get; set; }
}
private class MyCustomEventData2
{
public string Value { get; set; }
}
}
}

16
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/EntityChangeEvents_Tests.cs

@ -38,7 +38,9 @@ namespace Volo.Abp.TestApp.Testing
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin())
{
#pragma warning disable 618
LocalEventBus.Subscribe<EntityCreatingEventData<Person>>(data =>
#pragma warning restore 618
{
creatingEventTriggered.ShouldBeFalse();
createdEventTriggered.ShouldBeFalse();
@ -88,12 +90,16 @@ namespace Volo.Abp.TestApp.Testing
[Fact]
public async Task Multiple_Update_Should_Result_With_Single_Updated_Event_In_The_Same_Uow()
{
var personId = Guid.NewGuid();
await PersonRepository.InsertAsync(new Person(personId, Guid.NewGuid().ToString("D"), 42));
var createEventCount = 0;
var updateEventCount = 0;
var updatedAge = 0;
DistributedEventBus.Subscribe<EntityCreatedEto<PersonEto>>(eto =>
{
createEventCount++;
return Task.CompletedTask;
});
DistributedEventBus.Subscribe<EntityUpdatedEto<PersonEto>>(eto =>
{
updateEventCount++;
@ -101,6 +107,9 @@ namespace Volo.Abp.TestApp.Testing
return Task.CompletedTask;
});
var personId = Guid.NewGuid();
await PersonRepository.InsertAsync(new Person(personId, Guid.NewGuid().ToString("D"), 42));
using (var uow = GetRequiredService<IUnitOfWorkManager>().Begin())
{
var person = await PersonRepository.GetAsync(personId);
@ -120,6 +129,7 @@ namespace Volo.Abp.TestApp.Testing
await uow.CompleteAsync();
}
createEventCount.ShouldBe(1);
updateEventCount.ShouldBe(1);
updatedAge.ShouldBe(45);
}

1
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/TestAppTestBase.cs

@ -31,7 +31,6 @@ namespace Volo.Abp.TestApp.Testing
using (var uow = uowManager.Begin(options))
{
await action();
await uow.CompleteAsync();
}
}

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

Loading…
Cancel
Save