diff --git a/README.md b/README.md index 1d7b7e51a7..ebba20da41 100644 --- a/README.md +++ b/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 diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 023a4b18ed..18c5524d1f 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/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" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 8fa96b5ee2..0d7724df56 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/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 here.", "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 info@abp.io.", - "AddBasket": "Add to Basket" + "AddBasket": "Add to Basket", + "SendTrainingRequest": "Send Training Request" } } diff --git a/docs/en/AspNet-Boilerplate-Migration-Guide.md b/docs/en/AspNet-Boilerplate-Migration-Guide.md index 52a2fced6a..e337b78a6d 100644 --- a/docs/en/AspNet-Boilerplate-Migration-Guide.md +++ b/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. diff --git a/docs/en/Caching.md b/docs/en/Caching.md index 13129bbc95..074d6d951f 100644 --- a/docs/en/Caching.md +++ b/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 diff --git a/docs/en/Concurrency-Check.md b/docs/en/Concurrency-Check.md index e22afe82b8..9bad5f816f 100644 --- a/docs/en/Concurrency-Check.md +++ b/docs/en/Concurrency-Check.md @@ -1,3 +1,147 @@ ## Concurrency Check -TODO \ No newline at end of file +### 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, IHasConcurrencyStamp +{ + public string ConcurrencyStamp { get; set; } + + //... +} +``` + +Also, implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface: + +```csharp +public class BookDto : EntityDto, 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, for that purpose you can use the `SetConcurrencyStampIfNotNull` method as below: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + //... + + public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await BookRepository.GetAsync(id); + + book.SetConcurrencyStampIfNotNull(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` +- `CreationAuditedAggregateRoot`, `CreationAuditedAggregateRoot` +- `AuditedAggregateRoot`, `AuditedAggregateRoot` +- `FullAuditedAggregateRoot`, `FullAuditedAggregateRoot` + +**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 +{ + //... +} +``` + +Then, you can implement your output and update the DTO classes from the `IHasConcurrencyStamp` interface: + +```csharp +public class BookDto : EntityDto, 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, for that purpose you can use the `SetConcurrencyStampIfNotNull` method as below: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + //... + + public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await BookRepository.GetAsync(id); + + book.SetConcurrencyStampIfNotNull(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) diff --git a/docs/en/Distributed-Locking.md b/docs/en/Distributed-Locking.md index bea10645ca..d57fd45c83 100644 --- a/docs/en/Distributed-Locking.md +++ b/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(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(sp => + { + var connection = ConnectionMultiplexer .Connect(configuration["Redis:Configuration"]); - return new + return new RedisDistributedSynchronizationProvider(connection.GetDatabase()); - }); - } - } + }); + } + } } ```` diff --git a/docs/en/Modules/Docs.md b/docs/en/Modules/Docs.md index 9653b12618..4777275cd6 100644 --- a/docs/en/Modules/Docs.md +++ b/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. diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md index 6d636d5ecb..50b90b516d 100644 --- a/docs/en/Tutorials/Part-2.md +++ b/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 }}}%} -
+
diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md index 182ffa4005..24f922cb18 100644 --- a/docs/en/Tutorials/Part-3.md +++ b/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 @L["Books"] - + @L["Books"] - +
{%{{{ '::Menu:Books' | abpLocalization }}}%}
-
+
-
+
-
-
+
+
diff --git a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md index 763157e049..5ee4531131 100644 --- a/docs/pt-BR/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/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

@L["Books"]

- + `方法提供一个可选参数, `considerOuw`, 默认为`false`. 如果将其设置为`true`, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元](Unit-Of-Work.md)关联. 你将获得在同一工作单元中设置的缓存值, 但**仅当前工作单元成功时**更改才会生效. +`IDistributedCache<..>`方法提供一个可选参数, `considerUow`, 默认为`false`. 如果将其设置为`true`, 则你对缓存所做的更改不会应用于真正的缓存存储, 而是与当前的[工作单元](Unit-Of-Work.md)关联. 你将获得在同一工作单元中设置的缓存值, 但**仅当前工作单元成功时**更改才会生效. ### IDistributedCacheSerializer diff --git a/docs/zh-Hans/Domain-Services.md b/docs/zh-Hans/Domain-Services.md index 9c9bb7dfd2..a617aea645 100644 --- a/docs/zh-Hans/Domain-Services.md +++ b/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 _issueRepository; + public IssueManager(IRepository 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 +{ + 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 _userRepository; + private readonly IRepository _issueRepository; + public IssueAppService( + IssueManager issueManager, + IRepository userRepository, + IRepository 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) 的,它们会自动注册到依赖注入服务. diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md index 6a0d549abe..346927acbe 100644 --- a/docs/zh-Hans/Tutorials/Part-2.md +++ b/docs/zh-Hans/Tutorials/Part-2.md @@ -514,7 +514,7 @@ export class BookComponent implements OnInit { {%{{{ '::Menu:Books' | abpLocalization }}}%}
-
+
diff --git a/docs/zh-Hans/Tutorials/Part-3.md b/docs/zh-Hans/Tutorials/Part-3.md index 9cd464700a..b8d56fb26a 100644 --- a/docs/zh-Hans/Tutorials/Part-3.md +++ b/docs/zh-Hans/Tutorials/Part-3.md @@ -145,7 +145,7 @@ namespace Acme.BookStore.Web.Pages.Books @L["Books"] - + @L["Books"] - +
{%{{{ '::Menu:Books' | abpLocalization }}}%}
-
-
- +
+
-
+