Browse Source

Merge branch 'dev' into liangshiwei/inoutbox

pull/11243/head
liangshiwei 4 years ago
parent
commit
82c40b50d9
  1. 2
      README.md
  2. 4
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  3. 7
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  4. 7
      abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json
  5. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json
  6. 16
      docs/en/API/API-Versioning.md
  7. 2
      docs/en/AspNet-Boilerplate-Migration-Guide.md
  8. 2
      docs/en/Background-Workers-Hangfire.md
  9. 10
      docs/en/Background-Workers.md
  10. 51
      docs/en/Blog-Posts/2022-04-05 v5_2_Release_Stable/POST.md
  11. 2
      docs/en/Caching.md
  12. 548
      docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/POST.md
  13. BIN
      docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/concurrency-mismatch.gif
  14. BIN
      docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/optimistic-concurrency.png
  15. 146
      docs/en/Concurrency-Check.md
  16. 30
      docs/en/Distributed-Locking.md
  17. 6
      docs/en/Getting-Started-Create-Solution.md
  18. 2
      docs/en/Modules/Docs.md
  19. 2
      docs/en/Tutorials/Part-2.md
  20. 8
      docs/en/Tutorials/Part-3.md
  21. 2
      docs/en/Tutorials/Part-5.md
  22. 6
      docs/en/Tutorials/Part-9.md
  23. 4
      docs/en/UI/Angular/Component-Replacement.md
  24. 2
      docs/en/UI/Angular/Config-State-Service.md
  25. 31
      docs/en/UI/Angular/Confirmation-Service.md
  26. 62
      docs/en/UI/Angular/Environment.md
  27. 2
      docs/en/UI/Angular/How-Replaceable-Components-Work-with-Extensions.md
  28. 2
      docs/en/UI/Angular/Quick-Start.md
  29. 4
      docs/en/UI/Angular/Testing.md
  30. 2
      docs/en/UI/Angular/Theming.md
  31. 18
      docs/en/UI/Angular/Toaster-Service.md
  32. 71
      docs/en/UI/AspNetCore/AutoComplete-Select.md
  33. 2
      docs/en/UI/AspNetCore/Tag-Helpers/Figure.md
  34. 2
      docs/en/UI/AspNetCore/Theming.md
  35. 40
      docs/en/docs-nav.json
  36. BIN
      docs/en/images/abp-select2-multiple.png
  37. BIN
      docs/en/images/abp-select2-single.png
  38. BIN
      docs/en/images/optimistic-concurrency.png
  39. 2
      docs/pt-BR/Tutorials/Angular/Part-II.md
  40. 2
      docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md
  41. 2
      docs/zh-Hans/Caching.md
  42. 129
      docs/zh-Hans/Domain-Services.md
  43. 2
      docs/zh-Hans/Tutorials/Part-2.md
  44. 11
      docs/zh-Hans/Tutorials/Part-3.md
  45. 2
      docs/zh-Hans/UI/Angular/Component-Replacement.md
  46. 14
      framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Microsoft/AspNetCore/Authentication/OAuth/Claims/AbpClaimActionCollectionExtensions.cs
  47. 3
      framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Microsoft/Extensions/DependencyInjection/AbpOpenIdConnectExtensions.cs
  48. 2
      framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor
  49. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs
  50. 18
      framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationGlobalFeatureConfigurationDto.cs
  51. 4
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js
  52. 61
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpDataAnnotationAutoLocalizationMetadataDetailsProvider.cs
  53. 20
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
  54. 3
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentOutputFormatter.cs
  55. 1
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs
  56. 18
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditPropertySetter.cs
  57. 4
      framework/src/Volo.Abp.AutoMapper/Volo.Abp.AutoMapper.csproj
  58. 3
      framework/src/Volo.Abp.AutoMapper/Volo/Abp/AutoMapper/AbpAutoMapperModule.cs
  59. 2
      framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ConnectionPool.cs
  60. 13
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerBase.cs
  61. 7
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs
  62. 2
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs
  63. 7
      framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/IHangfireBackgroundWorker.cs
  64. 1
      framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor
  65. 5
      framework/src/Volo.Abp.BlazoriseUI/Components/UiMessageAlert.razor.cs
  66. 8
      framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj
  67. 4
      framework/src/Volo.Abp.BlobStoring.Minio/Volo.Abp.BlobStoring.Minio.csproj
  68. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgumentParser.cs
  69. 14
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/NewCommand.cs
  70. 26
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProjectCreationCommandBase.cs
  71. 17
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs
  72. 224
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs
  73. 2
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ConnectionStringChangeStep.cs
  74. 10
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/DatabaseManagementSystemChangeStep.cs
  75. 21
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveFileStep.cs
  76. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs
  77. 18
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs
  78. 9
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/BlazorAppsettingsFilePortChangeForSeparatedIdentityServersStep.cs
  79. 5
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs
  80. 13
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/NpmPackagesUpdater.cs
  81. 12
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs
  82. 39
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs
  83. 4
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionModuleAdder.cs
  84. 92
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs
  85. 16
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs
  86. 6
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/ICmdHelper.cs
  87. 30
      framework/src/Volo.Abp.Core/Volo/Abp/AsyncDisposeFunc.cs
  88. 6
      framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeServicesAttribute.cs
  89. 4
      framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs
  90. 19
      framework/src/Volo.Abp.Core/Volo/Abp/NullAsyncDisposable.cs
  91. 2
      framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpAzureEventBusOptions.cs
  92. 15
      framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpEventBusAzureModule.cs
  93. 15
      framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpRabbitMqEventBusOptions.cs
  94. 4
      framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/RabbitMqDistributedEventBus.cs
  95. 2
      framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionContext.cs
  96. 11
      framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqConsts.cs
  97. 14
      framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs
  98. 1
      framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentTestController_Tests.cs
  99. 25
      framework/test/Volo.Abp.Core.Tests/Volo/Abp/DependencyInjection/AutoRegistrationHelper_Tests.cs
  100. 56
      framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs

2
README.md

@ -110,7 +110,7 @@ Love ABP Framework? **Please give a star** to this repository :star:
## Discord Channel
You can use this link to join the ABP Community Discord Server: https://discord.gg/uVGt6hyhcm
You can use this link to join the ABP Community Discord Server: https://discord.gg/abp
## ABP Commercial

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

@ -380,6 +380,8 @@
"PurchaseItems": "Purchase Items",
"SuccessfullyUpdated": "Successfully updated",
"SuccessfullyAdded": "Successfully added",
"PurchaseState": "Purchase State"
"PurchaseState": "Purchase State",
"ShowBetweenDayCount": "Show Between Days",
"PurchaseOrder": "Purchase Order"
}
}

7
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json

@ -84,7 +84,7 @@
"WouldLikeToReceiveMarketingMaterials": "I would like to receive marketing materials like product deals & special offers.",
"JoinOurMarketingNewsletter": "Join our marketing newsletter",
"CommunityPrivacyPolicyConfirmation": "I agree to the Terms & Conditions and <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"ABPIO-Common": "ABPIO-Common",
"WouldLikeToReceiveNotification": "I would like to receive the latest news from abp.io websites.",
"CommercialNewsletterConfirmationMessage": "I agree to the <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/TermsConditions\">Terms & Conditions</a> and <a class=\"text-white fw-6 text-decoration-underline opacity-50\" href=\"https://commercial.abp.io/Privacy\">Privacy Policy</a>.",
"FreeDDDEBook": "Free DDD E-Book",
"AdditionalServices": "Additional Services",
@ -118,6 +118,9 @@
"ThereIsNoEvent": "There is no event.",
"Events": "Events",
"Volo.AbpIo.Domain:080000": "There is already a purchase item named \"{Name}\"",
"MasteringAbpFrameworkBook": "Book: Mastering ABP Framework"
"MasteringAbpFrameworkBook": "Book: Mastering ABP Framework",
"ABPIO-CommonPreferenceDefinition": "Get the latest news about ABP Platform like new posts, events and more.",
"BuiltOn": "Built-on",
"AbpFramework": "ABP Framework"
}
}

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

@ -495,7 +495,9 @@
"LicenseTypeNotCorrect": "The license type is not correct!",
"Trainings": "Trainings",
"ChoseTrainingPlaceholder": "Chose the training...",
"ContactUsToGetQuote": "Contact us to get a quote",
"DoYouNeedTrainings": "Do you need one of these trainings?",
"DoYouNeedTraining": "Do you need {0} training?",
"GetInTouchUs": "Get in touch with us",
"ForMoreInformationClickHere": "For more information, click <a href='{0}'>here.</a>",
"IsGetOnboardingTraining": "Would you like to get onboarding & web application development training?",
"OnboardingWebApplicationDevelopmentTrainingMessage": "To schedule your training calendar, please contact {0} after creating the organization",
@ -503,6 +505,7 @@
"AdditionalNote": "Additional Note",
"OnboardingTrainingFaqTitle": "Do you have ABP onboarding training?",
"OnboardingTrainingFaqExplanation": "Yes, we have ABP Training Services to help you get your ABP project started fast. You will learn about ABP from an ABP core team member and you will get the skills to begin your ABP project. In the onboarding training, we will explain how to set up your development environment, install the required tools, create a fully functional CRUD page. The training will be live and the Zoom application will be used, and we are open to using other online meeting platforms. The language of the training will be English. You can also ask your questions about ABP during the sessions. A convenient time and date will be planned for both parties. To get more information, contact us at <a href=\"mailto:info@abp.io\">info@abp.io</a>.",
"AddBasket": "Add to Basket"
"AddBasket": "Add to Basket",
"SendTrainingRequest": "Send Training Request"
}
}

2
abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json

@ -183,7 +183,7 @@
"InstallABPCLIInfo": "ABP CLI is the fastest way to start a new solution with the ABP framework. Install the ABP CLI using a command line window:",
"DifferentLevelOfNamespaces": "You can use different levels of namespaces; e.g. BookStore, Acme.BookStore or Acme.Retail.BookStore.",
"ABPCLIExamplesInfo": "The <strong>new</strong> command creates a <strong>layered MVC application</strong> with <strong>Entity Framework Core</strong> as the database provider. However, it has additional options.",
"SeeCliDocumentForMoreInformation": "Check out the <a href=\"{0}\">ABP CLI document</a> for more options or select the \"Direct Download\" tab above.",
"SeeCliDocumentForMoreInformation": "Check out the <a target=\"_blank\" href=\"{0}\">ABP CLI document</a> for more options or select the \"Direct Download\" tab above.",
"Optional": "Optional",
"LocalFrameworkRef": "Keep the local project reference for the framework packages.",
"BlobStoring": "BLOB Storing",

16
docs/en/API/API-Versioning.md

@ -26,7 +26,7 @@ public override void ConfigureServices(ServiceConfigurationContext context)
## C# and JavaScript Static Client Proxies
This feature does not compatible with [URL Path Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), I suggest you always use [Versioning-via-the-Query-String](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String).
This feature does not compatible with [URL Path Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), we suggest to use [Versioning-via-the-Query-String](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String).
### Example
@ -45,7 +45,7 @@ public interface IBookV2AppService : IApplicationService
}
```
**HttpApi Controillers:**
**HttpApi Controllers:**
```cs
[Area(BookStoreRemoteServiceConsts.ModuleName)]
[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)]
@ -175,7 +175,7 @@ bookStore.books.bookV2.getAsyncByIsbn = function(isbn, api_version, ajaxParams)
```
## Manually change version
## Changing version manually
If an application service class supports multiple versions. You can inject `ICurrentApiVersionInfo` to switch versions in C#.
@ -337,13 +337,15 @@ public override void OnApplicationInitialization(ApplicationInitializationContex
}
```
## Custom multi-version API controller.
## Custom multi-version API controller
ABP Framework will not affect to your APIs, you can freely implement your APIs according to Microsoft's documentation.
ABP Framework will not affect to your APIs, you can freely implement your APIs according to the Microsoft's documentation.
Get more from https://github.com/dotnet/aspnet-api-versioning/wiki
Further information, see https://github.com/dotnet/aspnet-api-versioning/wiki
## Sample source code
You can get the complete sample source code in https://github.com/abpframework/abp-samples/tree/master/Api-Versioning
Follow the link below to get the sample's complete source-code
https://github.com/abpframework/abp-samples/tree/master/Api-Versioning

2
docs/en/AspNet-Boilerplate-Migration-Guide.md

@ -1,4 +1,4 @@
# ASP.NET Boilerplate v5+ to ABP Framework Migration
# Migrating from ASP.NET Boilerplate to the ABP Framework
ABP Framework is **the successor** of the open source [ASP.NET Boilerplate](https://aspnetboilerplate.com/) framework. This guide aims to help you to **migrate your existing solutions** (you developed with the ASP.NET Boilerplate framework) to the ABP Framework.

2
docs/en/Background-Workers-Hangfire.md

@ -126,4 +126,4 @@ context.ServiceProvider
So, it resolves the given background worker and adds to the `IBackgroundWorkerManager`.
While we generally add workers in OnApplicationInitialization, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down.
While we generally add workers in OnApplicationInitialization, there are no restrictions on that. You can inject IBackgroundWorkerManager anywhere and add workers at runtime. Background worker manager will stop and release all the registered workers when your application is being shut down.

10
docs/en/Background-Workers.md

@ -86,20 +86,20 @@ After creating a background worker class, you should add it to the `IBackgroundW
[DependsOn(typeof(AbpBackgroundWorkersModule))]
public class MyModule : AbpModule
{
public override void OnApplicationInitialization(
public override Task OnApplicationInitializationAsync(
ApplicationInitializationContext context)
{
context.AddBackgroundWorker<PassiveUserCheckerWorker>();
context.AddBackgroundWorkerAsync<PassiveUserCheckerWorker>();
}
}
````
`context.AddBackgroundWorker(...)` is a shortcut extension method for the expression below:
`context.AddBackgroundWorkerAsync(...)` is a shortcut extension method for the expression below:
````csharp
context.ServiceProvider
await context.ServiceProvider
.GetRequiredService<IBackgroundWorkerManager>()
.Add(
.AddAsync(
context
.ServiceProvider
.GetRequiredService<PassiveUserCheckerWorker>()

51
docs/en/Blog-Posts/2022-04-05 v5_2_Release_Stable/POST.md

@ -0,0 +1,51 @@
# ABP.IO Platform 5.2 Final Has Been Released!
[ABP Framework](https://abp.io/) and [ABP Commercial](https://commercial.abp.io/) 5.2 versions have been released today.
## What's New With 5.2?
Since all the new features are already explained in details with the [5.2 RC Announcement Post](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published), I will not repeat all the details again. See the [RC Blog Post](https://blog.abp.io/abp/ABP.IO-Platform-5-2-RC-Has-Been-Published) for all the features and enhancements.
## Creating New Solutions
You can create a new solution with the ABP Framework version 5.2 by either using the `abp new` command or using the **direct download** tab on the [get started page](https://abp.io/get-started).
> See the [getting started document](https://docs.abp.io/en/abp/latest/Getting-Started) for more.
## How to Upgrade an Existing Solution
### Install/Update the ABP CLI
First of all, install the ABP CLI or upgrade to the latest version.
If you haven't installed yet:
```bash
dotnet tool install -g Volo.Abp.Cli
```
To update an existing installation:
```bash
dotnet tool update -g Volo.Abp.Cli
```
### ABP UPDATE Command
[ABP CLI](https://docs.abp.io/en/abp/latest/CLI) provides a handy command to update all the ABP related NuGet and NPM packages in your solution with a single command:
```bash
abp update
```
Run this command in the root folder of your solution.
## Migration Guide
Check [the migration guide](https://docs.abp.io/en/abp/5.2/Migration-Guides/Abp-5_2) for the applications with the version 5.x upgrading to the version 5.2.
## About the Next Version
The next feature version will be 5.3. It is planned to release the 5.3 RC (Release Candidate) on May 03 and the final version on May 31, 2022. You can follow the [release planning here](https://github.com/abpframework/abp/milestones).
Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problem with this version.

2
docs/en/Caching.md

@ -258,7 +258,7 @@ ABP's distributed cache interfaces provide methods to perform batch methods thos
Distributed cache service provides an interesting feature. Assume that you've updated the price of a book in the database, then set the new price to the cache, so you can use the cached value later. What if you have an exception after setting the cache and you **rollback the transaction** that updates the price of the book? In this case, cache value will be incorrect.
`IDistributedCache<..>` methods gets an optional parameter, named `considerOuw`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**.
`IDistributedCache<..>` methods gets an optional parameter, named `considerUow`, which is `false` by default. If you set it to `true`, then the changes you made for the cache are not actually applied to the real cache store, but associated with the current [unit of work](Unit-Of-Work.md). You get the value you set in the same unit of work, but the changes are applied **only if the current unit of work succeed**.
### IDistributedCacheSerializer

548
docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/POST.md

@ -0,0 +1,548 @@
# Handle Concurrency with EF Core in an ABP Framework Project with ASP.NET Core MVC
In this article, we'll create a basic application to demonstrate how "Concurrency Check/Control" can be implemented in an ABP project.
## Creating the Solution
For this article, we will create a simple BookStore application and add CRUD functionality to the pages. Hence we deal with the concurrency situation.
We can create a new startup template with EF Core as a database provider and MVC for the UI Framework.
> If you already have a project, you don't need to create a new startup template, you can directly implement the following steps to your project. So you can skip this section.
We can create a new startup template by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI).
```bash
abp new Acme.BookStore
```
After running the above command, our project boilerplate will be downloaded. Then we can open the solution and start the development.
## Starting the Development
Let's start with defining our entities.
### Creating Entities
Create a `Book.cs` (/Books/Book.cs) class in the `.Domain` layer:
```csharp
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
```
* To enable **Concurrency Check** for our entities, our entities should be implemented the `IHasConcurrencyStamp` interface, directly or indirectly.
* [Aggregate Root](https://docs.abp.io/en/abp/5.2/Entities#aggregateroot-class) entity classes already implement the `IHasConcurrencyStamp` interface, so if we inherit our entities from one of these entity classes then we won't need to manually implement the `IHasConcurrencyStamp` interface.
* And we've derived the `Book` entity from `AuditedAggregateRoot<TKey>` here, so we don't need to implement the `IHasConcurrencyStamp` interface because `AuditedAggregateRoot` class already implemented the `IHasConcurrencyStamp` interface.
> You can read more details from the [Concurrency Check](https://docs.abp.io/en/abp/5.2/Concurrency-Check) documentation.
Then, create a `BookType` (/Books/BookType.cs) enum in the `.Domain.Shared` layer:
```csharp
public enum BookType
{
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry
}
```
### Database Integration
Open the `BookStoreDbContext` (/EntityFrameworkCore/BookStoreDbContext.cs) class in the `*.EntityFrameworkCore` project and add the following `DbSet<Book>` statement:
```csharp
namespace Acme.BookStore.EntityFrameworkCore;
[ReplaceDbContext(typeof(IIdentityDbContext))]
[ReplaceDbContext(typeof(ITenantManagementDbContext))]
[ConnectionStringName("Default")]
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
//Entities from the modules
public DbSet<Book> Books { get; set; } //add this line
}
```
Then we can navigate to the `OnModelCreating` method in the same class and configure our tables/entities:
```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<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
}
```
After the mapping configurations, we can create a new migration and apply changes to the database.
To do this, open your command line terminal in the directory of the `EntityFrameworkCore` project and run the below command:
```bash
dotnet ef migrations add Added_Books
```
After this command, a new migration will be generated and then we can run the `*.DbMigrator` project to apply the last changes to the database such as creating a new table named `Books` according to the last created migration.
### Defining DTOs and Application Service Interfaces
We can start to define the use cases of the application.
Create the DTO classes (under the **Books** folder) in the `Application.Contracts` project:
**BookDto.cs**
```csharp
public class BookDto : AuditedEntityDto<Guid>, IHasConcurrencyStamp
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
public string ConcurrencyStamp { get; set; }
}
```
* The `AuditedEntityDto<TKey>` class is not implemented from the `IHasConcurrencyStamp` interface, so for the **BookDto** class we need to implement the `IHasConcurrencyStamp`.
* This is important, because we need to return books with their **ConcurrencyStamp** value.
**CreateBookDto.cs**
```csharp
public class CreateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
}
```
**UpdateBookDto.cs**
```csharp
public class UpdateBookDto : IHasConcurrencyStamp
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
public string ConcurrencyStamp { get; set; }
}
```
* Here, we've implemented the `IHasConcurrencyStamp` interface for the **UpdateBookDto** class.
* We will use this value while updating an existing book. ABP Framework will compare the current book's **ConcurrencyStamp** value with the provided one, if values are matched, this means everything is as it is supposed to be and will update the record.
* If values are mismatched, then it means the record that we're trying to update is already updated by another user and we need to get the latest changes to be able to make changes on it.
* Also, in that case, `AbpDbConcurrencyException` will be thrown by the ABP Framework and we can either handle this exception manually or let the ABP Framework handle it on behalf of us and show a user-friendly error message as in the image below.
![](./optimistic-concurrency.png)
Create a new `IBookAppService` (/Books/IBookAppService.cs) interface in the `Application.Contracts` project:
```csharp
public interface IBookAppService :
ICrudAppService<BookDto, Guid, PagedAndSortedResultRequestDto, CreateBookDto, UpdateBookDto>
{
}
```
* We've implemented the `ICrudAppService` here, because we just need to perform CRUD operations and this interface helps us define common CRUD operation methods.
### Application Service Implementations
Create a `BookAppService` (/Books/BookAppService.cs) class inside the `*.Application` project and implement the application service methods, as shown below:
```csharp
public class BookAppService :
CrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto, CreateBookDto, UpdateBookDto>,
IBookAppService
{
public BookAppService(IRepository<Book, Guid> repository)
: base(repository)
{
}
public override async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await Repository.GetAsync(id);
book.Name = input.Name;
book.Price = input.Price;
book.Type = input.Type;
book.PublishDate = input.PublishDate;
//set Concurrency Stamp value to the entity
book.ConcurrencyStamp = input.ConcurrencyStamp;
var updatedBook = await Repository.UpdateAsync(book);
return ObjectMapper.Map<Book, BookDto>(updatedBook);
}
}
```
* We've used the `CrudAppService` base class. This class implements all common CRUD operations and if we want to change a method, we can simply override the method and change it to our needs.
> Normally, you don't need to override the `UpdateAsync` method to do **Concurrency Check**. Because the `UpdateAsync` method of the `CrudAppService` class by default map input values to the entity. But I wanted to override this method to show what we need to do for **Concurrency Check**.
* We can look closer to the `UpdateAsync` method here, because as we've mentioned earlier we need to pass the provided **ConcurrencyStamp** value to be able to do **Concurrency Check/Control** to our entity while updating.
* At that point, if the given record is already updated by any other user, a **ConcurrencyStamp** mismatch will occur and `AbpDbConcurrencyException` will be thrown thanks to the **Concurrency Check** system of ABP, data-consistency will be provided and the current record won't be overridden.
* And if the values are matched, the record will be updated successfully.
After implementing the application service methods, we can do the related mapping configurations, so open the `BookStoreApplicationAutoMapperProfile.cs` and update the content as below:
```csharp
public class BookStoreApplicationAutoMapperProfile : Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateBookDto, Book>();
}
}
```
### User Interface
So far, we've applied the all necessary steps for the **Concurrency Check** system, let's see it in action.
Create a razor page in the `.Web` layer named `Index` (**/Pages/Books/Index.cshtml**), open this file and replace the content with the following code block:
```html
@page
@using Acme.BookStore.Localization
@using Microsoft.Extensions.Localization
@model Acme.BookStore.Web.Pages.Books.Index
@section scripts
{
<abp-script src="/Pages/Books/Index.js" />
}
<abp-card>
<abp-card-header>
<abp-row>
<abp-column size-md="_6">
<abp-card-title>Books</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-end">
<abp-button id="NewBookButton"
text="New Book"
icon="plus"
button-type="Primary"/>
</abp-column>
</abp-row>
</abp-card-header>
<abp-card-body>
<abp-table striped-rows="true" id="BooksTable"></abp-table>
</abp-card-body>
</abp-card>
```
* We've defined a table and "New Book" button inside a card element here, we'll fill the table with our book records in the next step by using the **Datatables** library.
Create an `Index.js` (**/Pages/Books/Index.js**) file and add the following code block:
```js
$(function () {
var l = abp.localization.getResource('BookStore');
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
var dataTable = $('#BooksTable').DataTable(
abp.libs.datatables.normalizeConfiguration({
serverSide: true,
paging: true,
order: [[1, "asc"]],
searching: false,
scrollX: true,
ajax: abp.libs.datatables.createAjax(acme.bookStore.books.book.getList),
columnDefs: [
{
title: l('Actions'),
rowAction: {
items:
[
{
text: l('Edit'),
action: function (data) {
editModal.open({ id: data.record.id });
}
}
]
}
},
{
title: l('Name'),
data: "name"
},
{
title: l('Type'),
data: "type",
render: function (data) {
return l('Enum:BookType:' + data);
}
},
{
title: l('PublishDate'),
data: "publishDate",
render: function (data) {
return luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
}).toLocaleString();
}
},
{
title: l('Price'),
data: "price"
},
{
title: l('CreationTime'),
data: "creationTime",
render: function (data) {
return luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
}).toLocaleString(luxon.DateTime.DATETIME_SHORT);
}
}
]
})
);
createModal.onResult(function () {
dataTable.ajax.reload();
});
editModal.onResult(function () {
dataTable.ajax.reload();
});
$('#NewBookButton').click(function (e) {
e.preventDefault();
createModal.open();
});
});
```
* We've used the [Datatables](https://datatables.net/) to list our books.
* Also defined **create** and **update** modals by using [ABP Modal Manager](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Modals#modalmanager-reference), but we didn't create them yet, so let's create the modals.
First, create a **CreateModal** razor page and update the **CreateModal.cshtml** and **CreateModal.cshtml.cs** files as below:
**CreateModal.cshtml**
```html
@page
@using Acme.BookStore.Web.Pages.Books
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model CreateModalModel
@{
Layout = null;
}
<abp-dynamic-form abp-model="Book" asp-page="/Books/CreateModal">
<abp-modal>
<abp-modal-header title="New Book"></abp-modal-header>
<abp-modal-body>
<abp-form-content />
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
</abp-dynamic-form>
```
* We've used `abp-dynamic-form` tag-helper and passed it a `Book` model, this tag helper will simply create form contents (inputs, select boxes etc.) on behalf of us.
* **CreateModal.cshtml.cs**
```csharp
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Microsoft.AspNetCore.Mvc;
namespace Acme.BookStore.Web.Pages.Books;
public class CreateModalModel : BookStorePageModel
{
[BindProperty]
public CreateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService;
public CreateModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public void OnGet()
{
Book = new CreateBookDto();
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.CreateAsync(Book);
return NoContent();
}
}
```
* In this file, we simply define **CreateBookDto** as a bind property and we'll use this class's properties in the form. Thanks to the `abp-dynamic-form` tag-helper we don't need to define all of these form elements one by one, it will generate on behalf of us.
We can create an **EditModal** razor page and update the **EditModal.cshtml** and **EditModal.cshtml.cs** files as below:
**EditModal.cshtml**
```html
@page
@using Acme.BookStore.Web.Pages.Books
@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal
@model EditModalModel
@{
Layout = null;
}
<form asp-page="/Books/EditModal">
<abp-modal>
<abp-modal-header title="Update"></abp-modal-header>
<abp-modal-body>
<abp-input asp-for="Id"/>
<abp-input asp-for="Book.Name"/>
<abp-input asp-for="Book.Price"/>
<abp-select asp-for="Book.Type"/>
<abp-input asp-for="Book.PublishDate"/>
<abp-input asp-for="Book.ConcurrencyStamp" type="hidden"/>
</abp-modal-body>
<abp-modal-footer buttons="@(AbpModalButtons.Cancel|AbpModalButtons.Save)"></abp-modal-footer>
</abp-modal>
</form>
```
* Here, we didn't use the `abp-dynamic-form` tag-helper and added all the necessary form elements to our form one by one.
* As you may have noticed, we've set the input type as **hidden** for the **ConcurrencyStamp** input, because the end-user should not see this value.
> Instead of doing it like that, we could create a view model class and use the `[HiddenInput]` data attribute for the **ConcurrencyStamp** property and use the `abp-dynamic-form` tag-helper. But to simplify the article I didn't want to do that, if you want you can create a view model and define the necessary data attributes for properties.
**EditModal.cshtml.cs**
```csharp
public class EditModalModel : BookStorePageModel
{
[HiddenInput]
[BindProperty(SupportsGet = true)]
public Guid Id { get; set; }
[BindProperty]
public UpdateBookDto Book { get; set; }
private readonly IBookAppService _bookAppService;
public EditModalModel(IBookAppService bookAppService)
{
_bookAppService = bookAppService;
}
public async Task OnGetAsync()
{
var bookDto = await _bookAppService.GetAsync(Id);
Book = ObjectMapper.Map<BookDto, UpdateBookDto>(bookDto);
}
public async Task<IActionResult> OnPostAsync()
{
await _bookAppService.UpdateAsync(Id, Book);
return NoContent();
}
}
```
Lastly, we can define the necessary mapping configurations and run the application to see the result.
Open the `BookStoreWebAutoMapperProfile.cs` class and update the content as below:
```csharp
public class BookStoreWebAutoMapperProfile : Profile
{
public BookStoreWebAutoMapperProfile()
{
CreateMap<BookDto, UpdateBookDto>();
}
}
```
Then we can run the application, navigate to the **/Books** endpoint and see the result.
![](concurrency-mismatch.gif)
* In the image above, we can see that multiple users open the edit model to change a record and try to update the relevant record independently of each other.
* After the first user updated the record, the second user tries to update the same record without getting the last state of the record. And therefore `AbpDbConcurrencyException` is thrown because **ConcurrencyStamp** values are different from each other.
* The second user should close and re-open the model to get the last state of the record and then they can make changes to the current record.

BIN
docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/concurrency-mismatch.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
docs/en/Community-Articles/2022-04-06-Concurrency-Check-in-ABP-Based-Applications/optimistic-concurrency.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

146
docs/en/Concurrency-Check.md

@ -1,3 +1,147 @@
## Concurrency Check
TODO
### Introduction
Concurrency Check (also known as **Concurrency Control**) refers to specific mechanisms used to ensure data consistency in the presence of concurrent changes (multiple processes, users access or change the same data in a database at the same time).
There are two commonly used concurrency control mechanisms/approaches:
* **Optimistic Concurrency Control**: Optimistic Concurrency Control allows multiple users to attempt to **update** the same record without informing the users that others are also attempting to **update** it.
* If a user successfully updates the record, the other users need to get the latest changes for the current record to be able to make changes.
* ABP's concurrency check system uses the **Optimistic Concurrency Control**.
* **Pessimistic Concurrency Control**: Pessimistic Concurrency Control prevents simultaneous updates to records and uses a locking mechanism. For more information please see [here](https://www.martinfowler.com/eaaCatalog/pessimisticOfflineLock.html).
### Usage
#### `IHasConcurrencyStamp` Interface
To enable **concurrency control** to your entity class, you should implement the `IHasConcurrencyStamp` interface, directly or indirectly.
```csharp
public interface IHasConcurrencyStamp
{
public string ConcurrencyStamp { get; set; }
}
```
* It is the base interface for **concurrency control** and only has a simple property named `ConcurrencyStamp`.
* While a new record is **creating**, if the entity implements the `IHasConcurrencyStamp` interface, ABP Framework automatically sets a unique value to the **ConcurrencyStamp** property.
* While a record is **updating**, ABP Framework compares the **ConcurrencyStamp** property of the entity with the provided **ConcurrencyStamp** value by the user and if the values match, it automatically updates the **ConcurrencyStamp** property with the new unique value. If there is a mismatch, `AbpDbConcurrencyException` is thrown.
**Example: Applying Concurrency Control for the Book Entity**
Implement the `IHasConcurrencyStamp` interface for your entity:
```csharp
public class Book : Entity<Guid>, IHasConcurrencyStamp
{
public string ConcurrencyStamp { get; set; }
//...
}
```
Also, implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface:
```csharp
public class BookDto : EntityDto<Guid>, IHasConcurrencyStamp
{
//...
public string ConcurrencyStamp { get; set; }
}
public class UpdateBookDto : IHasConcurrencyStamp
{
//...
public string ConcurrencyStamp { get; set; }
}
```
Set the **ConcurrencyStamp** input value to the entity in the **UpdateAsync** method of your application service as below:
```csharp
public class BookAppService : ApplicationService, IBookAppService
{
//...
public virtual async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await BookRepository.GetAsync(id);
book.ConcurrencyStamp = input.ConcurrencyStamp;
//set other input values to the entity ...
await BookRepository.UpdateAsync(book);
}
}
```
* After that, when multiple users try to update the same record at the same time, the concurrency stamp mismatch occurs and `AbpDbConcurrencyException` is thrown.
#### Base Classes
[Aggregate Root](./Entities.md#aggregateroot-class) entity classes already implement the `IHasConcurrencyStamp` interface. So, if you are deriving from one of these base classes, you don't need to manually implement the `IHasConcurrencyStamp` interface:
- `AggregateRoot`, `AggregateRoot<TKey>`
- `CreationAuditedAggregateRoot`, `CreationAuditedAggregateRoot<TKey>`
- `AuditedAggregateRoot`, `AuditedAggregateRoot<TKey>`
- `FullAuditedAggregateRoot`, `FullAuditedAggregateRoot<TKey>`
**Example: Applying Concurrency Control for the Book Entity**
You can inherit your entity from one of [the base classes](#base-classes):
```csharp
public class Book : FullAuditedAggregateRoot<Guid>
{
//...
}
```
Then, you can implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface:
```csharp
public class BookDto : EntityDto<Guid>, IHasConcurrencyStamp
{
//...
public string ConcurrencyStamp { get; set; }
}
public class UpdateBookDto : IHasConcurrencyStamp
{
//...
public string ConcurrencyStamp { get; set; }
}
```
Set the **ConcurrencyStamp** input value to the entity in the **UpdateAsync** method of your application service as below:
```csharp
public class BookAppService : ApplicationService, IBookAppService
{
//...
public virtual async Task<BookDto> UpdateAsync(Guid id, UpdateBookDto input)
{
var book = await BookRepository.GetAsync(id);
book.ConcurrencyStamp = input.ConcurrencyStamp;
//set other input values to the entity ...
await BookRepository.UpdateAsync(book);
}
}
```
After that, when multiple users try to update the same record at the same time, the concurrency stamp mismatch occurs and `AbpDbConcurrencyException` is thrown. You can either handle the exception manually or let the ABP Framework handle it for you.
ABP Framework shows a user-friendly error message as in the image below, if you don't handle the exception manually.
![Optimistic Concurrency](./images/optimistic-concurrency.png)

30
docs/en/Distributed-Locking.md

@ -27,21 +27,25 @@ using Medallion.Threading.Redis;
namespace AbpDemo
{
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
var connection = ConnectionMultiplexer
[DependsOn(
typeof(AbpDistributedLockingModule)
//If you have the other dependencies, you should do here
)]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
context.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
var connection = ConnectionMultiplexer
.Connect(configuration["Redis:Configuration"]);
return new
return new
RedisDistributedSynchronizationProvider(connection.GetDatabase());
});
}
}
});
}
}
}
````

6
docs/en/Getting-Started-Create-Solution.md

@ -41,19 +41,19 @@ abp new Acme.BookStore{{if UI == "NG"}} -u angular{{else if UI == "Blazor"}} -u
> [ABP CLI document](./CLI.md) covers all of the available commands and options.
## Mobile Development
### Mobile Development
If you want to include a [React Native](https://reactnative.dev/) project in your solution, add `-m react-native` (or `--mobile react-native`) argument to project creation command. This is a basic React Native startup template to develop mobile applications integrated to your ABP based backends.
See the [Getting Started with the React Native](Getting-Started-React-Native.md) document to learn how to configure and run the React Native application.
### The Solution Structure
## The Solution Structure
The solution has a layered structure (based on the [Domain Driven Design](Domain-Driven-Design.md)) and contains unit & integration test projects. See the [application template document](Startup-Templates/Application.md) to understand the solution structure in details.
{{ if DB == "Mongo" }}
#### MongoDB Transactions
## MongoDB Transactions
The [startup template](Startup-templates/Index.md) **disables** transactions in the `.MongoDB` project by default. If your MongoDB server supports transactions, you can enable it in the *YourProjectMongoDbModule* class's `ConfigureServices` method:

2
docs/en/Modules/Docs.md

@ -16,7 +16,7 @@ Docs module is an application module and does not offer any hosting solution. Yo
When you use GitHub to store your docs, Docs Module supports versioning. If you have multiple versions for your docs, there will be a combo-box on the UI to switch between versions. If you choose file system to store your docs, it does not support multiple versions.
[The documents](docs.abp.io) for ABP framework is also using this module.
[The documents](https://docs.abp.io/) for ABP framework is also using this module.
> Docs module follows the [module architecture best practices](../Best-Practices/Module-Architecture.md) guide.

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

@ -512,7 +512,7 @@ Open the `/src/app/book/book.component.html` and replace the content as shown be
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
</div>
<div class="text-right col col-md-6"></div>
<div class="text-end col col-md-6"></div>
</div>
</div>
<div class="card-body">

8
docs/en/Tutorials/Part-3.md

@ -145,7 +145,7 @@ Open the `Pages/Books/Index.cshtml` and set the content of `abp-card-header` tag
<abp-column size-md="_6">
<abp-card-title>@L["Books"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
<abp-column size-md="_6" class="text-end">
<abp-button id="NewBookButton"
text="@L["NewBook"].Value"
icon="plus"
@ -175,7 +175,7 @@ The final content of `Index.cshtml` is shown below:
<abp-column size-md="_6">
<abp-card-title>@L["Books"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
<abp-column size-md="_6" class="text-end">
<abp-button id="NewBookButton"
text="@L["NewBook"].Value"
icon="plus"
@ -687,10 +687,10 @@ Open `/src/app/book/book.component.html` and make the following changes:
<div class="col col-md-6">
<h5 class="card-title">{%{{{ '::Menu:Books' | abpLocalization }}}%}</h5>
</div>
<div class="text-right col col-md-6">
<div class="text-end col col-md-6">
<!-- Add the "new book" button here -->
<div class="text-lg-right pt-2">
<div class="text-lg-end pt-2">
<button id="create" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i>
<span>{%{{{ "::NewBook" | abpLocalization }}}%}</span>

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

@ -227,7 +227,7 @@ Open the `Pages/Books/Index.cshtml` file and change the content as shown below:
<abp-column size-md="_6">
<abp-card-title>@L["Books"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
<abp-column size-md="_6" class="text-end">
@if (await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create))
{
<abp-button id="NewBookButton"

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

@ -73,7 +73,7 @@ Create a new razor page, `Index.cshtml` under the `Pages/Authors` folder of the
<abp-column size-md="_6">
<abp-card-title>@L["Authors"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
<abp-column size-md="_6" class="text-end">
@if (await AuthorizationService
.IsGrantedAsync(BookStorePermissions.Authors.Create))
{
@ -722,8 +722,8 @@ Open the `/src/app/author/author.component.html` and replace the content as belo
{%{{{ '::Menu:Authors' | abpLocalization }}}%}
</h5>
</div>
<div class="text-right col col-md-6">
<div class="text-lg-right pt-2">
<div class="text-end col col-md-6">
<div class="text-lg-end pt-2">
<button *abpPermission="'BookStore.Authors.Create'" id="create" class="btn btn-primary" type="button" (click)="createAuthor()">
<i class="fa fa-plus mr-1"></i>
<span>{%{{{ '::NewAuthor' | abpLocalization }}}%}</span>

4
docs/en/UI/Angular/Component-Replacement.md

@ -227,7 +227,7 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac
abpEllipsis="210px"
[abpEllipsisEnabled]="!smallScreen"
role="button"
class="btn d-block text-left dropdown-toggle"
class="btn d-block text-start dropdown-toggle"
>
<i class="fa fa-id-card-o"></i>
{%{{{ 'AbpIdentity::Menu:IdentityManagement' | abpLocalization }}}%}
@ -264,7 +264,7 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac
abpEllipsis="210px"
[abpEllipsisEnabled]="!smallScreen"
role="button"
class="btn d-block text-left dropdown-toggle"
class="btn d-block text-start dropdown-toggle"
>
<i class="fa fa-users"></i>
{%{{{ 'AbpTenantManagement::Menu:TenantManagement' | abpLocalization }}}%}

2
docs/en/UI/Angular/Config-State-Service.md

@ -110,7 +110,7 @@ this.config.getSetting$("Abp.Identity.TwoFactor.Behaviour").subscribe(twoFactorB
#### State Properties
Please refer to `ApplicationConfiguration.Response` type for all the properties you can get with `getOne` and `getDeep`. It can be found in the [application-configuration.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/application-configuration.ts#L4).
Please refer to `ApplicationConfigurationDto` type for all the properties you can get with `getOne` and `getDeep`. It can be found in the [models.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/proxy/volo/abp/asp-net-core/mvc/application-configurations/models.ts#L11).
## Set State

31
docs/en/UI/Angular/Confirmation-Service.md

@ -2,12 +2,10 @@
You can use the `ConfirmationService` in @abp/ng.theme.shared package to display a confirmation popup by placing at the root level in your project.
## Getting Started
You do not have to provide the `ConfirmationService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { ConfirmationService } from '@abp/ng.theme.shared';
@ -26,7 +24,7 @@ You can use the `success`, `warn`, `error`, and `info` methods of `ConfirmationS
### How to Display a Confirmation Popup
```js
const confirmationStatus$ = this.confirmation.success('Message', 'Title');
const confirmationStatus$ = this.confirmation.success("Message", "Title");
```
- The `ConfirmationService` methods accept three parameters that are `message`, `title`, and `options`.
@ -48,18 +46,16 @@ this.confirmation
});
```
- The `message` and `title` parameters accept a string, localization key or localization object. See the [localization document](./Localization.md)
- `Confirmation.Status` is an enum and has three properties;
- `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button.
- `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button.
- `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape or clicking the backdrop.
- `Confirmation.Status.confirm` is a closing event value that will be emitted when the popup is closed by the confirm button.
- `Confirmation.Status.reject` is a closing event value that will be emitted when the popup is closed by the cancel button.
- `Confirmation.Status.dismiss` is a closing event value that will be emitted when the popup is closed by pressing the escape or clicking the backdrop.
If you are not interested in the confirmation status, you do not have to subscribe to the returned observable:
```js
this.confirmation.error('You are not authorized.', 'Error');
this.confirmation.error("You are not authorized.", "Error");
```
### How to Display a Confirmation Popup With Given Options
@ -71,9 +67,9 @@ const options: Partial<Confirmation.Options> = {
hideCancelBtn: false,
hideYesBtn: false,
dismissible: false,
cancelText: 'Close',
yesText: 'Confirm',
messageLocalizationParams: ['Demo'],
cancelText: "Close",
yesText: "Confirm",
messageLocalizationParams: ["Demo"],
titleLocalizationParams: [],
// You can customize icon
// icon: 'fa fa-exclamation-triangle', // or
@ -81,9 +77,9 @@ const options: Partial<Confirmation.Options> = {
}
this.confirmation.warn(
'AbpIdentity::RoleDeletionConfirmationMessage',
'Are you sure?',
options,
"AbpIdentity::RoleDeletionConfirmationMessage",
"Are you sure?",
options
);
```
@ -115,7 +111,7 @@ this.confirmation.warn(
Do you confirm that?
`,
'<span class="my-custom-title">Are you sure?</span>',
options,
options
);
```
@ -162,8 +158,7 @@ success(
): Observable<Confirmation.Status>
```
> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Confirmation` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts)
> See the [`LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/localization.ts#L6) and [`Confirmation` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/confirmation.ts)
### warn

62
docs/en/UI/Angular/Environment.md

@ -1,6 +1,6 @@
# Environment
Every application needs some **environment** variables. In Angular world, this is usually managed by `environment.ts`, `environment.prod.ts` and so on. It is the same for ABP as well.
Every application needs some **environment** variables. In Angular world, this is usually managed by `environment.ts`, `environment.prod.ts` and so on. It is the same for ABP as well.
Current `Environment` configuration holds sub config classes as follows:
@ -30,7 +30,7 @@ export interface ApiConfig {
```
Api config has to have a default config and it may have some additional ones for different modules.
I.e. you may want to connect to different Apis for different modules.
I.e. you may want to connect to different Apis for different modules.
Take a look at following example
@ -39,35 +39,34 @@ Take a look at following example
// ...
"apis": {
"default": {
"url": "https://localhost:8080",
"url": "https://localhost:8080"
},
"AbpIdentity": {
"url": "https://localhost:9090",
"url": "https://localhost:9090"
}
},
}
// ...
}
```
When an api from `AbpIdentity` is called, the request will be sent to `"https://localhost:9090"`.
When an api from `AbpIdentity` is called, the request will be sent to `"https://localhost:9090"`.
Everything else will be sent to `"https://localhost:8080"`
* `rootNamespace` **(new)** : Root namespace of the related API. e.g. Acme.BookStore
- `rootNamespace` **(new)** : Root namespace of the related API. e.g. Acme.BookStore
## Application
```js
export interface Application {
export interface Application {
name: string;
baseUrl?: string;
logoUrl?: string;
}
```
* `name`: Name of the backend Application. It is also used by `logo.component` if `logoUrl` is not provided.
* `logoUrl`: Url of the application logo. It is used by `logo.component`
* `baseUrl`: [For detailed information](./Multi-Tenancy.md#domain-tenant-resolver)
- `name`: Name of the backend Application. It is also used by `logo.component` if `logoUrl` is not provided.
- `logoUrl`: Url of the application logo. It is used by `logo.component`
- `baseUrl`: [For detailed information](./Multi-Tenancy.md#domain-tenant-resolver)
## AuthConfig
@ -75,7 +74,7 @@ For authentication, we use angular-oauth2-oidc. Please check their [docs](https:
## RemoteEnvironment
Some applications need to integrate an existing config into the `environment` used throughout the application.
Some applications need to integrate an existing config into the `environment` used throughout the application.
Abp Framework supports this out of box.
To integrate an existing config json into the `environment`, you need to set `remoteEnv`
@ -83,29 +82,28 @@ To integrate an existing config json into the `environment`, you need to set `re
```js
export type customMergeFn = (
localEnv: Partial<Config.Environment>,
remoteEnv: any,
remoteEnv: any
) => Config.Environment;
export interface RemoteEnv {
url: string;
mergeStrategy: 'deepmerge' | 'overwrite' | customMergeFn;
mergeStrategy: "deepmerge" | "overwrite" | customMergeFn;
method?: string;
headers?: ABP.Dictionary<string>;
}
```
* `url` *: Required. The url to be used to retrieve environment config
* `mergeStrategy` *: Required. Defines how the local and the remote `environment` json will be merged
* `deepmerge`: Both local and remote `environment` json will be merged recursively. If both configs have same nested path, the remote `environment` will be prioritized.
* `overwrite`: Remote `environment` will be used and local environment will be ignored.
* `customMergeFn`: You can also provide your own merge function as shown in the example. It will take two parameters, `localEnv: Partial<Config.Environment>` and `remoteEnv` and it needs to return a `Config.Environment` object.
* `method`: HTTP method to be used when retrieving environment config. Default: `GET`
* `headers`: If extra headers are needed for the request, it can be set through this field.
- `url` \*: Required. The url to be used to retrieve environment config
- `mergeStrategy` \*: Required. Defines how the local and the remote `environment` json will be merged
- `deepmerge`: Both local and remote `environment` json will be merged recursively. If both configs have same nested path, the remote `environment` will be prioritized.
- `overwrite`: Remote `environment` will be used and local environment will be ignored.
- `customMergeFn`: You can also provide your own merge function as shown in the example. It will take two parameters, `localEnv: Partial<Config.Environment>` and `remoteEnv` and it needs to return a `Config.Environment` object.
- `method`: HTTP method to be used when retrieving environment config. Default: `GET`
- `headers`: If extra headers are needed for the request, it can be set through this field.
## EnvironmentService
` EnvironmentService` is a singleton service, i.e. provided in root level of your application, and keeps the environment in the internal store.
` EnvironmentService` is a singleton service, i.e. provided in root level of your application, and keeps the environment in the internal store.
### Before Use
@ -124,7 +122,6 @@ class DemoComponent {
You do not have to provide the `EnvironmentService` at module or component/directive level, because it is already **provided in root**.
### Get Methods
`EnvironmentService` has numerous get methods which allow you to get a specific value or all environment object.
@ -141,9 +138,9 @@ You can use the `getEnvironment` or `getEnvironment$` method of `EnvironmentServ
const environment = this.environment.getEnvironment();
// or
this.environment.getEnvironment$().subscribe(environment => {
// use environment here
})
this.environment.getEnvironment$().subscribe((environment) => {
// use environment here
});
```
#### How to Get API URL
@ -156,14 +153,13 @@ The `getApiUrl` or `getApiUrl$` method is used to get a specific API URL from th
const apiUrl = this.environment.getApiUrl();
// environment.apis.default.url
this.environment.getApiUrl$("search").subscribe(searchUrl => {
// environment.apis.search.url
})
this.environment.getApiUrl$("search").subscribe((searchUrl) => {
// environment.apis.search.url
});
```
This method returns the `url` of a specific API based on the key given as its only parameter. If there is no key, `'default'` is used.
#### How to Set the Environment
`EnvironmentService` has a method named `setState` which allows you to set the state value.
@ -178,4 +174,4 @@ Note that **you do not have to call this method at application initiation**, bec
#### Environment Properties
Please refer to `Environment` type for all the properties. It can be found in the [config.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/config.ts#L13).
Please refer to `Environment` type for all the properties. It can be found in the [environment.ts file](https://github.com/abpframework/abp/blob/dev/npm/ng-packs/packages/core/src/lib/models/environment.ts#L4).

2
docs/en/UI/Angular/How-Replaceable-Components-Work-with-Extensions.md

@ -171,7 +171,7 @@ Open the generated `src/app/my-role/my-role.component.html` file and replace its
<div class="col col-md-6">
<h5 class="card-title">My Roles</h5>
</div>
<div class="text-right col col-md-6">
<div class="text-end col col-md-6">
<abp-page-toolbar [record]="data.items"></abp-page-toolbar>
</div>
</div>

2
docs/en/UI/Angular/Quick-Start.md

@ -198,7 +198,7 @@ In addition, you can [deploy your application to certain targets using the Angul
- [Azure](https://github.com/Azure/ng-deploy-azure#readme)
- [Firebase](https://github.com/angular/angularfire#readme)
- [Netlify](https://github.com/ngx-builders/netlify-builder#readme)
- [Vercel](https://github.com/vercel/ng-deploy-vercel#readme)
- [Vercel `vercel init angular`](https://github.com/vercel/vercel/tree/main/examples/angular)
- [GitHub Pages](https://github.com/angular-schule/angular-cli-ghpages/#readme)
---

4
docs/en/UI/Angular/Testing.md

@ -123,7 +123,7 @@ The **queries in Angular Testing Library follow practices for maintainable tests
- [Queries](https://testing-library.com/docs/dom-testing-library/api-queries)
- [User Event](https://testing-library.com/docs/ecosystem-user-event)
- [Examples](https://github.com/testing-library/angular-testing-library/tree/master/apps/example-app/app/examples)
- [Examples](https://github.com/testing-library/angular-testing-library/tree/main/apps/example-app/src/app/examples)
### Clearing DOM After Each Spec
@ -377,4 +377,4 @@ npm test -- --prod
## See Also
* [ABP Community Video - Unit Testing with the Angular UI](https://community.abp.io/articles/unit-testing-with-the-angular-ui-p4l550q3)
- [ABP Community Video - Unit Testing with the Angular UI](https://community.abp.io/articles/unit-testing-with-the-angular-ui-p4l550q3)

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

@ -227,7 +227,7 @@ import { Router } from '@angular/router';
template: `
<a class="dropdown-item pointer" (click)="data.action()">
<i *ngIf="data.textTemplate.icon" [class]="data.textTemplate.icon"></i>
{{ data.textTemplate.text | abpLocalization }}
{%{{{ data.textTemplate.text | abpLocalization }}}%}
</a>
`,
})

18
docs/en/UI/Angular/Toaster-Service.md

@ -2,12 +2,10 @@
You can use the `ToasterService` in @abp/ng.theme.shared package to display messages in an overlay by placing at the root level in your project.
## Getting Started
You do not have to provide the `ToasterService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services.
```js
import { ToasterService } from '@abp/ng.theme.shared';
@ -26,7 +24,7 @@ You can use the `success`, `warn`, `error`, and `info` methods of `ToasterServic
### How to Display a Toast Overlay
```js
this.toaster.success('Message', 'Title');
this.toaster.success("Message", "Title");
```
- The `ToasterService` methods accept three parameters that are `message`, `title`, and `options`.
@ -72,12 +70,11 @@ With the options above, the toast overlay looks like this:
The open toast overlay can be removed manually via the `remove` method by passing the `id` of toast:
```js
const toastId = this.toaster.success('Message', 'Title')
const toastId = this.toaster.success("Message", "Title");
this.toaster.remove(toastId);
```
### How to Remove All Toasts
The all open toasts can be removed manually via the `clear` method:
@ -85,11 +82,13 @@ The all open toasts can be removed manually via the `clear` method:
```js
this.toaster.clear();
```
## Replacing ToasterService with 3rd party toaster libraries
If you want the ABP Framework to utilize 3rd party libraries for the toasters instead of the built-in one, you can provide a service that implements `Toaster.Service` interface, and provide it as follows (ngx-toastr library used in example):
> You can use *LocalizationService* for toaster messages translations.
> You can use _LocalizationService_ for toaster messages translations.
```js
// your-custom-toaster.service.ts
import { Injectable } from '@angular/core';
@ -167,6 +166,7 @@ export class CustomToasterService implements Toaster.Service {
}
}
```
```js
// app.module.ts
@ -178,10 +178,11 @@ import { ToasterService } from '@abp/ng.theme.shared';
{
provide: ToasterService,
useClass: CustomToasterService,
},
},
]
})
```
## API
### success
@ -197,8 +198,7 @@ success(
- `Config` namespace can be imported from `@abp/ng.core`.
- `Toaster` namespace can be imported from `@abp/ng.theme.shared`.
> See the [`Config.LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/config.ts#L46) and [`Toaster` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts)
> See the [`LocalizationParam` type](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/core/src/lib/models/localization.ts#L6) and [`Toaster` namespace](https://github.com/abpframework/abp/blob/master/npm/ng-packs/packages/theme-shared/src/lib/models/toaster.ts)
### warn

71
docs/en/UI/AspNetCore/AutoComplete-Select.md

@ -0,0 +1,71 @@
# ASP.NET Core MVC / Razor Pages: Auto-Complete Select
A simple select component sometimes isn't useful with a huge amount of data. ABP Provides a select implementation that works with pagination and server-side search via using [Select2](https://select2.org/). It works with single or multiple choices well.
A screenshot can be shown below.
| Single | Multiple |
| --- | --- |
| ![autocomplete-select-example](../../images/abp-select2-single.png) |![autocomplete-select-example](../../images/abp-select2-multiple.png) |
## Getting Started
This is a core feature and it's used by the ABP Framework. There is no custom installation or additional packages required.
## Usage
A simple usage is presented below.
```html
<select asp-for="Book.AuthorId"
class="auto-complete-select"
data-autocomplete-api-url="/api/app/author"
data-autocomplete-display-property="name"
data-autocomplete-value-property="id"
data-autocomplete-items-property="items"
data-autocomplete-filter-param-name="filter">
<!-- You can define selected option(s) here -->
<option selected value="@SelectedAuthor.Id">@SelectedAuthor.Name</option>
</select>
```
The select must have the `auto-complete-select` class and the following attributes:
- `data-autocomplete-api-url`: * API Endpoint url to get select items. **GET** request will be sent to this url.
- `data-autocomplete-display-property`: * Property name to display. _(For example: `name` or `title`. Property name of entity/dto.)_.
- `data-autocomplete-value-property`: * Identifier property name. _(For example: `id`)_.
- `data-autocomplete-items-property`: * Property name of collection in response object. _(For example: `items`)_
- `data-autocomplete-filter-param-name`: * Filter text property name. _(For example: `filter`)_.
- `data-autocomplete-selected-item-name`: Text to display as selected item.
- `data-autocomplete-parent-selector`: jQuery selector expression for parent DOM. _(If it's in a modal, it's suggested to send the modal selector as this parameter)_.
Also, selected value(s) should be defined with the `<option>` tags inside select, since pagination is applied and the selected options might haven't loaded yet.
### Multiple Choices
AutoComplete Select supports multiple choices. If the select tag has a `multiple` attribute, it'll allow to choose multiple options.
```html
<select asp-for="Book.TagIds"
class="auto-complete-select"
multiple="multiple"
data-autocomplete-api-url="/api/app/tags"
data-autocomplete-display-property="name"
data-autocomplete-value-property="id"
data-autocomplete-items-property="items"
data-autocomplete-filter-param-name="filter">
@foreach(var tag in SelectedTags)
{
<option selected value="@tag.Id">@tag.Name</option>
}
</select>
```
It'll be automatically bound to a collection of defined value type.
```csharp
public List<Guid> TagIds { get; set; }
```
## Notices
If the authenticated user doesn't have permission on the given URL, the user will get an authorization error. Be careful while designing this kind of UIs.
You can create a specific, [unauthorized](../../Authorization.md) endpoint/method to get the list of items, so the page can retrieve lookup data of dependent entity without giving the entire read permission to users.

2
docs/en/UI/AspNetCore/Tag-Helpers/Figure.md

@ -7,7 +7,7 @@ Basic usage:
````html
<abp-figure>
<abp-image src="..." class="img-fluid rounded" alt="A generic square placeholder image with rounded corners in a figure.">
<abp-figcaption class="text-right">A caption for the above image.</abp-figcaption>
<abp-figcaption class="text-end">A caption for the above image.</abp-figcaption>
</abp-figure>
````

2
docs/en/UI/AspNetCore/Theming.md

@ -363,7 +363,7 @@ Another pre-defined section is the Content Toolbar section which can be used by
````html
<div id="AbpContentToolbar">
<div class="text-right mb-2">
<div class="text-end mb-2">
@RenderSection("content_toolbar", false)
</div>
</div>

40
docs/en/docs-nav.json

@ -284,10 +284,18 @@
}
]
},
{
"text": "Cancellation Token Provider",
"path": "Cancellation-Token-Provider.md"
},
{
"text": "CSRF/XSRF & Anti Forgery",
"path": "CSRF-Anti-Forgery.md"
},
{
"text": "Concurrency Check",
"path": "Concurrency-Check.md"
},
{
"text": "Current User",
"path": "CurrentUser.md"
@ -317,10 +325,6 @@
}
]
},
{
"text": "SMS Sending",
"path": "SMS-Sending.md"
},
{
"text": "Event Bus",
"items": [
@ -356,10 +360,6 @@
}
]
},
{
"text": "JSON",
"path": "JSON.md"
},
{
"text": "Features",
"path": "Features.md"
@ -372,10 +372,22 @@
"text": "GUID Generation",
"path": "Guid-Generation.md"
},
{
"text": "JSON",
"path": "JSON.md"
},
{
"text": "Object to Object Mapping",
"path": "Object-To-Object-Mapping.md"
},
{
"text": "Simple State Checker",
"path": "SimpleStateChecker.md"
},
{
"text": "SMS Sending",
"path": "SMS-Sending.md"
},
{
"text": "String Encryption",
"path": "String-Encryption.md"
@ -401,14 +413,6 @@
{
"text": "Virtual File System",
"path": "Virtual-File-System.md"
},
{
"text": "Cancellation Token Provider",
"path": "Cancellation-Token-Provider.md"
},
{
"text": "Simple State Checker",
"path": "SimpleStateChecker.md"
}
]
},
@ -627,6 +631,10 @@
"text": "Data Tables",
"path": "UI/AspNetCore/Data-Tables.md"
},
{
"text": "Auto-Complete Select",
"path": "UI/AspNetCore/AutoComplete-Select.md"
},
{
"text": "Page Alerts",
"path": "UI/AspNetCore/Page-Alerts.md"

BIN
docs/en/images/abp-select2-multiple.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
docs/en/images/abp-select2-single.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
docs/en/images/optimistic-concurrency.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

2
docs/pt-BR/Tutorials/Angular/Part-II.md

@ -108,7 +108,7 @@ Adicione um botão rotulado `New book`para mostrar o modal:
Books
</h5>
</div>
<div class="text-right col col-md-6">
<div class="text-end col col-md-6">
<button id="create-role" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i> <span>New book</span>
</button>

2
docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md

@ -113,7 +113,7 @@ Abra `Pages/Books/Index.cshtml`e altere a `abp-card-header`tag, como mostrado ab
<abp-column size-md="_6">
<h2>@L["Books"]</h2>
</abp-column>
<abp-column size-md="_6" class="text-right">
<abp-column size-md="_6" class="text-end">
<abp-button id="NewBookButton"
text="@L["NewBook"].Value"
icon="plus"

2
docs/zh-Hans/Caching.md

@ -257,7 +257,7 @@ ABP的分布式缓存接口定义了以下批量操作方法,当你需要在一
分布式缓存服务提供了一个有趣的功能. 假设你已经更新了数据库中某本书的价格, 然后将新价格设置到缓存中, 以便以后使用缓存的值. 如果设置缓存后出现异常, 并且更新图书价格的**事务被回滚了**, 该怎么办?在这种情况下, 缓存值是错误的.
`IDistributedCache<..>`方法提供一个可选参数, `considerOuw`, 默认为`false`. 如果将其设置为`true`, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元](Unit-Of-Work.md)关联. 你将获得在同一工作单元中设置的缓存值, 但**仅当前工作单元成功时**更改才会生效.
`IDistributedCache<..>`方法提供一个可选参数, `considerUow`, 默认为`false`. 如果将其设置为`true`, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元](Unit-Of-Work.md)关联. 你将获得在同一工作单元中设置的缓存值, 但**仅当前工作单元成功时**更改才会生效.
### IDistributedCacheSerializer

129
docs/zh-Hans/Domain-Services.md

@ -1,3 +1,128 @@
# ABP Documentation
# 领域服务
待添加
## 介绍
在 [领域驱动设计](Domain-Driven-Design.md) (DDD) 解决方案中,核心业务逻辑通常在聚合 ([实体](Entities.md)) 和领域服务中实现. 在以下情况下特别需要创建领域服务
* 你实现了依赖于某些服务(如存储库或其他外部服务)的核心域逻辑.
* 你需要实现的逻辑与多个聚合/实体相关,因此它不适合任何聚合.
## ABP 领域服务基础设施
领域服务是简单的无状态类. 虽然你不必从任何服务或接口派生,但 ABP 框架提供了一些有用的基类和约定.
### DomainService 和 IDomainService
`DomainService` 基类派生领域服务或直接实现 `IDomainService` 接口.
**示例: 创建从 `DomainService` 基类派生的领域服务.**
````csharp
using Volo.Abp.Domain.Services;
namespace MyProject.Issues
{
public class IssueManager : DomainService
{
}
}
````
当你这样做时:
* ABP 框架自动将类注册为瞬态生命周期到依赖注入系统.
* 你可以直接使用一些常用服务作为基础属性,而无需手动注入 (例如 [ILogger](Logging.md) and [IGuidGenerator](Guid-Generation.md)).
> 建议使用 `Manager``Service` 后缀命名领域服务. 我们通常使用如上面示例中的 `Manager` 后缀.
**示例: 实现将问题分配给用户的领域逻辑**
````csharp
public class IssueManager : DomainService
{
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueManager(IRepository<Issue, Guid> issueRepository)
{
_issueRepository = issueRepository;
}
public async Task AssignAsync(Issue issue, AppUser user)
{
var currentIssueCount = await _issueRepository
.CountAsync(i => i.AssignedUserId == user.Id);
//Implementing a core business validation
if (currentIssueCount >= 3)
{
throw new IssueAssignmentException(user.UserName);
}
issue.AssignedUserId = user.Id;
}
}
````
问题是定义如下所示的 [聚合根](Entities.md):
````csharp
public class Issue : AggregateRoot<Guid>
{
public Guid? AssignedUserId { get; internal set; }
//...
}
````
* 使用 `internal` 的 set 确保外层调用者不能直接在调用 set ,并强制始终使用 `IssueManager``User` 分配 `Issue`.
### 使用领域服务
领域服务通常用于 [应用程序服务](Application-Services.md).
**示例: 使用 `IssueManager` 将问题分配给用户**
````csharp
using System;
using System.Threading.Tasks;
using MyProject.Users;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace MyProject.Issues
{
public class IssueAppService : ApplicationService, IIssueAppService
{
private readonly IssueManager _issueManager;
private readonly IRepository<AppUser, Guid> _userRepository;
private readonly IRepository<Issue, Guid> _issueRepository;
public IssueAppService(
IssueManager issueManager,
IRepository<AppUser, Guid> userRepository,
IRepository<Issue, Guid> issueRepository)
{
_issueManager = issueManager;
_userRepository = userRepository;
_issueRepository = issueRepository;
}
public async Task AssignAsync(Guid id, Guid userId)
{
var issue = await _issueRepository.GetAsync(id);
var user = await _userRepository.GetAsync(userId);
await _issueManager.AssignAsync(issue, user);
await _issueRepository.UpdateAsync(issue);
}
}
}
````
由于 `IssueAppService` 在应用层, 它不能直接将问题分配给用户.因此,它使用 `IssueManager`.
## 应用程序服务与领域服务
虽然应用服务和领域服务都实现了业务规则,但存在根本的逻辑和形式差异;
虽然 [应用服务](Application-Services.md) 和领域服务都实现了业务规则,但存在根本的逻辑和形式差异:
* 应用程序服务实现应用程序的 **用例** (典型 Web 应用程序中的用户交互), 而领域服务实现 **核心的、用例独立的领域逻辑**.
* 应用程序服务获取/返回 [数据传输对象](Data-Transfer-Objects.md), 领域服务方法通常获取和返回 **领域对象** ([实体](Entities.md), [值对象](Value-Objects.md)).
* 领域服务通常由应用程序服务或其他领域服务使用,而应用程序服务由表示层或客户端应用程序使用.
## 生命周期
领域服务的生命周期是 [瞬态](https://docs.abp.io/en/abp/latest/Dependency-Injection) 的,它们会自动注册到依赖注入服务.

2
docs/zh-Hans/Tutorials/Part-2.md

@ -514,7 +514,7 @@ export class BookComponent implements OnInit {
{%{{{ '::Menu:Books' | abpLocalization }}}%}
</h5>
</div>
<div class="text-right col col-md-6"></div>
<div class="text-end col col-md-6"></div>
</div>
</div>
<div class="card-body">

11
docs/zh-Hans/Tutorials/Part-3.md

@ -145,7 +145,7 @@ namespace Acme.BookStore.Web.Pages.Books
<abp-column size-md="_6">
<abp-card-title>@L["Books"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
<abp-column size-md="_6" class="text-end">
<abp-button id="NewBookButton"
text="@L["NewBook"].Value"
icon="plus"
@ -175,7 +175,7 @@ namespace Acme.BookStore.Web.Pages.Books
<abp-column size-md="_6">
<abp-card-title>@L["Books"]</abp-card-title>
</abp-column>
<abp-column size-md="_6" class="text-right">
<abp-column size-md="_6" class="text-end">
<abp-button id="NewBookButton"
text="@L["NewBook"].Value"
icon="plus"
@ -688,11 +688,10 @@ export class BookComponent implements OnInit {
<div class="row">
<div class="col col-md-6">
<h5 class="card-title">{%{{{ '::Menu:Books' | abpLocalization }}}%}</h5>
</div>
<div class="text-right col col-md-6">
</div>
<div class="text-end col col-md-6">
<!-- Add the "new book" button here -->
<div class="text-lg-right pt-2">
<div class="text-lg-end pt-2">
<button id="create" class="btn btn-primary" type="button" (click)="createBook()">
<i class="fa fa-plus mr-1"></i>
<span>{%{{{ "::NewBook" | abpLocalization }}}%}</span>

2
docs/zh-Hans/UI/Angular/Component-Replacement.md

@ -490,7 +490,7 @@ export class NavItemsComponent implements AfterViewInit {
{%{{{ (currentUser$ | async)?.userName }}}%}
</a>
<div
class="dropdown-menu dropdown-menu-right border-0 shadow-sm"
class="dropdown-menu dropdown-menu-end border-0 shadow-sm"
aria-labelledby="dropdownMenuLink"
[class.d-block]="smallScreen && currentUserDropdown.isOpen()"
>

14
framework/src/Volo.Abp.AspNetCore.Authentication.OAuth/Microsoft/AspNetCore/Authentication/OAuth/Claims/AbpClaimActionCollectionExtensions.cs

@ -13,6 +13,20 @@ public static class AbpClaimActionCollectionExtensions
claimActions.DeleteClaim("name");
claimActions.RemoveDuplicate(AbpClaimTypes.UserName);
}
if (AbpClaimTypes.Name != "given_name")
{
claimActions.MapJsonKey(AbpClaimTypes.Name, "given_name");
claimActions.DeleteClaim("given_name");
claimActions.RemoveDuplicate(AbpClaimTypes.Name);
}
if (AbpClaimTypes.SurName != "family_name")
{
claimActions.MapJsonKey(AbpClaimTypes.SurName, "family_name");
claimActions.DeleteClaim("family_name");
claimActions.RemoveDuplicate(AbpClaimTypes.SurName);
}
if (AbpClaimTypes.Email != "email")
{

3
framework/src/Volo.Abp.AspNetCore.Authentication.OpenIdConnect/Microsoft/Extensions/DependencyInjection/AbpOpenIdConnectExtensions.cs

@ -57,8 +57,7 @@ public static class AbpOpenIdConnectExtensions
if (receivedContext.Request.Cookies.ContainsKey(tenantKey))
{
receivedContext.TokenEndpointRequest.SetParameter(tenantKey,
receivedContext.Request.Cookies[tenantKey]);
receivedContext.TokenEndpointRequest?.SetParameter(tenantKey, receivedContext.Request.Cookies[tenantKey]);
}
}
}

2
framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Layout/PageHeader.razor

@ -7,7 +7,7 @@
@if(Options.Value.RenderPageTitle)
{
<Column ColumnSize="ColumnSize.IsAuto">
<h1 class="content-header-title">@PageLayout.Title</h1>
<h5 class="content-header-title">@PageLayout.Title</h5>
</Column>
}

2
framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationConfigurationDto.cs

@ -17,6 +17,8 @@ public class ApplicationConfigurationDto
public ApplicationFeatureConfigurationDto Features { get; set; }
public ApplicationGlobalFeatureConfigurationDto GlobalFeatures { get; set; }
public MultiTenancyInfoDto MultiTenancy { get; set; }
public CurrentTenantDto CurrentTenant { get; set; }

18
framework/src/Volo.Abp.AspNetCore.Mvc.Contracts/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/ApplicationGlobalFeatureConfigurationDto.cs

@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
namespace Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
[Serializable]
public class ApplicationGlobalFeatureConfigurationDto
{
public HashSet<string> EnabledFeatures { get; set; }
public Dictionary<string, List<string>> ModuleEnabledFeatures { get; set; }
public ApplicationGlobalFeatureConfigurationDto()
{
EnabledFeatures = new HashSet<string>();
ModuleEnabledFeatures = new Dictionary<string, List<string>>();
}
}

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

@ -431,9 +431,7 @@ var abp = abp || {};
configuration.language = datatables.defaultConfigurations.language();
if(configuration.dom){
configuration.dom += datatables.defaultConfigurations.dom;
}else{
if(!configuration.dom){
configuration.dom = datatables.defaultConfigurations.dom;
}

61
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpDataAnnotationAutoLocalizationMetadataDetailsProvider.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
@ -16,14 +17,16 @@ public class AbpDataAnnotationAutoLocalizationMetadataDetailsProvider : IDisplay
private readonly Lazy<IStringLocalizerFactory> _stringLocalizerFactory;
private readonly Lazy<IOptions<MvcDataAnnotationsLocalizationOptions>> _localizationOptions;
private readonly ConcurrentDictionary<DisplayMetadataProviderContext, string> _displayNameKeys;
public AbpDataAnnotationAutoLocalizationMetadataDetailsProvider(IServiceCollection services)
{
_stringLocalizerFactory = services.GetRequiredServiceLazy<IStringLocalizerFactory>();
_localizationOptions = services.GetRequiredServiceLazy<IOptions<MvcDataAnnotationsLocalizationOptions>>();
_displayNameKeys = new ConcurrentDictionary<DisplayMetadataProviderContext, string>();
}
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
public virtual void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
var displayMetadata = context.DisplayMetadata;
if (displayMetadata.DisplayName != null)
@ -54,36 +57,58 @@ public class AbpDataAnnotationAutoLocalizationMetadataDetailsProvider : IDisplay
displayMetadata.DisplayName = () =>
{
/*
* DisplayName:ClassName:PropertyName
* DisplayName:PropertyName
* ClassName:PropertyName
* PropertyName
*/
var key = _displayNameKeys.GetOrAdd(context, _ => GetDisplayNameKey(context, localizer));
return key != null ? localizer[key] : null;
};
}
protected virtual string GetDisplayNameKey(DisplayMetadataProviderContext context, IStringLocalizer localizer)
{
/*
* DisplayName:ClassName:PropertyName
* DisplayName:PropertyName
* ClassName:PropertyName
* PropertyName
*/
LocalizedString localizedString = null;
LocalizedString localizedString = null;
if (context.Key.ContainerType != null)
if (context.Key.ContainerType != null)
{
localizedString = localizer[PropertyLocalizationKeyPrefix + context.Key.ContainerType.Name + ":" + context.Key.Name];
if (!localizedString.ResourceNotFound)
{
localizedString = localizer[PropertyLocalizationKeyPrefix + context.Key.ContainerType.Name + ":" + context.Key.Name];
return localizedString.Name;
}
}
if (localizedString == null || localizedString.ResourceNotFound)
if (localizedString == null || localizedString.ResourceNotFound)
{
localizedString = localizer[PropertyLocalizationKeyPrefix + context.Key.Name];
if (!localizedString.ResourceNotFound)
{
localizedString = localizer[PropertyLocalizationKeyPrefix + context.Key.Name];
return localizedString.Name;
}
}
if (localizedString.ResourceNotFound && context.Key.ContainerType != null)
if (localizedString.ResourceNotFound && context.Key.ContainerType != null)
{
localizedString = localizer[context.Key.ContainerType.Name + ":" + context.Key.Name];
if (!localizedString.ResourceNotFound)
{
localizedString = localizer[context.Key.ContainerType.Name + ":" + context.Key.Name];
return localizedString.Name;
}
}
if (localizedString.ResourceNotFound)
if (localizedString.ResourceNotFound)
{
localizedString = localizer[context.Key.Name];
if (!localizedString.ResourceNotFound)
{
localizedString = localizer[context.Key.Name];
return localizedString.Name;
}
}
return localizedString;
};
return null;
}
}

20
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs

@ -14,6 +14,7 @@ using Volo.Abp.AspNetCore.Mvc.MultiTenancy;
using Volo.Abp.Authorization;
using Volo.Abp.Authorization.Permissions;
using Volo.Abp.Features;
using Volo.Abp.GlobalFeatures;
using Volo.Abp.Localization;
using Volo.Abp.MultiTenancy;
using Volo.Abp.Settings;
@ -87,6 +88,7 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp
{
Auth = await GetAuthConfigAsync(),
Features = await GetFeaturesConfigAsync(),
GlobalFeatures = await GetGlobalFeaturesConfigAsync(),
Localization = await GetLocalizationConfigAsync(),
CurrentUser = GetCurrentUser(),
Setting = await GetSettingConfigAsync(),
@ -284,6 +286,24 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp
return result;
}
protected virtual Task<ApplicationGlobalFeatureConfigurationDto> GetGlobalFeaturesConfigAsync()
{
var result = new ApplicationGlobalFeatureConfigurationDto();
foreach (var enabledFeatureName in GlobalFeatureManager.Instance.GetEnabledFeatureNames())
{
result.EnabledFeatures.AddIfNotContains(enabledFeatureName);
}
foreach (var module in GlobalFeatureManager.Instance.Modules)
{
result.ModuleEnabledFeatures.AddIfNotContains(new KeyValuePair<string, List<string>>(module.Key, module.Value.GetFeatures().Select(x => x.FeatureName).ToList()));
}
return Task.FromResult(result);
}
protected virtual async Task<TimingDto> GetTimingConfigAsync()
{
var windowsTimeZoneId = await _settingProvider.GetOrNullAsync(TimingSettingNames.TimeZone);

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

@ -18,13 +18,14 @@ public class RemoteStreamContentOutputFormatter : OutputFormatter
return typeof(IRemoteStreamContent).IsAssignableFrom(type);
}
public override async Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
public async override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
{
var remoteStream = (IRemoteStreamContent)context.Object;
if (remoteStream != null)
{
context.HttpContext.Response.ContentType = remoteStream.ContentType;
context.HttpContext.Response.ContentLength = remoteStream.ContentLength;
if (!remoteStream.FileName.IsNullOrWhiteSpace())
{

1
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/ExceptionHandling/AbpExceptionHandlingMiddleware.cs

@ -82,6 +82,7 @@ public class AbpExceptionHandlingMiddleware : IMiddleware, ITransientDependency
httpContext.Response.StatusCode = (int)statusCodeFinder.GetStatusCode(httpContext, exception);
httpContext.Response.OnStarting(_clearCacheHeadersDelegate, httpContext.Response);
httpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
httpContext.Response.Headers.Add("Content-Type", "application/json");
await httpContext.Response.WriteAsync(
jsonSerializer.Serialize(

18
framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditPropertySetter.cs

@ -21,25 +21,25 @@ public class AuditPropertySetter : IAuditPropertySetter, ITransientDependency
Clock = clock;
}
public void SetCreationProperties(object targetObject)
public virtual void SetCreationProperties(object targetObject)
{
SetCreationTime(targetObject);
SetCreatorId(targetObject);
}
public void SetModificationProperties(object targetObject)
public virtual void SetModificationProperties(object targetObject)
{
SetLastModificationTime(targetObject);
SetLastModifierId(targetObject);
}
public void SetDeletionProperties(object targetObject)
public virtual void SetDeletionProperties(object targetObject)
{
SetDeletionTime(targetObject);
SetDeleterId(targetObject);
}
private void SetCreationTime(object targetObject)
protected virtual void SetCreationTime(object targetObject)
{
if (!(targetObject is IHasCreationTime objectWithCreationTime))
{
@ -52,7 +52,7 @@ public class AuditPropertySetter : IAuditPropertySetter, ITransientDependency
}
}
private void SetCreatorId(object targetObject)
protected virtual void SetCreatorId(object targetObject)
{
if (!CurrentUser.Id.HasValue)
{
@ -95,7 +95,7 @@ public class AuditPropertySetter : IAuditPropertySetter, ITransientDependency
}
}
private void SetLastModificationTime(object targetObject)
protected virtual void SetLastModificationTime(object targetObject)
{
if (targetObject is IHasModificationTime objectWithModificationTime)
{
@ -103,7 +103,7 @@ public class AuditPropertySetter : IAuditPropertySetter, ITransientDependency
}
}
private void SetLastModifierId(object targetObject)
protected virtual void SetLastModifierId(object targetObject)
{
if (!(targetObject is IModificationAuditedObject modificationAuditedObject))
{
@ -137,7 +137,7 @@ public class AuditPropertySetter : IAuditPropertySetter, ITransientDependency
modificationAuditedObject.LastModifierId = CurrentUser.Id;
}
private void SetDeletionTime(object targetObject)
protected virtual void SetDeletionTime(object targetObject)
{
if (targetObject is IHasDeletionTime objectWithDeletionTime)
{
@ -148,7 +148,7 @@ public class AuditPropertySetter : IAuditPropertySetter, ITransientDependency
}
}
private void SetDeleterId(object targetObject)
protected virtual void SetDeleterId(object targetObject)
{
if (!(targetObject is IDeletionAuditedObject deletionAuditedObject))
{

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

@ -4,7 +4,7 @@
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<AssemblyName>Volo.Abp.AutoMapper</AssemblyName>
<PackageId>Volo.Abp.AutoMapper</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
@ -21,7 +21,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="10.1.1" />
<PackageReference Include="AutoMapper" Version="11.0.1" />
</ItemGroup>
</Project>

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

@ -1,5 +1,6 @@
using System;
using AutoMapper;
using AutoMapper.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.Auditing;
@ -49,7 +50,7 @@ public class AbpAutoMapperModule : AbpModule
{
foreach (var profileType in options.ValidatingProfiles)
{
config.AssertConfigurationIsValid(((Profile)Activator.CreateInstance(profileType)).ProfileName);
config.Internal().AssertConfigurationIsValid(((Profile)Activator.CreateInstance(profileType)).ProfileName);
}
}

2
framework/src/Volo.Abp.AzureServiceBus/Volo/Abp/AzureServiceBus/ConnectionPool.cs

@ -47,7 +47,7 @@ public class ConnectionPool : IConnectionPool, ISingletonDependency
connectionName, new Lazy<ServiceBusAdministrationClient>(() =>
{
var config = _options.Connections.GetOrDefault(connectionName);
return new ServiceBusAdministrationClient(config.ConnectionString);
return new ServiceBusAdministrationClient(config.ConnectionString, config.Admin);
})
).Value;
}

13
framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerBase.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
@ -7,6 +8,16 @@ public abstract class HangfireBackgroundWorkerBase : BackgroundWorkerBase, IHang
public string RecurringJobId { get; set; }
public string CronExpression { get; set; }
public TimeZoneInfo TimeZone { get; set; }
public string Queue { get; set; }
public abstract Task DoWorkAsync();
protected HangfireBackgroundWorkerBase()
{
TimeZone = null;
Queue = "default";
}
}

7
framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfireBackgroundWorkerManager.cs

@ -40,12 +40,13 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle
var unProxyWorker = ProxyHelper.UnProxy(hangfireBackgroundWorker);
if (hangfireBackgroundWorker.RecurringJobId.IsNullOrWhiteSpace())
{
RecurringJob.AddOrUpdate(() => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(), hangfireBackgroundWorker.CronExpression);
RecurringJob.AddOrUpdate(() => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(),
hangfireBackgroundWorker.CronExpression, hangfireBackgroundWorker.TimeZone, hangfireBackgroundWorker.Queue);
}
else
{
RecurringJob.AddOrUpdate(hangfireBackgroundWorker.RecurringJobId, () => ((IHangfireBackgroundWorker)unProxyWorker).DoWorkAsync(),
hangfireBackgroundWorker.CronExpression);
hangfireBackgroundWorker.CronExpression, hangfireBackgroundWorker.TimeZone, hangfireBackgroundWorker.Queue);
}
}
else
@ -79,7 +80,7 @@ public class HangfireBackgroundWorkerManager : IBackgroundWorkerManager, ISingle
var adapterType = typeof(HangfirePeriodicBackgroundWorkerAdapter<>).MakeGenericType(ProxyHelper.GetUnProxiedType(worker));
var workerAdapter = Activator.CreateInstance(adapterType) as IHangfireBackgroundWorker;
RecurringJob.AddOrUpdate(() => workerAdapter.DoWorkAsync(), GetCron(period.Value));
RecurringJob.AddOrUpdate(() => workerAdapter.DoWorkAsync(), GetCron(period.Value), workerAdapter.TimeZone, workerAdapter.Queue);
}
return Task.CompletedTask;

2
framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/HangfirePeriodicBackgroundWorkerAdapter.cs

@ -17,7 +17,7 @@ public class HangfirePeriodicBackgroundWorkerAdapter<TWorker> : HangfireBackgrou
_doWorkMethod = typeof(TWorker).GetMethod("DoWork", BindingFlags.Instance | BindingFlags.NonPublic);
}
public override async Task DoWorkAsync()
public async override Task DoWorkAsync()
{
var workerContext = new PeriodicBackgroundWorkerContext(ServiceProvider);
var worker = ServiceProvider.GetRequiredService<TWorker>();

7
framework/src/Volo.Abp.BackgroundWorkers.Hangfire/Volo/Abp/BackgroundWorkers/Hangfire/IHangfireBackgroundWorker.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
namespace Volo.Abp.BackgroundWorkers.Hangfire;
@ -7,6 +8,10 @@ public interface IHangfireBackgroundWorker : IBackgroundWorker
string RecurringJobId { get; set; }
string CronExpression { get; set; }
TimeZoneInfo TimeZone { get; set; }
string Queue { get; set; }
Task DoWorkAsync();
}

1
framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor

@ -11,6 +11,7 @@
CurrentPage="@CurrentPage"
PageSize="@PageSize"
Responsive="@Responsive"
Striped
Class="@Class">
<LoadingTemplate>
<Row Class="w-100 align-items-center" Style="height: 150px;">

5
framework/src/Volo.Abp.BlazoriseUI/Components/UiMessageAlert.razor.cs

@ -94,6 +94,11 @@ public partial class UiMessageAlert : ComponentBase, IDisposable
Options = e.Options;
Callback = e.Callback;
await ShowMessageAlert();
}
protected virtual async Task ShowMessageAlert()
{
await InvokeAsync(ModalRef.Show);
}

8
framework/src/Volo.Abp.BlazoriseUI/Volo.Abp.BlazoriseUI.csproj

@ -14,10 +14,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazorise" Version="1.0.1" />
<PackageReference Include="Blazorise.DataGrid" Version="1.0.1" />
<PackageReference Include="Blazorise.Snackbar" Version="1.0.1" />
<PackageReference Include="Blazorise.Components" Version="1.0.1" />
<PackageReference Include="Blazorise" Version="1.0.2" />
<PackageReference Include="Blazorise.DataGrid" Version="1.0.2" />
<PackageReference Include="Blazorise.Snackbar" Version="1.0.2" />
<PackageReference Include="Blazorise.Components" Version="1.0.2" />
</ItemGroup>
</Project>

4
framework/src/Volo.Abp.BlobStoring.Minio/Volo.Abp.BlobStoring.Minio.csproj

@ -3,7 +3,7 @@
<Import Project="..\..\..\common.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>Volo.Abp.BlobStoring.Minio</AssemblyName>
<PackageId>Volo.Abp.BlobStoring.Minio</PackageId>
<AssetTargetFallback>$(AssetTargetFallback);portable-net45+win8+wp8+wpa81;</AssetTargetFallback>
@ -15,7 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\Volo.Abp.BlobStoring\Volo.Abp.BlobStoring.csproj" />
<PackageReference Include="Minio" Version="3.1.13" />
<PackageReference Include="Minio" Version="4.0.1" />
</ItemGroup>
</Project>

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Args/CommandLineArgumentParser.cs

@ -98,7 +98,7 @@ public class CommandLineArgumentParser : ICommandLineArgumentParser, ITransientD
{
if (argument.Length <= 1)
{
throw new ArgumentException("Should specify an option name after '--' prefix!");
throw new ArgumentException("Should specify an option name after '-' prefix!");
}
return argument.RemovePreFix("-");

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

@ -10,6 +10,7 @@ using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Utils;
@ -21,22 +22,19 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
{
public const string Name = "new";
public ILogger<NewCommand> Logger { get; set; }
protected TemplateProjectBuilder TemplateProjectBuilder { get; }
public ITemplateInfoProvider TemplateInfoProvider { get; }
public NewCommand(TemplateProjectBuilder templateProjectBuilder
, ITemplateInfoProvider templateInfoProvider,
ConnectionStringProvider connectionStringProvider,
SolutionPackageVersionFinder solutionPackageVersionFinder,
ICmdHelper cmdHelper)
: base(connectionStringProvider, solutionPackageVersionFinder, cmdHelper)
ICmdHelper cmdHelper,
IInstallLibsService installLibsService)
: base(connectionStringProvider, solutionPackageVersionFinder, cmdHelper, installLibsService)
{
TemplateProjectBuilder = templateProjectBuilder;
TemplateInfoProvider = templateInfoProvider;
Logger = NullLogger<NewCommand>.Instance;
}
public async Task ExecuteAsync(CommandLineArgs commandLineArgs)
@ -79,7 +77,7 @@ public class NewCommand : ProjectCreationCommandBase, IConsoleCommand, ITransien
Logger.LogInformation($"'{projectName}' has been successfully created to '{projectArgs.OutputFolder}'");
RunGraphBuildForMicroserviceServiceTemplate(projectArgs);
RunInstallLibsForWebTemplate(projectArgs);
await RunInstallLibsForWebTemplateAsync(projectArgs);
OpenRelatedWebPage(projectArgs, template, isTiered, commandLineArgs);
}

26
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProjectCreationCommandBase.cs

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;
using Microsoft.Extensions.Logging;
@ -8,6 +9,7 @@ using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.Cli.ProjectModification;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.ProjectBuilding.Building;
using Volo.Abp.Cli.ProjectBuilding.Templates.App;
@ -22,13 +24,19 @@ public abstract class ProjectCreationCommandBase
public ConnectionStringProvider ConnectionStringProvider { get; }
public SolutionPackageVersionFinder SolutionPackageVersionFinder { get; }
public ICmdHelper CmdHelper { get; }
public IInstallLibsService InstallLibsService { get; }
public ILogger<NewCommand> Logger { get; set; }
public ProjectCreationCommandBase(ConnectionStringProvider connectionStringProvider, SolutionPackageVersionFinder solutionPackageVersionFinder, ICmdHelper cmdHelper)
public ProjectCreationCommandBase(
ConnectionStringProvider connectionStringProvider,
SolutionPackageVersionFinder solutionPackageVersionFinder,
ICmdHelper cmdHelper,
IInstallLibsService installLibsService)
{
ConnectionStringProvider = connectionStringProvider;
SolutionPackageVersionFinder = solutionPackageVersionFinder;
CmdHelper = cmdHelper;
InstallLibsService = installLibsService;
Logger = NullLogger<NewCommand>.Instance;
}
@ -48,6 +56,12 @@ public abstract class ProjectCreationCommandBase
Logger.LogInformation("Preview: yes");
}
var pwa = commandLineArgs.Options.ContainsKey(Options.ProgressiveWebApp.Short);
if (pwa)
{
Logger.LogInformation("Progressive Web App: yes");
}
var databaseProvider = GetDatabaseProvider(commandLineArgs);
if (databaseProvider != DatabaseProvider.NotSpecified)
{
@ -307,14 +321,15 @@ public abstract class ProjectCreationCommandBase
}
}
protected virtual void RunInstallLibsForWebTemplate(ProjectBuildArgs projectArgs)
protected async Task RunInstallLibsForWebTemplateAsync(ProjectBuildArgs projectArgs)
{
if (AppTemplateBase.IsAppTemplate(projectArgs.TemplateName) ||
ModuleTemplateBase.IsModuleTemplate(projectArgs.TemplateName) ||
AppNoLayersTemplateBase.IsAppNoLayersTemplate(projectArgs.TemplateName) ||
MicroserviceServiceTemplateBase.IsMicroserviceTemplate(projectArgs.TemplateName))
{
CmdHelper.RunCmd("abp install-libs", projectArgs.OutputFolder);
Logger.LogInformation("Installing client-side packages...");
await InstallLibsService.InstallLibsAsync(projectArgs.OutputFolder);
}
}
@ -476,5 +491,10 @@ public abstract class ProjectCreationCommandBase
{
public const string Long = "preview";
}
public static class ProgressiveWebApp
{
public const string Short = "pwa";
}
}
}

17
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/ProxyCommandBase.cs

@ -83,15 +83,18 @@ public abstract class ProxyCommandBase<T> : IConsoleCommand, ITransientDependenc
sb.AppendLine("Options:");
sb.AppendLine("");
sb.AppendLine("-m|--module <module-name> (default: 'app') The name of the backend module you wish to generate proxies for.");
sb.AppendLine("-t|--type <generate-type> The name of generate type (csharp, js, ng).");
sb.AppendLine("-wd|--working-directory <directory-path> Execution directory.");
sb.AppendLine("-u|--url <url> API definition URL from.");
sb.AppendLine("-a|--api-name <module-name> (default: 'default') The name of the API endpoint defined in the /src/environments/environment.ts.");
sb.AppendLine("-s|--source <source-name> (default: 'defaultProject') Angular project name to resolve the root namespace & API definition URL from.");
sb.AppendLine("-o|--output <output-name> JavaScript file path or folder to place generated code in.");
sb.AppendLine("-p|--prompt Asks the options from the command line prompt (for the missing options)");
sb.AppendLine("--target <target-name> (default: 'defaultProject') Angular project name to place generated code in.");
sb.AppendLine("--folder <folder-name> (default: 'ClientProxies') Folder name to place generated CSharp code in.");
sb.AppendLine("-t|--type <generate-type> The name of generate type (csharp, js, ng).");
sb.AppendLine(" csharp");
sb.AppendLine(" --folder <folder-name> (default: 'ClientProxies') Folder name to place generated CSharp code in.");
sb.AppendLine(" js");
sb.AppendLine(" -o|--output <output-name> JavaScript file path or folder to place generated code in.");
sb.AppendLine(" ng");
sb.AppendLine(" -a|--api-name <module-name> (default: 'default') The name of the API endpoint defined in the /src/environments/environment.ts.");
sb.AppendLine(" -s|--source <source-name> (default: 'defaultProject') Angular project name to resolve the root namespace & API definition URL from.");
sb.AppendLine(" -p|--prompt Asks the options from the command line prompt (for the missing options)");
sb.AppendLine(" --target <target-name> (default: 'defaultProject') Angular project name to place generated code in.");
sb.AppendLine("");
sb.AppendLine("See the documentation for more info: https://docs.abp.io/en/abp/latest/CLI");

224
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Commands/SuiteCommand.cs

@ -1,33 +1,53 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Newtonsoft.Json.Linq;
using NuGet.Versioning;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.NuGet;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http;
using Volo.Abp.Json;
using Volo.Abp.Threading;
namespace Volo.Abp.Cli.Commands;
public class SuiteCommand : IConsoleCommand, ITransientDependency
{
public const string Name = "suite";
public ICmdHelper CmdHelper { get; }
private readonly AbpNuGetIndexUrlService _nuGetIndexUrlService;
private readonly NuGetService _nuGetService;
private readonly CliHttpClientFactory _cliHttpClientFactory;
private const string SuitePackageName = "Volo.Abp.Suite";
public ILogger<SuiteCommand> Logger { get; set; }
public SuiteCommand(AbpNuGetIndexUrlService nuGetIndexUrlService, NuGetService nuGetService, ICmdHelper cmdHelper)
private const string AbpSuiteHost = "http://localhost:3000";
public SuiteCommand(
AbpNuGetIndexUrlService nuGetIndexUrlService,
NuGetService nuGetService,
ICmdHelper cmdHelper,
CliHttpClientFactory cliHttpClientFactory)
{
CmdHelper = cmdHelper;
_nuGetIndexUrlService = nuGetIndexUrlService;
_nuGetService = nuGetService;
_cliHttpClientFactory = cliHttpClientFactory;
Logger = NullLogger<SuiteCommand>.Instance;
}
@ -48,6 +68,18 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
RunSuite();
break;
case "generate":
await InstallSuiteIfNotInstalledAsync();
var suiteProcess = StartSuite();
System.Threading.Thread.Sleep(500); //wait for initialization of the app
await GenerateCrudPageAsync(commandLineArgs);
if (suiteProcess != null)
{
KillSuite();
}
break;
case "install":
await InstallSuiteAsync(version, preview);
break;
@ -63,6 +95,115 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
}
}
private async Task GenerateCrudPageAsync(CommandLineArgs args)
{
var entityFile = args.Options.GetOrNull(Options.Crud.Entity.Short, Options.Crud.Entity.Long);
var solutionFile = args.Options.GetOrNull(Options.Crud.Solution.Short, Options.Crud.Solution.Long);
if (entityFile.IsNullOrEmpty() || !entityFile.EndsWith(".json") || !File.Exists(entityFile) ||
solutionFile.IsNullOrEmpty() || !solutionFile.EndsWith(".sln"))
{
throw new UserFriendlyException("Invalid Arguments!");
}
Logger.LogInformation("Generating CRUD Page...");
var client = _cliHttpClientFactory.CreateClient(false);
var solutionId = await GetSolutionIdAsync(client, solutionFile);
if (!solutionId.HasValue)
{
return;
}
var entityContent = new StringContent(
File.ReadAllText(entityFile),
Encoding.UTF8,
MimeTypes.Application.Json
);
var responseMessage = await client.PostAsync(
$"{AbpSuiteHost}/api/abpSuite/crudPageGenerator/{solutionId.ToString()}/save-and-generate-entity",
entityContent
);
var response = await responseMessage.Content.ReadAsStringAsync();
if (!response.IsNullOrWhiteSpace())
{
Logger.LogError(response);
}
else
{
Logger.LogInformation("CRUD page generation successfully completed.");
}
}
private async Task<Guid?> GetSolutionIdAsync(HttpClient client, string solutionPath)
{
var timeIntervals = new List<TimeSpan>();
for (var i = 0; i < 10; i++)
{
timeIntervals.Add(TimeSpan.FromSeconds(5));
}
var responseMessage = await client.GetHttpResponseMessageWithRetryAsync(
"http://localhost:3000/api/abpSuite/solutions",
_cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10)),
Logger,
timeIntervals.ToArray());
var response = await responseMessage.Content.ReadAsStringAsync();
JArray solutions;
try
{
solutions = (JArray)(JObject.Parse(response)["solutions"]);
}
catch (Exception)
{
Logger.LogError(response);
return await AddSolutionToSuiteAsync(client, solutionPath);
}
foreach (JObject solution in solutions)
{
if (solution["path"].ToString() == solutionPath)
{
return Guid.Parse(solution["id"].ToString());
}
}
return await AddSolutionToSuiteAsync(client, solutionPath);
}
private async Task<Guid?> AddSolutionToSuiteAsync(HttpClient client, string solutionPath)
{
var entityContent = new StringContent(
"{\"Path\": \"" + solutionPath.Replace("\\", "\\\\") + "\"}",
Encoding.UTF8,
MimeTypes.Application.Json
);
var responseMessage = await client.PostAsync(
"http://localhost:3000/api/abpSuite/addSolution",
entityContent,
_cliHttpClientFactory.GetCancellationToken(TimeSpan.FromMinutes(10))
);
var response = await responseMessage.Content.ReadAsStringAsync();
try
{
return Guid.Parse(JObject.Parse(response)["id"].ToString());
}
catch (Exception)
{
Logger.LogError(response);
return null;
}
}
private async Task InstallSuiteIfNotInstalledAsync()
{
var currentSuiteVersionAsString = GetCurrentSuiteVersion();
@ -132,7 +273,8 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
}
CmdHelper.RunCmd(
$"dotnet tool install {SuitePackageName}{versionOption} --add-source {nugetIndexUrl} -g", out int exitCode
$"dotnet tool install {SuitePackageName}{versionOption} --add-source {nugetIndexUrl} -g",
out int exitCode
);
if (exitCode == 0)
@ -155,7 +297,8 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
private void ShowSuiteManualInstallCommand()
{
Logger.LogInformation("You can also run the following command to install ABP Suite.");
Logger.LogInformation("dotnet tool install -g Volo.Abp.Suite --add-source https://nuget.abp.io/<your-private-key>/v3/index.json");
Logger.LogInformation(
"dotnet tool install -g Volo.Abp.Suite --add-source https://nuget.abp.io/<your-private-key>/v3/index.json");
}
private async Task UpdateSuiteAsync(string version = null, bool preview = false)
@ -202,7 +345,8 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
}
CmdHelper.RunCmd(
$"dotnet tool update {SuitePackageName}{versionOption} --add-source {nugetIndexUrl} -g", out int exitCode
$"dotnet tool update {SuitePackageName}{versionOption} --add-source {nugetIndexUrl} -g",
out int exitCode
);
if (exitCode != 0)
@ -231,7 +375,8 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
private void ShowSuiteManualUpdateCommand()
{
Logger.LogError("You can also run the following command to update ABP Suite.");
Logger.LogError("dotnet tool update -g Volo.Abp.Suite --add-source https://nuget.abp.io/<your-private-key>/v3/index.json");
Logger.LogError(
"dotnet tool update -g Volo.Abp.Suite --add-source https://nuget.abp.io/<your-private-key>/v3/index.json");
}
private void RemoveSuite()
@ -258,6 +403,56 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
CmdHelper.RunCmd("abp-suite");
}
private Process StartSuite()
{
try
{
if (!GlobalToolHelper.IsGlobalToolInstalled("abp-suite"))
{
Logger.LogWarning("ABP Suite is not installed! To install it you can run the command: \"abp suite install\"");
return null;
}
}
catch (Exception ex)
{
Logger.LogWarning("Couldn't check ABP Suite installed status: " + ex.Message);
}
if (IsSuiteAlreadyRunning())
{
return null;
}
return CmdHelper.RunCmdAndGetProcess("abp-suite --no-browser");
}
private bool IsSuiteAlreadyRunning()
{
var ipGP = IPGlobalProperties.GetIPGlobalProperties();
var endpoints = ipGP.GetActiveTcpListeners();
return endpoints.Any(e => e.Port == 3000);
}
private void KillSuite()
{
try
{
var suiteProcesses = (from p in Process.GetProcesses()
where p.ProcessName.ToLower().Contains("abp-suite")
select p);
foreach (var suiteProcess in suiteProcesses)
{
suiteProcess.Kill();
Logger.LogInformation("Suite closed.");
}
}
catch (Exception ex)
{
Logger.LogInformation("Cannot close Suite." + ex.Message);
}
}
public string GetUsageInfo()
{
var sb = new StringBuilder();
@ -306,5 +501,20 @@ public class SuiteCommand : IConsoleCommand, ITransientDependency
public const string Long = "version";
public const string Short = "v";
}
public static class Crud
{
public static class Solution
{
public const string Long = "solution";
public const string Short = "s";
}
public static class Entity
{
public const string Long = "entity";
public const string Short = "e";
}
}
}
}
}

2
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/ConnectionStringChangeStep.cs

@ -26,7 +26,7 @@ public class ConnectionStringChangeStep : ProjectBuildPipelineStep
return;
}
var newConnectionString = $"\"{DefaultConnectionStringKey}\": \"{context.BuildArgs.ConnectionString}\"";
var newConnectionString = $"\"{DefaultConnectionStringKey}\": \"{context.BuildArgs.ConnectionString.Replace(@"\\", @"\").Replace(@"\", @"\\")}\"";
foreach (var appSettingsJson in appSettingsJsonFiles)
{

10
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/DatabaseManagementSystemChangeStep.cs

@ -81,12 +81,12 @@ public class DatabaseManagementSystemChangeStep : ProjectBuildPipelineStep
private void ChangeEntityFrameworkCoreDependency(ProjectBuildContext context, string newPackageName, string newModuleNamespace, string newModuleClass)
{
var efCoreProjectFile = context.Files.First(f => f.Name.EndsWith("EntityFrameworkCore.csproj", StringComparison.OrdinalIgnoreCase));
efCoreProjectFile.ReplaceText("Volo.Abp.EntityFrameworkCore.SqlServer", newPackageName);
var efCoreProjectFile = context.Files.FirstOrDefault(f => f.Name.EndsWith("EntityFrameworkCore.csproj", StringComparison.OrdinalIgnoreCase));
efCoreProjectFile?.ReplaceText("Volo.Abp.EntityFrameworkCore.SqlServer", newPackageName);
var efCoreModuleClass = context.Files.First(f => f.Name.EndsWith("EntityFrameworkCoreModule.cs", StringComparison.OrdinalIgnoreCase));
efCoreModuleClass.ReplaceText("Volo.Abp.EntityFrameworkCore.SqlServer", newModuleNamespace);
efCoreModuleClass.ReplaceText("AbpEntityFrameworkCoreSqlServerModule", newModuleClass);
var efCoreModuleClass = context.Files.FirstOrDefault(f => f.Name.EndsWith("EntityFrameworkCoreModule.cs", StringComparison.OrdinalIgnoreCase));
efCoreModuleClass?.ReplaceText("Volo.Abp.EntityFrameworkCore.SqlServer", newModuleNamespace);
efCoreModuleClass?.ReplaceText("AbpEntityFrameworkCoreSqlServerModule", newModuleClass);
}
private void ChangeUseSqlServer(ProjectBuildContext context, string newUseMethodForEfModule, string newUseMethodForDbContext = null)

21
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/RemoveFileStep.cs

@ -0,0 +1,21 @@
using System;
namespace Volo.Abp.Cli.ProjectBuilding.Building.Steps;
public class RemoveFileStep : ProjectBuildPipelineStep
{
private readonly string _filePath;
public RemoveFileStep(string filePath)
{
_filePath = filePath;
}
public override void Execute(ProjectBuildContext context)
{
var fileToRemove = context.Files.Find(x => x.Name.EndsWith(_filePath));
if (fileToRemove != null)
{
context.Files.Remove(fileToRemove);
}
}
}

6
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Building/Steps/TemplateCodeDeleteStep.cs

@ -8,7 +8,11 @@ public class TemplateCodeDeleteStep : ProjectBuildPipelineStep
{
foreach (var file in context.Files)
{
if (file.Name.EndsWith(".cs") || file.Name.EndsWith(".csproj") || file.Name.EndsWith(".cshtml") || file.Name.EndsWith(".json"))
if (file.Name.EndsWith(".cs") ||
file.Name.EndsWith(".csproj") ||
file.Name.EndsWith(".cshtml") ||
file.Name.EndsWith(".json") ||
file.Name.EndsWith(".html"))
{
file.RemoveTemplateCode(context.Symbols);
file.RemoveTemplateCodeMarkers();

18
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/AppTemplateBase.cs

@ -209,6 +209,15 @@ public abstract class AppTemplateBase : TemplateInfo
{
RemoveCmsKitDependenciesFromPackageJsonFiles(steps);
}
if (context.BuildArgs.ExtraProperties.ContainsKey(NewCommand.Options.ProgressiveWebApp.Short))
{
context.Symbols.Add("PWA");
}
else
{
RemovePwaFiles(steps);
}
}
protected static void RemoveCmsKitDependenciesFromPackageJsonFiles(List<ProjectBuildPipelineStep> steps)
@ -238,6 +247,15 @@ public abstract class AppTemplateBase : TemplateInfo
}
}
protected static void RemovePwaFiles(List<ProjectBuildPipelineStep> steps)
{
steps.Add(new RemoveFileStep("/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/wwwroot/service-worker.js"));
steps.Add(new RemoveFileStep("/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/wwwroot/service-worker.published.js"));
steps.Add(new RemoveFileStep("/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/wwwroot/manifest.json"));
steps.Add(new RemoveFileStep("/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/wwwroot/icon-192.png"));
steps.Add(new RemoveFileStep("/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/wwwroot/icon-512.png"));
}
protected bool IsCmsKitSupportedForTargetVersion(ProjectBuildContext context)
{
if (string.IsNullOrWhiteSpace(context.BuildArgs.Version))

9
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectBuilding/Templates/App/BlazorAppsettingsFilePortChangeForSeparatedIdentityServersStep.cs

@ -17,15 +17,20 @@ public class BlazorAppsettingsFilePortChangeForSeparatedIdentityServersStep : Pr
appsettingsFile.NormalizeLineEndings();
var lines = appsettingsFile.GetLines();
for (var i = 0; i < lines.Length; i++)
for (var i = 1; i < lines.Length; i++)
{
var line = lines[i];
var previousLine = lines[i-1];
if (line.Contains("Authority") && line.Contains("localhost"))
{
line = line.Replace("44305", "44301");
}
else if (line.Contains("BaseUrl") && line.Contains("localhost"))
else if (previousLine.Contains("AbpAccountPublic") && line.Contains("BaseUrl") && line.Contains("localhost"))
{
line = line.Replace("44305", "44301");
}
else if (previousLine.Contains("Default") && line.Contains("BaseUrl") && line.Contains("localhost"))
{
line = line.Replace("44305", "44300");
}

5
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/EfCoreMigrationManager.cs

@ -48,9 +48,10 @@ public class EfCoreMigrationManager : ITransientDependency
? string.Empty
: $"--context {dbContext}";
CmdHelper.RunCmd($"cd \"{dbMigrationsProjectFolder}\" && dotnet ef migrations add {migrationName}" +
CmdHelper.RunCmd($"dotnet ef migrations add {migrationName}" +
$" --output-dir {outputDirectory}" +
$" {dbContextOption}");
$" {dbContextOption}",
workingDirectory: dbMigrationsProjectFolder);
}
protected virtual string ParseModuleName(string fullModuleName)

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

@ -14,6 +14,7 @@ using NuGet.Versioning;
using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
using Volo.Abp.IO;
@ -25,7 +26,7 @@ public class NpmPackagesUpdater : ITransientDependency
{
public ILogger<NpmPackagesUpdater> Logger { get; set; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
public InstallLibsCommand InstallLibsCommand { get; }
public IInstallLibsService InstallLibsService { get; }
public ICmdHelper CmdHelper { get; }
private readonly PackageJsonFileFinder _packageJsonFileFinder;
@ -38,13 +39,13 @@ public class NpmPackagesUpdater : ITransientDependency
NpmGlobalPackagesChecker npmGlobalPackagesChecker,
ICancellationTokenProvider cancellationTokenProvider,
CliHttpClientFactory cliHttpClientFactory,
InstallLibsCommand ınstallLibsCommand,
IInstallLibsService installLibsService,
ICmdHelper cmdHelper)
{
_packageJsonFileFinder = packageJsonFileFinder;
_npmGlobalPackagesChecker = npmGlobalPackagesChecker;
CancellationTokenProvider = cancellationTokenProvider;
InstallLibsCommand = ınstallLibsCommand;
InstallLibsService = installLibsService;
CmdHelper = cmdHelper;
_cliHttpClientFactory = cliHttpClientFactory;
Logger = NullLogger<NpmPackagesUpdater>.Instance;
@ -306,10 +307,8 @@ public class NpmPackagesUpdater : ITransientDependency
protected virtual async Task RunInstallLibsAsync(string fileDirectory)
{
var args = new CommandLineArgs("install-libs");
args.Options.Add(InstallLibsCommand.Options.WorkingDirectory.Short, fileDirectory);
await InstallLibsCommand.ExecuteAsync(args);
Logger.LogInformation("Installing client-side packages...");
await InstallLibsService.InstallLibsAsync(fileDirectory);
}
protected virtual void RunYarn(string fileDirectory)

12
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/ProjectNpmPackageAdder.cs

@ -12,6 +12,7 @@ using Volo.Abp.Cli.Args;
using Volo.Abp.Cli.Commands;
using Volo.Abp.Cli.Commands.Services;
using Volo.Abp.Cli.Http;
using Volo.Abp.Cli.LIbs;
using Volo.Abp.Cli.ProjectBuilding;
using Volo.Abp.Cli.Utils;
using Volo.Abp.DependencyInjection;
@ -26,7 +27,7 @@ public class ProjectNpmPackageAdder : ITransientDependency
public SourceCodeDownloadService SourceCodeDownloadService { get; }
public AngularSourceCodeAdder AngularSourceCodeAdder { get; }
public IRemoteServiceExceptionHandler RemoteServiceExceptionHandler { get; }
public InstallLibsCommand InstallLibsCommand { get; }
public IInstallLibsService InstallLibsService { get; }
public ICmdHelper CmdHelper { get; }
private readonly CliHttpClientFactory _cliHttpClientFactory;
public ILogger<ProjectNpmPackageAdder> Logger { get; set; }
@ -36,14 +37,14 @@ public class ProjectNpmPackageAdder : ITransientDependency
SourceCodeDownloadService sourceCodeDownloadService,
AngularSourceCodeAdder angularSourceCodeAdder,
IRemoteServiceExceptionHandler remoteServiceExceptionHandler,
InstallLibsCommand ınstallLibsCommand,
IInstallLibsService installLibsService,
ICmdHelper cmdHelper)
{
JsonSerializer = jsonSerializer;
SourceCodeDownloadService = sourceCodeDownloadService;
AngularSourceCodeAdder = angularSourceCodeAdder;
RemoteServiceExceptionHandler = remoteServiceExceptionHandler;
InstallLibsCommand = ınstallLibsCommand;
InstallLibsService = installLibsService;
CmdHelper = cmdHelper;
_cliHttpClientFactory = cliHttpClientFactory;
Logger = NullLogger<ProjectNpmPackageAdder>.Instance;
@ -142,9 +143,8 @@ public class ProjectNpmPackageAdder : ITransientDependency
return;
}
await InstallLibsCommand.ExecuteAsync(
new CommandLineArgs("install-libs")
);
Logger.LogInformation("Installing client-side packages...");
await InstallLibsService.InstallLibsAsync(directory);
}
}

39
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/SolutionFileModifier.cs

@ -10,13 +10,30 @@ namespace Volo.Abp.Cli.ProjectModification;
public class SolutionFileModifier : ITransientDependency
{
public static Encoding DefaultEncoding = Encoding.UTF8;
public async Task RemoveProjectFromSolutionFileAsync(string solutionFile, string projectName)
{
var solutionFileContent = File.ReadAllText(solutionFile);
solutionFileContent.NormalizeLineEndings();
var lines = solutionFileContent.Split(new[] { Environment.NewLine, "\n" }, StringSplitOptions.None);
File.WriteAllText(solutionFile,
RemoveProject(lines.ToList(), projectName).JoinAsString(Environment.NewLine));
using (var fileStream = File.Open(solutionFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
using (var sr = new StreamReader(fileStream, Encoding.Default, true))
{
var solutionFileContent = await sr.ReadToEndAsync();
solutionFileContent.NormalizeLineEndings();
var lines = solutionFileContent.Split(new[] { Environment.NewLine, "\n" }, StringSplitOptions.None);
var updatedContent = RemoveProject(lines.ToList(), projectName).JoinAsString(Environment.NewLine);
fileStream.Seek(0, SeekOrigin.Begin);
fileStream.SetLength(0);
using (var sw = new StreamWriter(fileStream, DefaultEncoding))
{
await sw.WriteAsync(updatedContent);
await sw.FlushAsync();
}
}
}
}
public async Task AddModuleToSolutionFileAsync(ModuleWithMastersInfo module, string solutionFile)
@ -33,8 +50,8 @@ public class SolutionFileModifier : ITransientDependency
{
var srcFolderId = await AddNewFolderAndGetIdOrGetExistingIdAsync(solutionFile, "src");
var file = File.ReadAllText(solutionFile);
var lines = file.Split(Environment.NewLine).ToList();
var solutionFileContent = File.ReadAllText(solutionFile);
var lines = solutionFileContent.Split(Environment.NewLine).ToList();
if (lines.Any(l => l.Contains($"\"{package.Name}\"")))
{
@ -64,7 +81,7 @@ public class SolutionFileModifier : ITransientDependency
lines.InsertAfter(l => l.Contains("GlobalSection") && l.Contains("NestedProjects"), newPreSolutionLine);
File.WriteAllText(solutionFile, string.Join(Environment.NewLine, lines));
File.WriteAllText(solutionFile, string.Join(Environment.NewLine, lines), DefaultEncoding);
}
private List<string> RemoveProject(List<string> solutionFileLines, string projectName)
@ -137,7 +154,7 @@ public class SolutionFileModifier : ITransientDependency
Path.Combine(Path.GetDirectoryName(solutionFile), "modules", module.Name, "test"),
"*.csproj",
SearchOption.AllDirectories).ToList();
}
}
foreach (var projectPath in projectsUnderModule)
{
@ -174,7 +191,7 @@ public class SolutionFileModifier : ITransientDependency
lines.InsertAfter(l => l.Contains("GlobalSection") && l.Contains("NestedProjects"), newPreSolutionLine);
}
File.WriteAllText(solutionFile, string.Join(Environment.NewLine, lines));
File.WriteAllText(solutionFile, string.Join(Environment.NewLine, lines), Encoding.UTF8);
if (module.MasterModuleInfos != null)
{
@ -219,7 +236,7 @@ public class SolutionFileModifier : ITransientDependency
.Split(" ").Last();
}
File.WriteAllText(solutionFile, string.Join(Environment.NewLine, lines));
File.WriteAllText(solutionFile, string.Join(Environment.NewLine, lines), Encoding.UTF8);
return folderId;
}

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

@ -100,7 +100,7 @@ public class SolutionModuleAdder : ITransientDependency
Check.NotNull(solutionFile, nameof(solutionFile));
Check.NotNull(moduleName, nameof(moduleName));
await PublishEventAsync(1, "Retriving module info...");
await PublishEventAsync(1, "Retrieving module info...");
var module = await GetModuleInfoAsync(moduleName, newTemplate, newProTemplate);
@ -679,7 +679,7 @@ public class SolutionModuleAdder : ITransientDependency
if (!string.IsNullOrEmpty(dbMigratorProject))
{
CmdHelper.RunCmd("cd \"" + Path.GetDirectoryName(dbMigratorProject) + "\" && dotnet run", out int exitCode);
CmdHelper.RunCmd($"dotnet run", out int exitCode, workingDirectory: Path.GetDirectoryName(dbMigratorProject));
}
}

92
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/ProjectModification/VoloNugetPackagesVersionUpdater.cs

@ -9,6 +9,7 @@ using Volo.Abp.Cli.NuGet;
using Volo.Abp.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using System.Text;
namespace Volo.Abp.Cli.ProjectModification;
@ -17,6 +18,7 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency
private readonly NuGetService _nuGetService;
private readonly MyGetPackageListFinder _myGetPackageListFinder;
public ILogger<VoloNugetPackagesVersionUpdater> Logger { get; set; }
public static Encoding DefaultEncoding = Encoding.UTF8;
public VoloNugetPackagesVersionUpdater(NuGetService nuGetService, MyGetPackageListFinder myGetPackageListFinder)
{
@ -41,17 +43,30 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency
async Task UpdateAsync(string filePath)
{
var fileContent = File.ReadAllText(filePath);
var updatedContent = await UpdateVoloPackagesAsync(fileContent,
includePreviews,
includeReleaseCandidates,
switchToStable,
latestVersionFromNuget,
latestReleaseCandidateVersionFromNuget,
latestVersionFromMyGet,
version);
File.WriteAllText(filePath, updatedContent);
using (var fs = File.Open(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
using (var sr = new StreamReader(fs, Encoding.Default, true))
{
var fileContent = await sr.ReadToEndAsync();
var updatedContent = await UpdateVoloPackagesAsync(fileContent,
includePreviews,
includeReleaseCandidates,
switchToStable,
latestVersionFromNuget,
latestReleaseCandidateVersionFromNuget,
latestVersionFromMyGet,
version);
fs.Seek(0, SeekOrigin.Begin);
fs.SetLength(0);
using (var sw = new StreamWriter(fs, DefaultEncoding))
{
await sw.WriteAsync(updatedContent);
await sw.FlushAsync();
}
}
}
}
Task.WaitAll(projectPaths.Select(UpdateAsync).ToArray());
@ -70,27 +85,54 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency
var latestReleaseCandidateVersionFromNuget = await _nuGetService.GetLatestVersionOrNullAsync("Volo.Abp.Core", includeReleaseCandidates: true);
var latestVersionFromMyGet = await GetLatestVersionFromMyGet("Volo.Abp.Core");
var fileContent = File.ReadAllText(projectPath);
using (var fs = File.Open(projectPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
using (var sr = new StreamReader(fs, Encoding.Default, true))
{
var fileContent = await sr.ReadToEndAsync();
var updatedContent = await UpdateVoloPackagesAsync(fileContent,
includeNightlyPreviews,
includeReleaseCandidates,
switchToStable,
latestVersionFromNuget,
latestReleaseCandidateVersionFromNuget,
latestVersionFromMyGet,
version);
var updatedContent = await UpdateVoloPackagesAsync(fileContent,
includeNightlyPreviews,
includeReleaseCandidates,
switchToStable,
latestVersionFromNuget,
latestReleaseCandidateVersionFromNuget,
latestVersionFromMyGet,
version);
fs.Seek(0, SeekOrigin.Begin);
fs.SetLength(0);
File.WriteAllText(projectPath, updatedContent);
using (var sw = new StreamWriter(fs, sr.CurrentEncoding))
{
await sw.WriteAsync(updatedContent);
await sw.FlushAsync();
}
}
}
}
}
protected virtual async Task UpdateInternalAsync(string projectPath, bool includeNightlyPreviews = false, bool includeReleaseCandidates = false, bool switchToStable = false)
{
var fileContent = File.ReadAllText(projectPath);
var updatedContent = await UpdateVoloPackagesAsync(fileContent, includeNightlyPreviews, includeReleaseCandidates, switchToStable);
using (var fs = File.Open(projectPath, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
{
using (var sr = new StreamReader(fs, Encoding.Default, true))
{
var fileContent = await sr.ReadToEndAsync();
var updatedContent = await UpdateVoloPackagesAsync(fileContent, includeNightlyPreviews, includeReleaseCandidates, switchToStable);
File.WriteAllText(projectPath, updatedContent);
fs.Seek(0, SeekOrigin.Begin);
fs.SetLength(0);
using (var sw = new StreamWriter(fs, sr.CurrentEncoding))
{
await sw.WriteAsync(updatedContent);
await sw.FlushAsync();
}
}
}
}
protected virtual async Task<bool> SpecifiedVersionExists(string version, string packageId)
@ -210,7 +252,7 @@ public class VoloNugetPackagesVersionUpdater : ITransientDependency
}
catch (Exception ex)
{
Logger.LogError("Cannot update Volo.* packages! An error occured while updating the package \"{0}\". Error: {1}", packageId, ex.Message);
Logger.LogError("Cannot update Volo.* packages! An error occurred while updating the package \"{0}\". Error: {1}", packageId, ex.Message);
Logger.LogException(ex);
}

16
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/CmdHelper.cs

@ -58,6 +58,22 @@ public class CmdHelper : ICmdHelper, ITransientDependency
}
}
public Process RunCmdAndGetProcess(string command, string workingDirectory = null)
{
var procStartInfo = new ProcessStartInfo(
GetFileName(),
GetArguments(command)
);
if (!string.IsNullOrEmpty(workingDirectory))
{
procStartInfo.WorkingDirectory = workingDirectory;
procStartInfo.CreateNoWindow = false;
}
return Process.Start(procStartInfo);
}
public string RunCmdAndGetOutput(string command, string workingDirectory = null)
{
return RunCmdAndGetOutput(command, out int _, workingDirectory);

6
framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Utils/ICmdHelper.cs

@ -1,4 +1,6 @@
namespace Volo.Abp.Cli.Utils;
using System.Diagnostics;
namespace Volo.Abp.Cli.Utils;
public interface ICmdHelper
{
@ -12,6 +14,8 @@ public interface ICmdHelper
void RunCmd(string command, string workingDirectory = null);
Process RunCmdAndGetProcess(string command, string workingDirectory = null);
void RunCmd(string command, out int exitCode, string workingDirectory = null);
string RunCmdAndGetOutput(string command, string workingDirectory = null);

30
framework/src/Volo.Abp.Core/Volo/Abp/AsyncDisposeFunc.cs

@ -0,0 +1,30 @@
using System;
using System.Threading.Tasks;
using JetBrains.Annotations;
namespace Volo.Abp;
/// <summary>
/// This class can be used to provide an action when
/// DisposeAsync method is called.
/// </summary>
public class AsyncDisposeFunc : IAsyncDisposable
{
private readonly Func<Task> _func;
/// <summary>
/// Creates a new <see cref="AsyncDisposeFunc"/> object.
/// </summary>
/// <param name="func">func to be executed when this object is DisposeAsync.</param>
public AsyncDisposeFunc([NotNull] Func<Task> func)
{
Check.NotNull(func, nameof(func));
_func = func;
}
public async ValueTask DisposeAsync()
{
await _func();
}
}

6
framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeServicesAttribute.cs

@ -15,7 +15,7 @@ public class ExposeServicesAttribute : Attribute, IExposedServiceTypesProvider
public ExposeServicesAttribute(params Type[] serviceTypes)
{
ServiceTypes = serviceTypes ?? new Type[0];
ServiceTypes = serviceTypes ?? Type.EmptyTypes;
}
public Type[] GetExposedServiceTypes(Type targetType)
@ -49,6 +49,10 @@ public class ExposeServicesAttribute : Attribute, IExposedServiceTypesProvider
foreach (var interfaceType in type.GetTypeInfo().GetInterfaces())
{
var interfaceName = interfaceType.Name;
if (interfaceType.IsGenericType)
{
interfaceName = interfaceType.Name.Left(interfaceType.Name.IndexOf('`'));
}
if (interfaceName.StartsWith("I"))
{

4
framework/src/Volo.Abp.Core/Volo/Abp/Localization/CultureHelper.cs

@ -57,8 +57,6 @@ public static class CultureHelper
public static string GetBaseCultureName(string cultureName)
{
return cultureName.Contains("-")
? cultureName.Left(cultureName.IndexOf("-", StringComparison.Ordinal))
: cultureName;
return new CultureInfo(cultureName).Parent.Name;
}
}

19
framework/src/Volo.Abp.Core/Volo/Abp/NullAsyncDisposable.cs

@ -0,0 +1,19 @@
using System;
using System.Threading.Tasks;
namespace Volo.Abp;
public sealed class NullAsyncDisposable : IAsyncDisposable
{
public static NullAsyncDisposable Instance { get; } = new NullAsyncDisposable();
private NullAsyncDisposable()
{
}
public ValueTask DisposeAsync()
{
return default;
}
}

2
framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpAzureEventBusOptions.cs

@ -7,4 +7,6 @@ public class AbpAzureEventBusOptions
public string SubscriberName { get; set; }
public string TopicName { get; set; }
public bool IsServiceBusDisabled { get; set; }
}

15
framework/src/Volo.Abp.EventBus.Azure/Volo/Abp/EventBus/Azure/AbpEventBusAzureModule.cs

@ -1,4 +1,5 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Volo.Abp.AzureServiceBus;
using Volo.Abp.Modularity;
@ -19,9 +20,15 @@ public class AbpEventBusAzureModule : AbpModule
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
context
.ServiceProvider
.GetRequiredService<AzureDistributedEventBus>()
.Initialize();
var options = context.ServiceProvider.GetRequiredService<IOptions<AbpAzureEventBusOptions>>().Value;
if (!options.IsServiceBusDisabled)
{
context
.ServiceProvider
.GetRequiredService<AzureDistributedEventBus>()
.Initialize();
}
}
}

15
framework/src/Volo.Abp.EventBus.RabbitMQ/Volo/Abp/EventBus/RabbitMq/AbpRabbitMqEventBusOptions.cs

@ -1,10 +1,23 @@
namespace Volo.Abp.EventBus.RabbitMq;
using Volo.Abp.RabbitMQ;
namespace Volo.Abp.EventBus.RabbitMq;
public class AbpRabbitMqEventBusOptions
{
public const string DefaultExchangeType = RabbitMqConsts.ExchangeTypes.Direct;
public string ConnectionName { get; set; }
public string ClientName { get; set; }
public string ExchangeName { get; set; }
public string ExchangeType { get; set; }
public string GetExchangeTypeOrDefault()
{
return string.IsNullOrEmpty(ExchangeType)
? DefaultExchangeType
: ExchangeType;
}
}

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

@ -71,7 +71,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe
Consumer = MessageConsumerFactory.Create(
new ExchangeDeclareConfiguration(
AbpRabbitMqEventBusOptions.ExchangeName,
type: "direct",
type: AbpRabbitMqEventBusOptions.GetExchangeTypeOrDefault(),
durable: true
),
new QueueDeclareConfiguration(
@ -320,7 +320,7 @@ public class RabbitMqDistributedEventBus : DistributedEventBusBase, ISingletonDe
{
channel.ExchangeDeclare(
AbpRabbitMqEventBusOptions.ExchangeName,
"direct",
AbpRabbitMqEventBusOptions.GetExchangeTypeOrDefault(),
durable: true
);
}

2
framework/src/Volo.Abp.Features/Volo/Abp/Features/FeatureDefinitionContext.cs

@ -18,7 +18,7 @@ public class FeatureDefinitionContext : IFeatureDefinitionContext
if (Groups.ContainsKey(name))
{
throw new AbpException($"There is already an existing permission group with name: {name}");
throw new AbpException($"There is already an existing feature group with name: {name}");
}
return Groups[name] = new FeatureGroupDefinition(name, displayName);

11
framework/src/Volo.Abp.RabbitMQ/Volo/Abp/RabbitMQ/RabbitMqConsts.cs

@ -8,4 +8,15 @@ public static class RabbitMqConsts
public const int Persistent = 2;
}
public static class ExchangeTypes
{
public const string Direct = "direct";
public const string Topic = "topic";
public const string Fanout = "fanout";
public const string Headers = "headers";
}
}

14
framework/src/Volo.Abp.Settings/Volo/Abp/Settings/SettingEncryptionService.cs

@ -1,4 +1,6 @@
using System;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Security.Encryption;
@ -7,10 +9,12 @@ namespace Volo.Abp.Settings;
public class SettingEncryptionService : ISettingEncryptionService, ITransientDependency
{
protected IStringEncryptionService StringEncryptionService { get; }
public ILogger<SettingEncryptionService> Logger { get; set; }
public SettingEncryptionService(IStringEncryptionService stringEncryptionService)
{
StringEncryptionService = stringEncryptionService;
Logger = NullLogger<SettingEncryptionService>.Instance;
}
public virtual string Encrypt(SettingDefinition settingDefinition, string plainValue)
@ -30,6 +34,14 @@ public class SettingEncryptionService : ISettingEncryptionService, ITransientDep
return encryptedValue;
}
return StringEncryptionService.Decrypt(encryptedValue);
try
{
return StringEncryptionService.Decrypt(encryptedValue);
}
catch (Exception e)
{
Logger.LogException(e);
return string.Empty;
}
}
}

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

@ -16,6 +16,7 @@ public class RemoteStreamContentTestController_Tests : AspNetCoreMvcTestBase
var result = await GetResponseAsync("/api/remote-stream-content-test/download");
result.Content.Headers.ContentType?.ToString().ShouldBe("application/rtf");
result.Content.Headers.ContentDisposition?.FileName.ShouldBe("download.rtf");
result.Content.Headers.ContentLength.ShouldBe("DownloadAsync".Length);
(await result.Content.ReadAsStringAsync()).ShouldBe("DownloadAsync");
}

25
framework/test/Volo.Abp.Core.Tests/Volo/Abp/DependencyInjection/AutoRegistrationHelper_Tests.cs

@ -33,6 +33,21 @@ public class AutoRegistrationHelper_Tests
exposedServices.ShouldContain(typeof(IDerivedService));
}
[Fact]
public void Should_Get_Conventional_Exposed_Generic_Types_By_Default()
{
//Act
var exposedServices = ExposedServiceExplorer.GetExposedServices(typeof(DefaultGenericService));
//Assert
exposedServices.Count.ShouldBe(4);
exposedServices.ShouldContain(typeof(IService));
exposedServices.ShouldContain(typeof(IGenericService<string>));
exposedServices.ShouldContain(typeof(IGenericService<int>));
exposedServices.ShouldContain(typeof(DefaultGenericService));
}
public class DefaultDerivedService : IDerivedService
{
}
@ -50,4 +65,14 @@ public class AutoRegistrationHelper_Tests
{
}
public interface IGenericService<T>
{
}
public class DefaultGenericService : IService, IGenericService<string>, IGenericService<int>
{
}
}

56
framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs

@ -1,4 +1,4 @@
using System.Globalization;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Localization;
using Shouldly;
@ -183,6 +183,60 @@ public class AbpLocalization_Tests : AbpIntegratedTest<AbpLocalization_Tests.Tes
{
_localizer["CarPlural"].Value.ShouldBe("Autos");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans")))
{
_localizer["Car"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans")))
{
_localizer["CarPlural"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-CN")))
{
_localizer["Car"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-CN")))
{
_localizer["CarPlural"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans-CN")))
{
_localizer["Car"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans-CN")))
{
_localizer["CarPlural"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant")))
{
_localizer["Car"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant")))
{
_localizer["CarPlural"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-TW")))
{
_localizer["Car"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-TW")))
{
_localizer["CarPlural"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant-TW")))
{
_localizer["Car"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant-TW")))
{
_localizer["CarPlural"].Value.ShouldBe("汽車");
}
}
[Fact]

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

Loading…
Cancel
Save