mirror of https://github.com/abpframework/abp.git
committed by
GitHub
215 changed files with 6667 additions and 778 deletions
@ -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. |
|||
@ -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. |
|||
|
|||
 |
|||
|
|||
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. |
|||
|
|||
 |
|||
|
|||
* 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. |
|||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 21 KiB |
@ -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) 的,它们会自动注册到依赖注入服务. |
|||
|
|||
@ -1,3 +1,55 @@ |
|||
# Background Jobs Module |
|||
# 后台作业模块 |
|||
|
|||
待添加 |
|||
后台作业模块实现了 `IBackgroundJobStore` 接口,并且可以使用ABP框架的默认后台作业管理.如果你不想使用这个模块,那么你需要自己实现 `IBackgroundJobStore` 接口. |
|||
|
|||
> 本文档仅介绍后台作业模块,该模块将后台作业持久化到数据库.有关后台作业系统的更多信息,请参阅[后台作业](../Background-Jobs.md)文档. |
|||
|
|||
## 如何使用 |
|||
|
|||
当你使用ABP框架[创建一个新的解决方案](https://abp.io/get-started)时,这个模块是(作为NuGet/NPM包)预先安装的.你可以继续将其作为软件包使用并轻松获取更新,也可以将其源代码包含到解决方案中(请参阅 `get-source` [CLI](../CLI.md)命令)以开发自定义模块. |
|||
|
|||
### 源代码 |
|||
|
|||
此模块的源代码可在[此处](https://github.com/abpframework/abp/tree/dev/modules/background-jobs)访问.源代码是由[MIT](https://choosealicense.com/licenses/mit/)授权的,所以你可以自由使用和定制它. |
|||
|
|||
## 内部结构 |
|||
|
|||
### 领域层 |
|||
|
|||
#### 聚合 |
|||
|
|||
- `BackgroundJobRecord` (聚合根): 表示后台工作记录. |
|||
|
|||
#### 仓储 |
|||
|
|||
为该模块定义了以下自定义仓储: |
|||
|
|||
- `IBackgroundJobRepository` |
|||
|
|||
### 数据库提供程序 |
|||
|
|||
#### 通用 |
|||
|
|||
##### 表/集合的前缀与架构 |
|||
|
|||
默认情况下,所有表/集合都使用 `Abp` 前缀.如果需要更改表前缀或设置架构名称(如果数据库提供程序支持),请在 `BackgroundJobsDbProperties` 类上设置静态属性. |
|||
|
|||
##### 连接字符串 |
|||
|
|||
此模块使用 `AbpBackgroundJobs` 作为连接字符串名称.如果不使用此名称定义连接字符串,它将返回 `Default` 连接字符串.有关详细信息,请参阅[连接字符串](https://docs.abp.io/en/abp/latest/Connection-Strings)文档. |
|||
|
|||
#### Entity Framework Core |
|||
|
|||
##### 表 |
|||
|
|||
- **AbpBackgroundJobs** |
|||
|
|||
#### MongoDB |
|||
|
|||
##### 集合 |
|||
|
|||
- **AbpBackgroundJobs** |
|||
|
|||
## 另请参阅 |
|||
|
|||
* [后台作业系统](../Background-Jobs.md) |
|||
|
|||
@ -1,3 +1,257 @@ |
|||
## 规约 |
|||
|
|||
TODO.. |
|||
规约模式用于为实体和其他业务对象定义 **命名、可复用、可组合和可测试的过滤器** . |
|||
|
|||
> 规约是领域层的一部分. |
|||
|
|||
## 安装 |
|||
|
|||
> 这个包 **已经安装** 在启动模板中.所以,大多数时候你不需要手动去安装. |
|||
|
|||
添加 [Volo.Abp.Specifications](https://abp.io/package-detail/Volo.Abp.Specifications) 包到你的项目. 如果当前文件夹是你的项目的根目录(`.csproj`)时,你可以在命令行终端中使用 [ABP CLI](CLI.md) *add package* 命令: |
|||
|
|||
````bash |
|||
abp add-package Volo.Abp.Specifications |
|||
```` |
|||
|
|||
## 定义规约 |
|||
|
|||
假设你定义了如下的顾客实体: |
|||
|
|||
````csharp |
|||
using System; |
|||
using Volo.Abp.Domain.Entities; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class Customer : AggregateRoot<Guid> |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public byte Age { get; set; } |
|||
|
|||
public long Balance { get; set; } |
|||
|
|||
public string Location { get; set; } |
|||
} |
|||
} |
|||
```` |
|||
|
|||
你可以创建一个由 `Specification<Customer>` 派生的新规约类. |
|||
|
|||
**例如:规定选择一个18岁以上的顾客** |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Linq.Expressions; |
|||
using Volo.Abp.Specifications; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class Age18PlusCustomerSpecification : Specification<Customer> |
|||
{ |
|||
public override Expression<Func<Customer, bool>> ToExpression() |
|||
{ |
|||
return c => c.Age >= 18; |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
你只需通过定义一个lambda[表达式](https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/lambda-expressions)来定义规约. |
|||
|
|||
> 你也可以直接实现`ISpecification<T>`接口,但是基类`Specification<T>`做了大量简化. |
|||
|
|||
## 使用规约 |
|||
|
|||
这里有两种常见的规约用例. |
|||
|
|||
### IsSatisfiedBy |
|||
|
|||
`IsSatisfiedBy` 方法可以用于检查单个对象是否满足规约. |
|||
|
|||
**例如:如果顾客不满足年龄规定,则抛出异常** |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class CustomerService : ITransientDependency |
|||
{ |
|||
public async Task BuyAlcohol(Customer customer) |
|||
{ |
|||
if (!new Age18PlusCustomerSpecification().IsSatisfiedBy(customer)) |
|||
{ |
|||
throw new Exception( |
|||
"这位顾客不满足年龄规定!" |
|||
); |
|||
} |
|||
|
|||
//TODO... |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
### ToExpression & Repositories |
|||
|
|||
`ToExpression()` 方法可用于将规约转化为表达式.通过这种方式,你可以使用规约在**数据库查询时过滤实体**. |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Domain.Services; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class CustomerManager : DomainService, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Customer, Guid> _customerRepository; |
|||
|
|||
public CustomerManager(IRepository<Customer, Guid> customerRepository) |
|||
{ |
|||
_customerRepository = customerRepository; |
|||
} |
|||
|
|||
public async Task<List<Customer>> GetCustomersCanBuyAlcohol() |
|||
{ |
|||
var queryable = await _customerRepository.GetQueryableAsync(); |
|||
var query = queryable.Where( |
|||
new Age18PlusCustomerSpecification().ToExpression() |
|||
); |
|||
|
|||
return await AsyncExecuter.ToListAsync(query); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
> 规约被正确地转换为SQL/数据库查询语句,并且在DBMS端高效执行.虽然它与规约无关,但如果你想了解有关 `AsyncExecuter` 的更多信息,请参阅[仓储](Repositories.md)文档. |
|||
|
|||
实际上,没有必要使用 `ToExpression()` 方法,因为规约会自动转换为表达式.这也会起作用: |
|||
|
|||
````csharp |
|||
var queryable = await _customerRepository.GetQueryableAsync(); |
|||
var query = queryable.Where( |
|||
new Age18PlusCustomerSpecification() |
|||
); |
|||
```` |
|||
|
|||
## 编写规约 |
|||
|
|||
规约有一个强大的功能是,它们可以与`And`、`Or`、`Not`以及`AndNot`扩展方法组合使用. |
|||
|
|||
假设你有另一个规约,定义如下: |
|||
|
|||
```csharp |
|||
using System; |
|||
using System.Linq.Expressions; |
|||
using Volo.Abp.Specifications; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class PremiumCustomerSpecification : Specification<Customer> |
|||
{ |
|||
public override Expression<Func<Customer, bool>> ToExpression() |
|||
{ |
|||
return (customer) => (customer.Balance >= 100000); |
|||
} |
|||
} |
|||
} |
|||
``` |
|||
|
|||
你可以将 `PremiumCustomerSpecification` 和 `Age18PlusCustomerSpecification` 结合起来,查询优质成人顾客的数量,如下所示: |
|||
|
|||
````csharp |
|||
using System; |
|||
using System.Threading.Tasks; |
|||
using Volo.Abp.DependencyInjection; |
|||
using Volo.Abp.Domain.Repositories; |
|||
using Volo.Abp.Domain.Services; |
|||
using Volo.Abp.Specifications; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class CustomerManager : DomainService, ITransientDependency |
|||
{ |
|||
private readonly IRepository<Customer, Guid> _customerRepository; |
|||
|
|||
public CustomerManager(IRepository<Customer, Guid> customerRepository) |
|||
{ |
|||
_customerRepository = customerRepository; |
|||
} |
|||
|
|||
public async Task<int> GetAdultPremiumCustomerCountAsync() |
|||
{ |
|||
return await _customerRepository.CountAsync( |
|||
new Age18PlusCustomerSpecification() |
|||
.And(new PremiumCustomerSpecification()).ToExpression() |
|||
); |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
如果你想让这个组合成为一个可复用的规约,你可以创建这样一个组合的规约类,它派生自`AndSpecification`: |
|||
|
|||
````csharp |
|||
using Volo.Abp.Specifications; |
|||
|
|||
namespace MyProject |
|||
{ |
|||
public class AdultPremiumCustomerSpecification : AndSpecification<Customer> |
|||
{ |
|||
public AdultPremiumCustomerSpecification() |
|||
: base(new Age18PlusCustomerSpecification(), |
|||
new PremiumCustomerSpecification()) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
```` |
|||
|
|||
现在,你就可以向下面一样重新编写 `GetAdultPremiumCustomerCountAsync` 方法: |
|||
|
|||
````csharp |
|||
public async Task<int> GetAdultPremiumCustomerCountAsync() |
|||
{ |
|||
return await _customerRepository.CountAsync( |
|||
new AdultPremiumCustomerSpecification() |
|||
); |
|||
} |
|||
```` |
|||
|
|||
> 你可以从这些例子中看到规约的强大之处.如果你之后想要更改 `PremiumCustomerSpecification` ,比如将余额从 `100.000` 修改为 `200.000` ,所有查询语句和合并的规约都将受到本次更改的影响.这是减少代码重复的好方法! |
|||
|
|||
## 讨论 |
|||
|
|||
虽然规约模式通常与C#的lambda表达式相比较,算是一种更老的方式.一些开发人员可能认为不再需要它,我们可以直接将表达式传入到仓储或领域服务中,如下所示: |
|||
|
|||
````csharp |
|||
var count = await _customerRepository.CountAsync(c => c.Balance > 100000 && c.Age => 18); |
|||
```` |
|||
|
|||
自从ABP的[仓储](Repositories.md)支持表达式,这是一个完全有效的用法.你不必在应用程序中定义或使用任何规约,可以直接使用表达式. |
|||
|
|||
所以,规约的意义是什么?为什么或者应该在什么时候考虑去使用它? |
|||
|
|||
### 何时使用? |
|||
|
|||
使用规约的一些好处: |
|||
|
|||
- **可复用**:假设你在代码库的许多地方都需要用到优质顾客过滤器.如果使用表达式而不创建规约,那么如果以后更改“优质顾客”的定义会发生什么?假设你想将最低余额从100000美元更改为250000美元,并添加另一个条件,成为顾客超过3年.如果使用了规约,只需修改一个类.如果在任何其他地方重复(复制/粘贴)相同的表达式,则需要更改所有的表达式. |
|||
- **可组合**:可以组合多个规约来创建新规约.这是另一种可复用性. |
|||
- **命名**:`PremiumCustomerSpecification` 更好地解释了为什么使用规约,而不是复杂的表达式.因此,如果在你的业务中使用了一个有意义的表达式,请考虑使用规约. |
|||
- **可测试**:规约是一个单独(且易于)测试的对象. |
|||
|
|||
### 什么时侯不要使用? |
|||
|
|||
- **没有业务含义的表达式**:不要对与业务无关的表达式和操作使用规约. |
|||
- **报表**:如果只是创建报表,不要创建规约,而是直接使用 `IQueryable` 和LINQ表达式.你甚至可以使用普通SQL、视图或其他工具生成报表.DDD不关心报表,因此从性能角度来看,查询底层数据存储的方式可能很重要. |
|||
|
|||
@ -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>>(); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
|
|||
File diff suppressed because it is too large
@ -0,0 +1,26 @@ |
|||
using Microsoft.EntityFrameworkCore.Migrations; |
|||
|
|||
#nullable disable |
|||
|
|||
namespace Volo.CmsKit.Migrations |
|||
{ |
|||
public partial class Added_BlogPostStatus : Migration |
|||
{ |
|||
protected override void Up(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.AddColumn<int>( |
|||
name: "Status", |
|||
table: "CmsBlogPosts", |
|||
type: "int", |
|||
nullable: false, |
|||
defaultValue: 0); |
|||
} |
|||
|
|||
protected override void Down(MigrationBuilder migrationBuilder) |
|||
{ |
|||
migrationBuilder.DropColumn( |
|||
name: "Status", |
|||
table: "CmsBlogPosts"); |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue