@ -14,11 +14,11 @@ When we've examined the ER Diagram, we can see the one-to-many relationship betw
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
### Demo of The Final Application
At the end of this article, we will have created an application as in the below image.
At the end of this article, we will have created an application as in the below gif.


## Creating the Solution
@ -683,6 +683,885 @@ dotnet ef migrations add <Migration_Name>
### Step 5 - (Create Application Services)
* Let's start with defining our DTOs and application service interfaces in the `BookStore.Application.Contracts` layer. We can create a folder-structure like in the below image.
* We can use the [`CrudAppService`](https://docs.abp.io/en/abp/latest/Application-Services#crud-application-services) base class of the ABP Framework to create application services to **Get**, **Create**, **Update** and **Delete** authors and categories.
* **AuthorDto.cs**
```csharp
using System;
using Volo.Abp.Application.Dtos;
namespace BookStore.Authors
{
public class AuthorDto : EntityDto<Guid>
{
public string Name { get; set; }
public DateTime BirthDate { get; set; }
public string ShortBio { get; set; }
}
}
```
* **AuthorLookupDto.cs**
```csharp
using System;
using Volo.Abp.Application.Dtos;
namespace BookStore.Authors
{
public class AuthorLookupDto : EntityDto<Guid>
{
public string Name { get; set; }
}
}
```
* We will use this DTO class to get all authors and list them in a select box in the book creation modal. (Like in the below image.)
* Instead of Author and Category app services (CrudAppService), we need to customize the **GetList**, **GetAsync**, **CreateAsync**, **UpdateAsync** and **DeleteAsync** methods. So we will create custom application services for them.
* Also we will create two additional methods and they are `GetAuthorLookupAsync` and `GetCategoryLookupAsync`. We will use these two methods to retrieve all authors and categories without pagination and listed them as select box item in create/update modals for Book page.
(You can see the usage of these two methods in the below gif.)

* **CategoryDto.cs**
```csharp
using System;
using Volo.Abp.Application.Dtos;
namespace BookStore.Categories
{
public class CategoryDto : EntityDto<Guid>
{
public string Name { get; set; }
}
}
```
* **CategoryLookupDto.cs**
```csharp
using System;
using Volo.Abp.Application.Dtos;
namespace BookStore.Categories
{
public class CategoryLookupDto : EntityDto<Guid>
{
public string Name { get; set; }
}
}
```
* We will use this DTO class to get all categories without pagination and list them in a select box in the book create/update modal.
* After creating the DTOs and application service interfaces, now we can define implementation of that interfaces. So, we can create a folder-structure like in the below image for `BookStore.Application` layer. Open the application service classes and add the following codes to each of these classes.
* As you can notice in here, we've used our **Domain Service** class named `BookManager` in the **CreateAsync** and **UpdateAsync** methods. (In step 1, we've defined them)
* As you may remember, in these methods, new categories are added to the book or removed from the sub-collection (**Categories** (`BookCategory`)) according to the relevant category names.
* After implementing the application services, we need to define the mappings for our services to work. So open the `BookStoreApplicationAutoMapperProfile` class and update with the following code.
```csharp
using AutoMapper;
using BookStore.Authors;
using BookStore.Books;
using BookStore.Categories;
namespace BookStore
{
public class BookStoreApplicationAutoMapperProfile : Profile
{
public BookStoreApplicationAutoMapperProfile()
{
CreateMap<Category,CategoryDto>();
CreateMap<Category,CategoryLookupDto>();
CreateMap<CreateUpdateCategoryDto,Category>();
CreateMap<Author,AuthorDto>();
CreateMap<Author,AuthorLookupDto>();
CreateMap<CreateUpdateAuthorDto,Author>();
CreateMap<BookWithDetails,BookDto>();
}
}
}
```
### Step 6 - (UI)
The only thing we need to do is, by using the application service methods that we've defined in the previous step to create the UI.
> For keep the article shorter, I'll just show you how to create the Book page (with Create/Edit modals). If you want to implement it to other pages, you can access the source code of the application at https://github.com/EngincanV/ABP-Many-to-Many-Relationship-Demo and copy-paste the relevant code-blocks to your application.
#### Book Page
* Create a razor page named **Index.cshtml** under the **Pages/Books** folder of the `BookStore.Web` project and paste the following code to that page.
* In here we've added a **New Book** button and a table with id named "BooksTable". We'll create an `Index.js` file and by the using [datatable.js](https://datatables.net) we will fill the table with our records.
* **Index.js**
```js
$(function () {
var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal');
var editModal = new abp.ModalManager(abp.appPath + 'Books/EditModal');
return "Are you sure to delete the book '" + data.record.name +"'?";
},
action: function (data) {
bookService
.delete(data.record.id)
.then(function() {
abp.notify.info("Successfully deleted!");
dataTable.ajax.reload();
});
}
}
]
}
},
{
title: 'Name',
data: "name"
},
{
title: 'Publish Date',
data: "publishDate",
render: function (data) {
return luxon
.DateTime
.fromISO(data, {
locale: abp.localization.currentCulture.name
}).toLocaleString();
}
},
{
title: 'Author Name',
data: "authorName"
},
{
title: 'Price',
data: "price"
},
{
title: 'Categories',
data: "categoryNames",
render: function (data) {
return data.join(", ");
}
}
]
})
);
createModal.onResult(function () {
dataTable.ajax.reload();
});
editModal.onResult(function () {
dataTable.ajax.reload();
});
$('#NewBookButton').click(function (e) {
e.preventDefault();
createModal.open();
});
});
```
> `abp.libs.datatables.normalizeConfiguration` is a helper function defined by the ABP Framework. It simplifies the Datatables configuration by providing conventional default values for missing options.
* Let's examine what we've done it `Index.js` file.
* Firstly, we've defined our `createModal` and `editModal` modals by using the [ABP Modals](https://docs.abp.io/en/abp/latest/UI/AspNetCore/Modals). Then, we've created the DataTable and fetch our books by using the dynamic JavaScript proxy function (`bookStore.books.book.getList`) (It sends a request to the **GetListAsync** method that we've defined in the `BookAppService`, under the hook) and display them in the table with id named "BooksTable".
* Now let's run the application and navigates to **/Books** route to see how our Book page looks.

* Now that our app is working properly, we can continue to development.
#### View Model Classes and Mapping Configurations
* Create a folder named **Modals** and add a class named `CategoryViewModel` inside of it. We will use this view modal class to determine which categories are selected or not in our Create/Edit modals.
* **CategoryViewModel.cs**
```csharp
using System;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc;
namespace BookStore.Web.Models
{
public class CategoryViewModel
{
[HiddenInput]
public Guid Id { get; set; }
public bool IsSelected { get; set; }
[Required]
[HiddenInput]
public string Name { get; set; }
}
}
```
* Then, we can open the `BookStoreWebAutoMapperProfile` class and define the required mappings as follows.
```csharp
using AutoMapper;
using BookStore.Authors;
using BookStore.Books;
using BookStore.Categories;
using BookStore.Web.Models;
using BookStore.Web.Pages.Books;
using Volo.Abp.AutoMapper;
namespace BookStore.Web
{
public class BookStoreWebAutoMapperProfile : Profile
{
public BookStoreWebAutoMapperProfile()
{
CreateMap<CategoryLookupDto,CategoryViewModel>()
.Ignore(x => x.IsSelected);
CreateMap<BookDto,CreateUpdateBookDto>();
CreateMap<AuthorDto,CreateUpdateAuthorDto>();
CreateMap<CategoryDto,CreateUpdateCategoryDto>();
}
}
}
```
#### Create/Edit Modals
* After creating our index page for Books and configuring mappings, let's continue with creating the Create/Edit modals for Books.
* Create a razor page named **CreateModal.cshtml** (and **CreateModal.cshtml.cs**).
var selectedCategories = Categories.Where(x => x.IsSelected).ToList();
if (selectedCategories.Any())
{
var categoryNames = selectedCategories.Select(x => x.Name).ToArray();
Book.CategoryNames = categoryNames;
}
await _bookAppService.CreateAsync(Book);
return NoContent();
}
}
}
```
* Here, we've get all categories and authors inside of the `OnGetAsync` method. And use them inside of the create modal to list them to let the user choose when creating a new book.

* When the user submitted the form, `OnPostAsync` method runs. Inside of this method, we get the selected categories and pass them into the **CategoryNames** array of the Book object and call the `IBookAppService.CreateAsync` method to create a new book.
* Create a razor page named **EditModal.cshtml** (and **EditModal.cshtml.cs**).
* As in the create model, we've get all categories and authors inside of the `OnGetAsync` method. And also get the book by id and marked as selected for the categories of the book.
* When the user updated the inputs and submitted the form, `OnPostAsync` method runs. Inside of this method, we get the selected categories and pass them into the **CategoryNames** array of the Book object and call the `IBookAppService.UpdateAsync` method to update the book.

#### Conclusion
In this article, I've tried to explain how to create many-to-many relationship by using the ABP framework. (by following DDD principles)
Thanks for reading this article, I hope it was helpful.