diff --git a/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/POST.md b/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/POST.md index 3e5f24d15c..97fbbc0ba3 100644 --- a/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/POST.md +++ b/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/POST.md @@ -13,11 +13,18 @@ When we've examined the ER Diagram, we can see the one-to-many relationship betw ### Source Code You can find the source code of the application at https://github.com/EngincanV/ABP-Many-to-Many-Relationship-Demo . + +### Screenshot of The Final Application + +At the end of this article, we will have created an application as in the below image. + +![Book Homepage](./demo.png) + ## Creating the Solution In this article, I will create a new startup template with EF Core as a database provider and MVC for UI framework. -* We can create a new startup template by using [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): +* We can create a new startup template by using the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI): ```bash abp new BookStore -t app --version 5.0.0-beta.2 @@ -33,7 +40,7 @@ Let's start with creating our Domain Entities. We can create a folder-structure under the `BookStore.Domain` project like in the below image. -![Domain-Layer-Folder-Structure](./domain-entity-structure.png) +![Domain-Layer-Folder-Structure](./domain-file-structure.png) Open the entity classes and add the following codes to each of these classes. @@ -86,6 +93,10 @@ namespace BookStore.Authors ```csharp using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using Volo.Abp; using Volo.Abp.Domain.Entities.Auditing; namespace BookStore.Books @@ -94,70 +105,94 @@ namespace BookStore.Books { public Guid AuthorId { get; set; } - public string Name { get; set; } - - public BookType Type { get; set; } + public string Name { get; private set; } public DateTime PublishDate { get; set; } public float Price { get; set; } - - /* This constructor is for deserialization / ORM purpose */ + + public ICollection Categories { get; private set; } + private Book() { } - public Book(Guid id, Guid authorId, string name, BookType type, DateTime publishDate, float price) + public Book(Guid id, Guid authorId, string name, DateTime publishDate, float price) : base(id) { AuthorId = authorId; - Name = name; - Type = type; + SetName(name); PublishDate = publishDate; Price = price; + + Categories = new Collection(); } - } -} -``` -> We'll create the `BookType` enum later in this step. + public void SetName(string name) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), BookConsts.MaxNameLength); + } -* **Category.cs** + public void AddCategory(Guid categoryId) + { + Check.NotNull(categoryId, nameof(categoryId)); + + if (IsInCategory(categoryId)) + { + return; + } + + Categories.Add(new BookCategory(bookId: Id, categoryId)); + } -```csharp -using System; -using Volo.Abp; -using Volo.Abp.Domain.Entities.Auditing; + public void RemoveCategory(Guid categoryId) + { + Check.NotNull(categoryId, nameof(categoryId)); -namespace BookStore.Categories -{ - public class Category : AuditedAggregateRoot - { - public string Name { get; private set; } + if (!IsInCategory(categoryId)) + { + return; + } - public bool IsActive { get; set; } + Categories.RemoveAll(x => x.CategoryId == categoryId); + } - /* This constructor is for deserialization / ORM purpose */ - private Category() + public void RemoveAllCategoriesExceptGivenIds(List categoryIds) { + Check.NotNullOrEmpty(categoryIds, nameof(categoryIds)); + + Categories.RemoveAll(x => !categoryIds.Contains(x.CategoryId)); } - public Category(Guid id, string name, bool isActive = true) : base(id) + public void RemoveAllCategories() { - SetName(name); - IsActive = isActive; + Categories.RemoveAll(x => x.BookId == Id); } - public Category SetName(string name) - { - Name = Check.NotNullOrWhiteSpace(name, nameof(name), BookConsts.MaxNameLength); - return this; + private bool IsInCategory(Guid categoryId) + { + return Categories.Any(x => x.CategoryId == categoryId); } } } ``` -> We'll create the `BookConsts` class later in this step. +* In our scenerio, a book can have more than one category and a category can have more +than one book so it's obvious to we need to establish many-to-many relationship between them. + +* For achieve this we will create an **join entity** named `BookCategory`, and this class +will simply have variables named `BookId` and `CategoryId`. + +* To manage this **join entity**, we can add it as sub-collection to the **Book** entity, as we do above. We add this sub-collection +to **Book** class instead of **Category** class, because a book can have tens or mostly hundreds of category in it but in other +view a category can have more than hundred (or even way much) book inside of it. + +* It is a significant performance problem to load thousands of items whenever you query a category. Therefore it's make much sense to add that sub-collection to `Book` entity. (Don't forget: **An aggregate (with the root entity and sub-collections) should +be serializable and transferrable on the wire as a single unit.**) + +* Notice that, `BookCategory` is not an **Aggregate Root** so we are not violating the one of the base rule about Aggregate Root (Rule: "Reference Other Aggregates Only by ID"). + +> We'll create the `BookCategory` and `BookConsts` classes later in this step. * **BookCategory.cs** @@ -173,6 +208,7 @@ namespace BookStore.Books public Guid CategoryId { get; protected set; } + /* This constructor is for deserialization / ORM purpose */ private BookCategory() { } @@ -191,27 +227,52 @@ namespace BookStore.Books } ``` -Here, as you can notice we've defined the `BookCategory` as the **Join Table** for our many-to-many relationship and ensure the required properties (BookId and CategoryId) must be set to create this object. +* Here, as you can notice we've defined the `BookCategory` as the **Join Table/Entity** for our many-to-many relationship and ensure the required properties (BookId and CategoryId) must be set to create this object. -And also we've derived this class from `Entity` class and therefore we've had to override the **GetKeys** method of this class to define **Composite Key** +* And also we've derived this class from `Entity` class and therefore we've had to override the **GetKeys** method of this class to define **Composite Key**. -> The composite key is composed of `BookId` and `CategoryId` in our case. +> The composite key is composed of `BookId` and `CategoryId` in our case. And they are unique with together. > For more information about **Entities with Composite Keys**, you can read the relavant section from [Entites documentation](https://docs.abp.io/en/abp/latest/Entities#entities-with-composite-keys) +//TODO: add BookManager class later? +* **Category.cs** -* After defining our entities, now we can open the `BookStore.Domain.Shared` project and add the relevant constants and enums from there. +```csharp +using System; +using Volo.Abp; +using Volo.Abp.Domain.Entities.Auditing; -* +namespace BookStore.Categories +{ + public class Category : AuditedAggregateRoot + { + public string Name { get; private set; } + + /* This constructor is for deserialization / ORM purpose */ + private Category() + { + } + + public Category(Guid id, string name) : base(id) + { + SetName(name); + } + + public Category SetName(string name) + { + Name = Check.NotNullOrWhiteSpace(name, nameof(name), CategoryConsts.MaxNameLength); + return this; + } + } +} +``` ### Step 2 - (Database Integration) +### Step 3 - (Database Migration) ---- +### Step 4 - (Create Application Services) -* Create the Domain Entities ✓ -* Database Integration ✓ -* Db migration ✓ -* app services (category -> crud app service) -* ui \ No newline at end of file +### Step 5 - (UI) diff --git a/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/demo.png b/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/demo.png new file mode 100644 index 0000000000..9b954bd7d6 Binary files /dev/null and b/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/demo.png differ diff --git a/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-entity-structure.png b/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-entity-structure.png deleted file mode 100644 index 496483ef1b..0000000000 Binary files a/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-entity-structure.png and /dev/null differ diff --git a/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-file-structure.png b/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-file-structure.png new file mode 100644 index 0000000000..6a5405dafe Binary files /dev/null and b/docs/en/Community-Articles/2021-10-31-Many-to-Many-Relationship-with-ABP-and-EF-Core/domain-file-structure.png differ