diff --git a/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md b/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md index eec70c5566..a9cdb9b2af 100644 --- a/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md +++ b/docs/en/Blog-Posts/2019-08-16 v0_19_Release/Post.md @@ -12,7 +12,7 @@ Finally, ABP has a **SPA UI** option with the latest [Angular](https://angular.i * Created Angular UI packages for the modules like account, identity and tenant-management. * Created a minimal startup template that authenticates using IdentityServer and uses the ASP.NET Core backend. This template uses the packages mentioned above. * Worked on the [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) and the [download page](https://abp.io/get-started) to be able to generate projects with the new UI option. -* Created a [tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Angular/Part-I) to jump start with the new UI option. +* Created a [tutorial](https://docs.abp.io/en/abp/latest/Tutorials/Part-1?UI=NG) to jump start with the new UI option. We've created the template, document and infrastructure based on the latest Angular tools and trends: diff --git a/docs/en/Getting-Started-Angular-Template.md b/docs/en/Getting-Started-Angular-Template.md index d6d3b84be8..0fe6a62b04 100644 --- a/docs/en/Getting-Started-Angular-Template.md +++ b/docs/en/Getting-Started-Angular-Template.md @@ -123,4 +123,4 @@ The startup template includes the **identity management** and **tenant managemen ### What's Next? -* [Application development tutorial](Tutorials/Angular/Part-I.md) +* [Application development tutorial](Tutorials/Part-1.md?UI=NG) diff --git a/docs/en/Getting-Started-AspNetCore-MVC-Template.md b/docs/en/Getting-Started-AspNetCore-MVC-Template.md index 563ce0182d..fd8ef3a81a 100644 --- a/docs/en/Getting-Started-AspNetCore-MVC-Template.md +++ b/docs/en/Getting-Started-AspNetCore-MVC-Template.md @@ -101,4 +101,4 @@ The startup template includes the **identity management** and **tenant managemen ### What's Next? -* [Application development tutorial](Tutorials/AspNetCore-Mvc/Part-I.md) +* [Application development tutorial](Tutorials/Part-1.md?UI=MVC) diff --git a/docs/en/Startup-Templates/Application.md b/docs/en/Startup-Templates/Application.md index 1fbf1be8e2..d25feb490a 100644 --- a/docs/en/Startup-Templates/Application.md +++ b/docs/en/Startup-Templates/Application.md @@ -5,8 +5,8 @@ This template provides a layered application structure based on the [Domain Driven Design](../Domain-Driven-Design.md) (DDD) practices. This document explains the solution structure and projects in details. If you want to start quickly, follow the guides below: * See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template (uses MVC as the UI framework and Entity Framework Core as the database provider). -* See the [ASP.NET Core MVC Application Development Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template (uses MVC as the UI framework and Entity Framework Core as the database provider). -* See the [Angular Application Development Tutorial](../Tutorials/Angular/Part-I.md) to learn how to develop applications using this template (uses Angular as the UI framework and MongoDB as the database provider). +* See the [ASP.NET Core MVC Application Development Tutorial](../Tutorials/Part-1.md?UI=MVC) to learn how to develop applications using this template (uses MVC as the UI framework and Entity Framework Core as the database provider). +* See the [Angular Application Development Tutorial](../Tutorials/Part-1.md?UI=NG) to learn how to develop applications using this template (uses Angular as the UI framework and MongoDB as the database provider). ## How to Start With? @@ -270,4 +270,4 @@ The files under the `angular/src/environments` folder has the essential configur ## What's Next? - See [Getting Started With the ASP.NET Core MVC Template](../Getting-Started-AspNetCore-MVC-Template.md) to create a new solution and run it for this template. -- See the [ASP.NET Core MVC Tutorial](../Tutorials/AspNetCore-Mvc/Part-I.md) to learn how to develop applications using this template. +- See the [ASP.NET Core MVC Tutorial](../Tutorials/Part-1.md?UI=MVC) to learn how to develop applications using this template. diff --git a/docs/en/Tutorials/Angular/Part-I.md b/docs/en/Tutorials/Angular/Part-I.md index 6d5f91bce7..65a7dc5714 100644 --- a/docs/en/Tutorials/Angular/Part-I.md +++ b/docs/en/Tutorials/Angular/Part-I.md @@ -1,659 +1,6 @@ -## Angular Tutorial - Part I +# Tutorials -### About this Tutorial +## Application Development -In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Angular** will be used as the UI framework and **MongoDB** will be used as the database provider. - -This is the first part of the Angular tutorial series. See all parts: - -- **Part I: Create the project and a book list page (this tutorial)** -- [Part II: Create, Update and Delete books](Part-II.md) -- [Part III: Integration Tests](Part-III.md) - -You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). - -### Creating the Project - -Create a new project named `Acme.BookStore` by selecting the Angular as the UI framework and MongoDB as the database provider, create the database and run the application by following the [Getting Started document](../../Getting-Started-Angular-Template.md). - -### Solution Structure (Backend) - -This is how the layered solution structure looks after it's created: - -![bookstore-backend-solution](images/bookstore-backend-solution-v2.png) - -> You can see the [Application template document](../../Startup-Templates/Application.md) to understand the solution structure in details. However, you will understand the basics with this tutorial. - -### Create the Book Entity - -Domain layer in the startup template is separated into two projects: - -- `Acme.BookStore.Domain` contains your [entities](../../Entities.md), [domain services](../../Domain-Services.md) and other core domain objects. -- `Acme.BookStore.Domain.Shared` contains constants, enums or other domain related objects those can be shared with clients. - -Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below: - -```C# -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore -{ - public class Book : AuditedAggregateRoot - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -- ABP has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](../../Entities.md) for details and best practices. -- `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class. -- `Guid` is the **primary key type** of the `Book` entity. - -#### BookType Enum - -Define the `BookType` enum in the `Acme.BookStore.Domain.Shared` project: - -```C# -namespace Acme.BookStore -{ - public enum BookType - { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry - } -} -``` - -#### Add Book Entity to Your DbContext - -Add a `IMongoCollection` property to the `BookStoreMongoDbContext` inside the `Acme.BookStore.MongoDB` project: - -```csharp -public class BookStoreMongoDbContext : AbpMongoDbContext -{ - public IMongoCollection Books => Collection(); - ... -} -``` - -#### Add Seed (Sample) Data - -This section is optional, but it would be good to have an initial data in the database in the first run. ABP provides a [data seed system](../../Data-Seeding.md). Create a class deriving from the `IDataSeedContributor` in the `.Domain` project: - -```csharp -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookStoreDataSeederContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - - public BookStoreDataSeederContributor(IRepository bookRepository) - { - _bookRepository = bookRepository; - } - - public async Task SeedAsync(DataSeedContext context) - { - if (await _bookRepository.GetCountAsync() > 0) - { - return; - } - - await _bookRepository.InsertAsync( - new Book - { - Name = "1984", - Type = BookType.Dystopia, - PublishDate = new DateTime(1949, 6, 8), - Price = 19.84f - } - ); - - await _bookRepository.InsertAsync( - new Book - { - Name = "The Hitchhiker's Guide to the Galaxy", - Type = BookType.ScienceFiction, - PublishDate = new DateTime(1995, 9, 27), - Price = 42.0f - } - ); - } - } -} - -``` - -`BookStoreDataSeederContributor` simply inserts two books into database if there is no book added before. ABP automatically discovers and executes this class when you seed the database by running the `Acme.BookStore.DbMigrator` project. - -### Create the Application Service - -The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) the books. Application layer in the startup template is separated into two projects: - -- `Acme.BookStore.Application.Contracts` mainly contains your DTOs and application service interfaces. -- `Acme.BookStore.Application` contains the implementations of your application services. - -#### BookDto - -Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project: - -```C# -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore -{ - public class BookDto : AuditedEntityDto - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -``` - -- **DTO** classes are used to **transfer data** between the _presentation layer_ and the _application layer_. See the [Data Transfer Objects document](../../Data-Transfer-Objects.md) for more details. -- `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI. -- `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above. - -It will be needed to convert `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. Startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: - -```csharp -using AutoMapper; - -namespace Acme.BookStore -{ - public class BookStoreApplicationAutoMapperProfile : Profile - { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } - } -} -``` - -#### CreateUpdateBookDto - -Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project: - -```c# -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore -{ - public class CreateUpdateBookDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - public DateTime PublishDate { get; set; } - - [Required] - public float Price { get; set; } - } -} -``` - -- This DTO class is used to get book information from the user interface while creating or updating a book. -- It defines data annotation attributes (like `[Required]`) to define validations for the properties. DTOs are [automatically validated](../../Validation.md) by the ABP framework. - -Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity: - -```csharp -CreateMap(); -``` - -#### IBookAppService - -Define an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project: - -```C# -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore -{ - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books - CreateUpdateBookDto, //Used to create a new book - CreateUpdateBookDto> //Used to update a book - { - - } -} -``` - -- Defining interfaces for application services is not required by the framework. However, it's suggested as a best practice. -- `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually. -- There are some variations of the `ICrudAppService` where you can use separated DTOs for each method. - -#### BookAppService - -Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project: - -```C# -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookAppService : - CrudAppService, - IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - - } - } -} -``` - -- `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD methods defined above. -- `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](../../Repositories.md). -- `BookAppService` uses `IObjectMapper` to convert `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. You defined the mappings before, so it will work as expected. - -### Auto API Controllers - -You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX. ABP can [**automagically**](../../AspNetCore/Auto-API-Controllers.md) configures your application services as MVC API Controllers by convention. - -#### Swagger UI - -The startup template is configured to run the [swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the `Acme.BookStore.HttpApi.Host` application and enter `https://localhost:XXXX/swagger/` (replace XXXX by your own port) as URL on your browser. - -You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: - -![bookstore-swagger](images/bookstore-swagger-api.png) - -Swagger has a nice UI to test APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books. - -### Create the Books Page - -In this tutorial; - -- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services -- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library -- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. -- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor). - -#### Install NPM Packages - -Open a terminal window and go to `angular` folder and then run `yarn` command for installing NPM packages: - -``` -yarn -``` - -#### BooksModule - -Run the following command line to create a new module, named `BooksModule`: - -```bash -yarn ng generate module books --route books --module app.module -``` - -![creating-books-module.terminal](images/bookstore-creating-books-module-terminal.png) - -Run `yarn start`, wait Angular to run the application and open `http://localhost:4200/books` on a browser: - -![initial-books-page](images/bookstore-initial-books-page.png) - -#### Routing - -Open the `app-routing.module.ts` and replace `books` as shown below: - -```js -import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; - -//... -{ - path: 'books', - component: ApplicationLayoutComponent, - loadChildren: () => import('./books/books.module').then(m => m.BooksModule), - data: { - routes: { - name: 'Books', - } as ABP.Route, - }, -}, -``` - -`ApplicationLayoutComponent` configuration sets the application layout to the new page. If you would like to see your route on the navigation bar (main menu) you must also add the `data` object with `name` property in your route. - -![initial-books-page](images/bookstore-initial-books-page-with-layout.png) - -#### Book List Component - -First, replace the `books.component.html` to the following line to place the router-outlet: - -```html - -``` - -Then run the command below on the terminal in the root folder to generate a new component, named book-list: - -```bash -yarn ng generate component books/book-list -``` - -![creating-books-list-terminal](images/bookstore-creating-book-list-terminal.png) - -Import the `SharedModule` to the `BooksModule` to reuse some components and services defined in: - -```js -import { SharedModule } from '../shared/shared.module'; - -@NgModule({ - //... - imports: [ - //... - SharedModule, - ], -}) -export class BooksModule {} -``` - -Then, update the `routes` in the `books-routing.module.ts` to add the new book-list component: - -```js -import { BookListComponent } from './book-list/book-list.component'; - -const routes: Routes = [ - { - path: '', - component: BooksComponent, - children: [{ path: '', component: BookListComponent }], - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class BooksRoutingModule {} -``` - -![initial-book-list-page](images/bookstore-initial-book-list-page.png) - -#### Create BooksState - -Run the following command in the terminal to create a new state, named `BooksState`: - -```shell -yarn ng generate ngxs-schematic:state books -``` - -This command creates several new files and edits `app.modules.ts` to import the `NgxsModule` with the new state: - -```js -// app.module.ts - -import { BooksState } from './store/states/books.state'; - -@NgModule({ - imports: [ - //... - NgxsModule.forRoot([BooksState]), - ], - //... -}) -export class AppModule {} -``` - -#### Get Books Data from Backend - -First, create data types to map data returning from the backend (you can check swagger UI or your backend API to know the data format). - -Modify the `books.ts` as shown below: - -```js -export namespace Books { - export interface State { - books: Response; - } - - export interface Response { - items: Book[]; - totalCount: number; - } - - export interface Book { - name: string; - type: BookType; - publishDate: string; - price: number; - lastModificationTime: string; - lastModifierId: string; - creationTime: string; - creatorId: string; - id: string; - } - - export enum BookType { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry, - } -} -``` - -Added `Book` interface that represents a book object and `BookType` enum represents a book category. - -#### BooksService - -Now, create a new service, named `BooksService` to perform HTTP calls to the server: - -```bash -yarn ng generate service books/shared/books -``` - -![service-terminal-output](images/bookstore-service-terminal-output.png) - -Modify `books.service.ts` as shown below: - -```js -import { Injectable } from '@angular/core'; -import { RestService } from '@abp/ng.core'; -import { Books } from '../../store/models'; -import { Observable } from 'rxjs'; - -@Injectable({ - providedIn: 'root', -}) -export class BooksService { - constructor(private restService: RestService) {} - - get(): Observable { - return this.restService.request({ - method: 'GET', - url: '/api/app/book' - }); - } -} -``` - -Added the `get` method to get the list of books by performing an HTTP request to the related endpoint. - -Replace `books.actions.ts` content as shown below: - -```js -export class GetBooks { - static readonly type = '[Books] Get'; -} -``` - -#### Implement the BooksState - -Open the `books.state.ts` and change the file as shown below: - -```js -import { State, Action, StateContext, Selector } from '@ngxs/store'; -import { GetBooks } from '../actions/books.actions'; -import { Books } from '../models/books'; -import { BooksService } from '../../books/shared/books.service'; -import { tap } from 'rxjs/operators'; - -@State({ - name: 'BooksState', - defaults: { books: {} } as Books.State, -}) -export class BooksState { - @Selector() - static getBooks(state: Books.State) { - return state.books.items || []; - } - - constructor(private booksService: BooksService) {} - - @Action(GetBooks) - get(ctx: StateContext) { - return this.booksService.get().pipe( - tap(booksResponse => { - ctx.patchState({ - books: booksResponse, - }); - }), - ); - } -} -``` - -Added the `GetBooks` action that uses the `BookService` defined above to get the books and patch the state. - -> NGXS requires to return the observable without subscribing it, as done in this sample (in the get function). - -#### BookListComponent - -Modify the `book-list.component.ts` as shown below: - -```js -import { Component, OnInit } from '@angular/core'; -import { Store, Select } from '@ngxs/store'; -import { BooksState } from '../../store/states'; -import { Observable } from 'rxjs'; -import { Books } from '../../store/models'; -import { GetBooks } from '../../store/actions'; - -@Component({ - selector: 'app-book-list', - templateUrl: './book-list.component.html', - styleUrls: ['./book-list.component.scss'], -}) -export class BookListComponent implements OnInit { - @Select(BooksState.getBooks) - books$: Observable; - - booksType = Books.BookType; - - loading = false; - - constructor(private store: Store) {} - - ngOnInit() { - this.loading = true; - this.store.dispatch(new GetBooks()).subscribe(() => { - this.loading = false; - }); - } -} -``` - -> See the [Dispatching Actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the NGXS documentation for more information on these NGXS features. - -Replace `book-list.component.html` content as shown below: - -```html -
-
-
-
-
- Books -
-
-
-
-
- - - - Book name - Book type - Publish date - Price - - - - - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - - - -
-
-``` - -> We've used [PrimeNG table](https://www.primefaces.org/primeng/#/table) in this component. - -The resulting books page is shown below: - -![bookstore-book-list](images/bookstore-book-list.png) - -And this is the folder & file structure by the end of this tutorial: - - - -> This tutorial follows the [Angular Style Guide](https://angular.io/guide/styleguide#file-tree). - -### Next Part - -See the [next part](Part-II.md) of this tutorial. +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/Angular/Part-II.md b/docs/en/Tutorials/Angular/Part-II.md index 6d1c563600..65a7dc5714 100644 --- a/docs/en/Tutorials/Angular/Part-II.md +++ b/docs/en/Tutorials/Angular/Part-II.md @@ -1,587 +1,6 @@ -## Angular Tutorial - Part II +# Tutorials -### About this Tutorial +## Application Development -This is the second part of the Angular tutorial series. See all parts: - -- [Part I: Create the project and a book list page](Part-I.md) -- **Part II: Create, Update and Delete books (this tutorial)** -- [Part III: Integration Tests](Part-III.md) - -You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). - -### Creating a New Book - -In this section, you will learn how to create a new modal dialog form to create a new book. - -#### Type Definition - -Create an interface, named `CreateUpdateBookInput` in the `books.ts` as shown below: - -```js -export namespace Books { - //... - export interface CreateUpdateBookInput { - name: string; - type: BookType; - publishDate: string; - price: number; - } -} -``` - -`CreateUpdateBookInput` interface matches the `CreateUpdateBookDto` in the backend. - -#### Service Method - -Open the `books.service.ts` and add a new method, named `create` to perform an HTTP POST request to the server: - -```js -create(createBookInput: Books.CreateUpdateBookInput): Observable { - return this.restService.request({ - method: 'POST', - url: '/api/app/book', - body: createBookInput - }); -} -``` - -- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used). - -#### State Definitions - -Add the `CreateUpdateBook` action to the `books.actions.ts` as shown below: - -```js -import { Books } from '../models'; - -export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput) {} -} -``` - -Open `books.state.ts` and define the `save` method that will listen to a `CreateUpdateBook` action to create a book: - -```js -import { ... , CreateUpdateBook } from '../actions/books.actions'; -import { ... , switchMap } from 'rxjs/operators'; -//... -@Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - return this.booksService - .create(action.payload) - .pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -When the `SaveBook` action dispatched, the save method is executed. It call `create` method of the `BooksService` defined before. After the service call, `BooksState` dispatches the `GetBooks` action to get books again from the server to refresh the page. - -#### Add a Modal to BookListComponent - -Open the `book-list.component.html` and add the `abp-modal` to show/hide the modal to create a new book. - -```html - - -

New Book

-
- - - - - - -
-``` - -`abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. - -Add a button, labeled `New book` to show the modal: - -```html -
-
-
- Books -
-
-
- -
-
-``` - -Open the `book-list.component.ts` and add `isModalOpen` variable and `createBook` method to show/hide the modal. - -```js -isModalOpen = false; - -//... - -createBook() { - this.isModalOpen = true; -} -``` - -![empty-modal](images/bookstore-empty-new-book-modal.png) - -#### Create a Reactive Form - -> [Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time. - -Add a `form` variable and inject a `FormBuilder` service to the `book-list.component.ts` as shown below (remember add the import statement). - -```js -import { FormGroup, FormBuilder, Validators } from '@angular/forms'; - -form: FormGroup; - -constructor( - //... - private fb: FormBuilder -) {} -``` - -> The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms. - -Add the `buildForm` method to create book form. - -```js -buildForm() { - this.form = this.fb.group({ - name: ['', Validators.required], - type: [null, Validators.required], - publishDate: [null, Validators.required], - price: [null, Validators.required], - }); -} -``` - -- The `group` method of `FormBuilder` (`fb`) creates a `FormGroup`. -- Added `Validators.required` static method that validates the related form element. - -Modify the `createBook` method as shown below: - -```js -createBook() { - this.buildForm(); - this.isModalOpen = true; -} -``` - -#### Create the DOM Elements of the Form - -Open `book-list.component.html` and add the form in the body template of the modal. - -```html - -
-
- * - -
- -
- * - -
- -
- * - -
- -
- * - -
-
-
-``` - -- This template creates a form with Name, Price, Type and Publish date fields. - -> We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. - -#### Datepicker Requirements - -You need to import `NgbDatepickerModule` to the `books.module.ts`: - -```js -import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; - -@NgModule({ - imports: [ - // ... - NgbDatepickerModule, - ], -}) -export class BooksModule {} -``` - -Then open the `book-list.component.ts` and add `providers` as shown below: - -```js -import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; - -@Component({ - // ... - providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }], -}) -export class BookListComponent implements OnInit { -// ... -``` - -> The `NgbDateAdapter` converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. - -#### Create the Book Type Array - -Open the `book-list.component.ts` and then create an array, named `bookTypeArr`: - -```js -//... -booksType = Books.BookType; - -bookTypeArr = Object.keys(Books.BookType).filter( - bookType => typeof this.booksType[bookType] === 'number' -); -``` - -The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below: - -```js -['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] -``` - -This array was used in the previous form template (in the `ngFor` loop). - - -![new-book-form](images/bookstore-new-book-form.png) - -#### Saving the Book - -Open the `book-list.component.html` and add an `abp-button` to save the form. - -```html - - - - -``` - -This adds a save button to the bottom area of the modal: - -![bookstore-new-book-form-v2](images/bookstore-new-book-form-v2.png) - -Then define a `save` method in the `BookListComponent`: - -```js -//... -import { ..., CreateUpdateBook } from '../../store/actions'; -//... -save() { - if (this.form.invalid) { - return; - } - - this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - }); -} -``` - -### Updating An Existing Book - -#### BooksService - -Open the `books.service.ts` and then add the `getById` and `update` methods. - -```js -getById(id: string): Observable { - return this.restService.request({ - method: 'GET', - url: `/api/app/book/${id}` - }); -} - -update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable { - return this.restService.request({ - method: 'PUT', - url: `/api/app/book/${id}`, - body: updateBookInput - }); -} -``` - -#### CreateUpdateBook Action - -Open the `books.actions.ts` and add `id` parameter to the `CreateUpdateBook` action: - -```js -export class CreateUpdateBook { - static readonly type = '[Books] Create Update Book'; - constructor(public payload: Books.CreateUpdateBookInput, public id?: string) {} -} -``` - -Open `books.state.ts` and modify the `save` method as show below: - -```js -@Action(CreateUpdateBook) -save(ctx: StateContext, action: CreateUpdateBook) { - let request; - - if (action.id) { - request = this.booksService.update(action.payload, action.id); - } else { - request = this.booksService.create(action.payload); - } - - return request.pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -#### BookListComponent - -Inject `BooksService` dependency by adding it to the `book-list.component.ts` constructor and add a variable named `selectedBook`. - -```js -import { BooksService } from '../shared/books.service'; -//... -selectedBook = {} as Books.Book; - -constructor( - //... - private booksService: BooksService -) -``` - -`booksService` is used to get the editing book to prepare the form. Modify the `buildForm` method to reuse the same form while editing a book. - -```js -buildForm() { - this.form = this.fb.group({ - name: [this.selectedBook.name || '', Validators.required], - type: this.selectedBook.type || null, - publishDate: this.selectedBook.publishDate ? new Date(this.selectedBook.publishDate) : null, - price: this.selectedBook.price || null, - }); -} -``` - -Add the `editBook` method as shown below: - -```js - editBook(id: string) { - this.booksService.getById(id).subscribe(book => { - this.selectedBook = book; - this.buildForm(); - this.isModalOpen = true; - }); - } -``` - -Added `editBook` method to get the editing book, build the form and show the modal. - -Now, add the `selectedBook` definition to `createBook` method to reuse the same form while creating a new book: - -```js - createBook() { - this.selectedBook = {} as Books.Book; - //... - } -``` - -Modify the `save` method to pass the id of the selected book as shown below: - -```js -save() { - if (this.form.invalid) { - return; - } - - this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) - .subscribe(() => { - this.isModalOpen = false; - this.form.reset(); - }); -} -``` - -#### Add "Actions" Dropdown to the Table - -Open the `book-list.component.html` and add modify the `p-table` as shown below: - -```html - - - - Actions - Book name - Book type - Publish date - Price - - - - - -
- -
- -
-
- - {%{{{ data.name }}}%} - {%{{{ booksType[data.type] }}}%} - {%{{{ data.publishDate | date }}}%} - {%{{{ data.price }}}%} - -
-
-``` - -- Added a `th` for the "Actions" column. -- Added `button` with `ngbDropdownToggle` to open actions when clicked the button. - -> We've used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. - -The final UI looks like: - -![actions-buttons](images/bookstore-actions-buttons.png) - -Update the modal header to change the title based on the current operation: - -```html - -

{%{{{ selectedBook.id ? 'Edit' : 'New Book' }}}%}

-
-``` - -![actions-buttons](images/bookstore-edit-modal.png) - -### Deleting an Existing Book - -#### BooksService - -Open `books.service.ts` and add a `delete` method to delete a book with the `id` by performing an HTTP request to the related endpoint: - -```js -delete(id: string): Observable { - return this.restService.request({ - method: 'DELETE', - url: `/api/app/book/${id}` - }); -} -``` - -#### DeleteBook Action - -Add an action named `DeleteBook` to `books.actions.ts`: - -```js -export class DeleteBook { - static readonly type = '[Books] Delete'; - constructor(public id: string) {} -} -``` - -Open the `books.state.ts` and add the `delete` method that will listen to the `DeleteBook` action to delete a book: - -```js -import { ... , DeleteBook } from '../actions/books.actions'; -//... -@Action(DeleteBook) -delete(ctx: StateContext, action: DeleteBook) { - return this.booksService.delete(action.id).pipe(switchMap(() => ctx.dispatch(new GetBooks()))); -} -``` - -- Added `DeleteBook` to the import list. -- Uses `bookService` to delete the book. - -#### Add a Delete Button - -Open `book-list.component.html` and modify the `ngbDropdownMenu` to add the delete button as shown below: - -```html -
- ... - -
-``` - -The final actions dropdown UI looks like below: - -![bookstore-final-actions-dropdown](images/bookstore-final-actions-dropdown.png) - -#### Delete Confirmation Dialog - -Open `book-list.component.ts` and inject the `ConfirmationService`. - -```js -import { ConfirmationService } from '@abp/ng.theme.shared'; -//... -constructor( - //... - private confirmationService: ConfirmationService -) -``` - -> `ConfirmationService` is a simple service provided by ABP framework that internally uses the PrimeNG. - -Add a delete method to the `BookListComponent`: - -```js -import { ... , DeleteBook } from '../../store/actions'; -import { ... , Toaster } from '@abp/ng.theme.shared'; -//... -delete(id: string, name: string) { - this.confirmationService - .error(`${name} will be deleted. Do you confirm that?`, 'Are you sure?') - .subscribe(status => { - if (status === Toaster.Status.confirm) { - this.store.dispatch(new DeleteBook(id)); - } - }); -} -``` - -The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: - -![bookstore-confirmation-popup](images/bookstore-confirmation-popup.png) - -### Next Part - -See the [next part](Part-III.md) of this tutorial. +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/Angular/Part-III.md b/docs/en/Tutorials/Angular/Part-III.md index 6601bfb938..65a7dc5714 100644 --- a/docs/en/Tutorials/Angular/Part-III.md +++ b/docs/en/Tutorials/Angular/Part-III.md @@ -1,178 +1,6 @@ -## Angular Tutorial - Part III +# Tutorials -### About this Tutorial +## Application Development -This is the third part of the Angular tutorial series. See all parts: - -- [Part I: Create the project and a book list page](Part-I.md) -- [Part II: Create, Update and Delete books](Part-II.md) -- **Part III: Integration Tests (this tutorial)** - -This part covers the **server side** tests. You can access to the **source code** of the application from the [GitHub repository](https://github.com/abpframework/abp/tree/dev/samples/BookStore-Angular-MongoDb). - -### Test Projects in the Solution - -There are multiple test projects in the solution: - -![bookstore-test-projects](images/bookstore-test-projects-v3.png) - -Each project is used to test the related application project. Test projects use the following libraries for testing: - -* [xunit](https://xunit.github.io/) as the main test framework. -* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library. -* [NSubstitute](http://nsubstitute.github.io/) as a mocking library. - -### Adding Test Data - -Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on. - -Change the `BookStoreTestDataSeedContributor` class as show below: - -````C# -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - - public BookStoreTestDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator) - { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; - } - - public async Task SeedAsync(DataSeedContext context) - { - await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 1", - Type = BookType.Fantastic, - PublishDate = new DateTime(2015, 05, 24), - Price = 21 - } - ); - - await _bookRepository.InsertAsync( - new Book - { - Id = _guidGenerator.Create(), - Name = "Test book 2", - Type = BookType.Science, - PublishDate = new DateTime(2014, 02, 11), - Price = 15 - } - ); - } - } -} -```` - -* Injected `IRepository` and used it in the `SeedAsync` to create two book entities as the test data. -* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more). - -### Testing the BookAppService - -Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: - -````C# -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Xunit; - -namespace Acme.BookStore -{ - public class BookAppService_Tests : BookStoreApplicationTestBase - { - private readonly IBookAppService _bookAppService; - - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "Test book 1"); - } - } -} -```` - -* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. - -Add a new test that creates a valid new book: - -````C# -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -```` - -Add a new test that tries to create an invalid book and fails: - -````C# -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); -} -```` - -* Since the `Name` is empty, ABP throws an `AbpValidationException`. - -Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: - -![bookstore-appservice-tests](images/bookstore-test-explorer.png) - -Congratulations, green icons show that tests have been successfully passed! \ No newline at end of file +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png b/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png deleted file mode 100644 index aecf31c1ad..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-actions-buttons.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png deleted file mode 100644 index be05ad3e4a..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-angular-file-tree.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-book-list.png b/docs/en/Tutorials/Angular/images/bookstore-book-list.png deleted file mode 100644 index 3f9717df6e..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-book-list.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png b/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png deleted file mode 100644 index 9f01e94121..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-creating-book-list-terminal.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png b/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png deleted file mode 100644 index c74b37b1b5..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-creating-books-module-terminal.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png b/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png deleted file mode 100644 index 6b0be415c4..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-final-actions-dropdown.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png b/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png deleted file mode 100644 index 0b345ac61d..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-book-list-page.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png b/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png deleted file mode 100644 index 484837f78a..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page-with-layout.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png b/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png deleted file mode 100644 index 4af86c50fa..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-initial-books-page.png and /dev/null differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png b/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png deleted file mode 100644 index 69aaccba31..0000000000 Binary files a/docs/en/Tutorials/Angular/images/bookstore-service-terminal-output.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md index a1fbd1de54..65a7dc5714 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-I.md @@ -1,476 +1,6 @@ -## ASP.NET Core MVC Tutorial - Part I +# Tutorials -### About this Tutorial +## Application Development -In this tutorial series, you will build an application that is used to manage a list of books & their authors. **Entity Framework Core** (EF Core) will be used as the ORM provider as it is the default database provider. - -This is the first part of the ASP.NET Core MVC tutorial series. See all parts: - -- **Part I: Create the project and a book list page (this tutorial)** -- [Part II: Create, Update and Delete books](Part-II.md) -- [Part III: Integration Tests](Part-III.md) - -You can access to the **source code** of the application from [the GitHub repository](https://github.com/abpframework/abp/tree/master/samples/BookStore). - -> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. - -### Creating the Project - -Create a new project named `Acme.BookStore`, create the database and run the application by following the [Getting Started document](../../Getting-Started-AspNetCore-MVC-Template.md). - -### Solution Structure - -This is how the layered solution structure looks after it's created: - -![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution-v3.png) - -> You can see the [Application template document](../../Startup-Templates/Application.md) to understand the solution structure in details. However, you will understand the basics with this tutorial. - -### Create the Book Entity - -Domain layer in the startup template is separated into two projects: - -- `Acme.BookStore.Domain` contains your [entities](../../Entities.md), [domain services](../../Domain-Services.md) and other core domain objects. -- `Acme.BookStore.Domain.Shared` contains constants, enums or other domain related objects those can be shared with clients. - -Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below: - -````C# -using System; -using Volo.Abp.Domain.Entities.Auditing; - -namespace Acme.BookStore -{ - public class Book : AuditedAggregateRoot - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - - protected Book() - { - - } - - public Book(Guid id, string name, BookType type, DateTime publishDate, float price) - :base(id) - { - Name = name; - Type = type; - PublishDate = publishDate; - Price = price; - } - } -} -```` - -* ABP has two fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](../../Entities.md) for details and best practices. -* `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class. -* `Guid` is the **primary key type** of the `Book` entity. - -#### BookType Enum - -Define the `BookType` enum in the `Acme.BookStore.Domain.Shared` project: - -````C# -namespace Acme.BookStore -{ - public enum BookType - { - Undefined, - Adventure, - Biography, - Dystopia, - Fantastic, - Horror, - Science, - ScienceFiction, - Poetry - } -} -```` - -#### Add Book Entity to Your DbContext - -EF Core requires you to relate entities with your DbContext. The easiest way to do this is to add a `DbSet` property to the `BookStoreDbContext` class in the `Acme.BookStore.EntityFrameworkCore` project, as shown below: - -````C# - public class BookStoreDbContext : AbpDbContext - { - public DbSet Books { get; set; } - ... - } -```` - -#### Configure Your Book Entity - -Open `BookStoreDbContextModelCreatingExtensions.cs` file in the `Acme.BookStore.EntityFrameworkCore` project and add following code to the end of the `ConfigureBookStore` method to configure the Book entity: - -````C# -builder.Entity(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); -}); -```` - -#### Add New Migration & Update the Database - -The Startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)** (under the *Tools/Nuget Package Manager* menu), select the `Acme.BookStore.EntityFrameworkCore.DbMigrations` as the **default project** and execute the following command: - -![bookstore-pmc-add-book-migration](images/bookstore-pmc-add-book-migration-v2.png) - -This will create a new migration class inside the `Migrations` folder. Then execute the `Update-Database` command to update the database schema: - -```` -PM> Update-Database -```` - -#### Add Sample Data - -`Update-Database` command created the `AppBooks` table in the database. Open your database and enter a few sample rows, so you can show them on the page: - -![bookstore-books-table](images/bookstore-books-table.png) - -### Create the Application Service - -The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) the books. Application layer in the startup template is separated into two projects: - -* `Acme.BookStore.Application.Contracts` mainly contains your DTOs and application service interfaces. -* `Acme.BookStore.Application` contains the implementations of your application services. - -#### BookDto - -Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project: - -````C# -using System; -using Volo.Abp.Application.Dtos; - -namespace Acme.BookStore -{ - public class BookDto : AuditedEntityDto - { - public string Name { get; set; } - - public BookType Type { get; set; } - - public DateTime PublishDate { get; set; } - - public float Price { get; set; } - } -} -```` - -* **DTO** classes are used to **transfer data** between the *presentation layer* and the *application layer*. See the [Data Transfer Objects document](../../Data-Transfer-Objects.md) for more details. -* `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI. -* `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above. - -It will be needed to convert `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. Startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: - -````csharp -using AutoMapper; - -namespace Acme.BookStore -{ - public class BookStoreApplicationAutoMapperProfile : Profile - { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -#### CreateUpdateBookDto - -Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project: - -````c# -using System; -using System.ComponentModel.DataAnnotations; - -namespace Acme.BookStore -{ - public class CreateUpdateBookDto - { - [Required] - [StringLength(128)] - public string Name { get; set; } - - [Required] - public BookType Type { get; set; } = BookType.Undefined; - - [Required] - public DateTime PublishDate { get; set; } - - [Required] - public float Price { get; set; } - } -} -```` - -* This DTO class is used to get book information from the user interface while creating or updating a book. -* It defines data annotation attributes (like `[Required]`) to define validations for the properties. DTOs are [automatically validated](../../Validation.md) by the ABP framework. - -Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity: - -````csharp -CreateMap(); -```` - -#### IBookAppService - -Define an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project: - -````C# -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; - -namespace Acme.BookStore -{ - public interface IBookAppService : - ICrudAppService< //Defines CRUD methods - BookDto, //Used to show books - Guid, //Primary key of the book entity - PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books - CreateUpdateBookDto, //Used to create a new book - CreateUpdateBookDto> //Used to update a book - { - - } -} -```` - -* Defining interfaces for application services is not required by the framework. However, it's suggested as a best practice. -* `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually. -* There are some variations of the `ICrudAppService` where you can use separated DTOs for each method. - -#### BookAppService - -Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project: - -````C# -using System; -using Volo.Abp.Application.Dtos; -using Volo.Abp.Application.Services; -using Volo.Abp.Domain.Repositories; - -namespace Acme.BookStore -{ - public class BookAppService : - CrudAppService, - IBookAppService - { - public BookAppService(IRepository repository) - : base(repository) - { - - } - } -} -```` - -* `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD methods defined above. -* `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](../../Repositories.md). -* `BookAppService` uses `IObjectMapper` to convert `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. You defined the mappings before, so it will work as expected. - -### Auto API Controllers - -You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX. ABP can [**automagically**](../../AspNetCore/Auto-API-Controllers.md) configures your application services as MVC API Controllers by convention. - -#### Swagger UI - -The startup template is configured to run the [swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the application and enter `https://localhost:XXXX/swagger/` (replace XXXX by your own port) as URL on your browser. - -You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: - -![bookstore-swagger](images/bookstore-swagger.png) - -Swagger has a nice UI to test APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books. - -### Dynamic JavaScript Proxies - -It's common to call HTTP API endpoints via AJAX from the **JavaScript** side. You can use `$.ajax` or another tool to call the endpoints. However, ABP offers a better way. - -ABP **dynamically** creates JavaScript **proxies** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**. - -#### Testing in the Browser Developer Console - -You can easily test the JavaScript proxies using your favorite browser's **Developer Console** now. Run the application, open your browser's **developer tools** (shortcut: F12), switch to the **Console** tab, type the following code and press enter: - -````js -acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); -```` - -* `acme.bookStore` is the namespace of the `BookAppService` converted to [camelCase](https://en.wikipedia.org/wiki/Camel_case). -* `book` is the conventional name for the `BookAppService` (removed AppService postfix and converted to camelCase). -* `getList` is the conventional name for the `GetListAsync` method defined in the `AsyncCrudAppService` base class (removed Async postfix and converted to camelCase). -* `{}` argument is used to send an empty object to the `GetListAsync` method which normally expects an object of type `PagedAndSortedResultRequestDto` that is used to send paging and sorting options to the server (all properties are optional, so you can send an empty object). -* `getList` function returns a `promise`. So, you can pass a callback to the `done` (or `then`) function to get the result from the server. - -Running this code produces the following output: - -![bookstore-test-js-proxy-getlist](images/bookstore-test-js-proxy-getlist.png) - -You can see the **book list** returned from the server. You can also check the **network** tab of the developer tools to see the client to server communication: - -![bookstore-test-js-proxy-getlist-network](images/bookstore-test-js-proxy-getlist-network.png) - -Let's **create a new book** using the `create` function: - -````js -acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); -```` - -You should see a message in the console something like that: - -```` -successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7 -```` - -Check the `Books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions yourself. - -### Create the Books Page - -It's time to create something visible and usable! Instead of classic MVC, we will use the new [Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) approach which is recommended by Microsoft. - -Create a new `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project and add a new Razor Page named `Index.cshtml`: - -![bookstore-add-index-page](images/bookstore-add-index-page-v2.png) - -Open the `Index.cshtml` and change the content as shown below: - -````html -@page -@using Acme.BookStore.Web.Pages.Books -@inherits Acme.BookStore.Web.Pages.BookStorePage -@model IndexModel - -

Books

-```` - -* This code changes the default inheritance of the Razor View Page Model so it **inherits** from the `BookStorePage` class (instead of `PageModel`). The `BookStorePage` class which comes with the startup template and provides some shared properties/methods used by all pages. -* Ensure that the `IndexModel` (*Index.cshtml.cs)* has the `Acme.BookStore.Web.Pages.Books` namespace, or update it in the `Index.cshtml`. - -#### Add Books Page to the Main Menu - -Open the `BookStoreMenuContributor` class in the `Menus` folder and add the following code to the end of the `ConfigureMainMenuAsync` method: - -````c# -context.Menu.AddItem( - new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) - .AddItem(new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books")) -); -```` - -#### Localizing the Menu Items - -Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project: - -![bookstore-localization-files](images/bookstore-localization-files-v2.png) - -Open the `en.json` file and add localization texts for `Menu:BookStore` and `Menu:Books` keys to the end of the file: - -````json -{ - "culture": "en", - "texts": { - "Menu:BookStore": "Book Store", - "Menu:Books": "Books" - } -} -```` - -* ABP's localization system is built on [ASP.NET Core's standard localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) system and extends it in many ways. See the [localization document](../../Localization.md) for details. -* Localization key names are arbitrary. You can set any name. We prefer to add `Menu:` prefix for menu items to distinguish from other texts. If a text is not defined in the localization file, it **fallbacks** to the localization key (ASP.NET Core's standard behavior). - -Run the application and see the new menu item has been added to the top bar: - -![bookstore-menu-items](images/bookstore-menu-items.png) - -When you click to the Books menu item, you are redirected to the new Books page. - -#### Book List - -We will use the [Datatables.net](https://datatables.net/) JQuery plugin to show list of tables on the page. Datatables can completely work via AJAX, it is fast and provides a good user experience. Datatables plugin is configured in the startup template, so you can directly use it in any page without including any style or script file to your page. - -##### Index.cshtml - -Change the `Pages/Books/Index.cshtml` as following: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@model Acme.BookStore.Web.Pages.Books.IndexModel -@section scripts -{ - -} - - -

@L["Books"]

-
- - - - - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - - -
-```` - -* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning** for example. See the [bundling & minification document](../../AspNetCore/Bundling-Minification.md) for details. -* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are many tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help of the intellisense and compile time type checking. See the [tag helpers document](../../AspNetCore/Tag-Helpers/Index.md). -* You can **localize** the column names in the localization file as you did for the menu items above. - -##### Add a Script File - -Create `index.js` JavaScript file under the `Pages/Books/` folder: - -![bookstore-index-js-file](images/bookstore-index-js-file-v2.png) - -`index.js` content is shown below: - -````js -$(function () { - var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); -}); -```` - -* `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to Datatable's format. -* `abp.libs.datatables.normalizeConfiguration` is another helper function. There's no requirement to use it, but it simplifies the datatables configuration by providing conventional values for missing options. -* `acme.bookStore.book.getList` is the function to get list of books (you have seen it before). -* See [Datatable's documentation](https://datatables.net/manual/) for more configuration options. - -The final UI is shown below: - -![bookstore-book-list](images/bookstore-book-list-2.png) - -### Next Part - -See the [next part](Part-II.md) of this tutorial. +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md index 505de55adc..65a7dc5714 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-II.md @@ -1,432 +1,6 @@ -## ASP.NET Core MVC Tutorial - Part II +# Tutorials -### About this Tutorial +## Application Development -This is the second part of the ASP.NET Core MVC tutorial series. See all parts: - -* [Part I: Create the project and a book list page](Part-I.md) -* **Part II: Create, Update and Delete books (this tutorial)** -* [Part III: Integration Tests](Part-III.md) - -You can access to the **source code** of the application from [the GitHub repository](https://github.com/volosoft/abp/tree/master/samples/BookStore). - -> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. - -### Creating a New Book - -In this section, you will learn how to create a new modal dialog form to create a new book. The result dialog will be like that: - -![bookstore-create-dialog](images/bookstore-create-dialog-2.png) - -#### Create the Modal Form - -Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: - -![bookstore-add-create-dialog](images/bookstore-add-create-dialog-v2.png) - -##### CreateModal.cshtml.cs - -Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code: - -````C# -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class CreateModalModel : BookStorePageModel - { - [BindProperty] - public CreateUpdateBookDto Book { get; set; } - - private readonly IBookAppService _bookAppService; - - public CreateModalModel(IBookAppService bookAppService) - { - _bookAppService = bookAppService; - } - - public async Task OnPostAsync() - { - await _bookAppService.CreateAsync(Book); - return NoContent(); - } - } -} -```` - -* This class is derived from the `BookStorePageModel` instead of standard `PageModel`. `BookStorePageModel` inherits the `PageModel` and adds some common properties/methods those can be used by your page model classes. -* `[BindProperty]` attribute on the `Book` property binds post request data to this property. -* This class simply injects the `IBookAppService` in its constructor and calls the `CreateAsync` method in the `OnPostAsync` handler. - -##### CreateModal.cshtml - -Open the `CreateModal.cshtml` file and paste the code below: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model Acme.BookStore.Web.Pages.Books.CreateModalModel -@{ - Layout = null; -} - - - - - - - - - -```` - -* This modal uses `abp-dynamic-form` tag helper to automatically create the form from the `CreateBookViewModel` class. - * `abp-model` attribute indicates the model object, the `Book` property in this case. - * `data-ajaxForm` attribute makes the form submitting via AJAX, instead of a classic page post. - * `abp-form-content` tag helper is a placeholder to render the form controls (this is optional and needed only if you added some other content in the `abp-dynamic-form` tag, just like in this page). - -#### Add the "New book" Button - -Open the `Pages/Books/Index.cshtml` and change the `abp-card-header` tag as shown below: - -````html - - - -

@L["Books"]

-
- - - -
-
-```` - -Just added a **New book** button to the **top right** of the table: - -![bookstore-new-book-button](images/bookstore-new-book-button.png) - -Open the `pages/books/index.js` and add the following code just after the datatable configuration: - -````js -var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); - -createModal.onResult(function () { - dataTable.ajax.reload(); -}); - -$('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); -}); -```` - -* `abp.ModalManager` is a helper class to open and manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API. - -Now, you can **run the application** and add new books using the new modal form. - -### Updating An Existing Book - -Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: - -![bookstore-add-edit-dialog](images/bookstore-add-edit-dialog.png) - -#### EditModal.cshtml.cs - -Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code: - -````csharp -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; - -namespace Acme.BookStore.Web.Pages.Books -{ - public class EditModalModel : BookStorePageModel - { - [HiddenInput] - [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } - - [BindProperty] - public CreateUpdateBookDto 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); - } - - public async Task OnPostAsync() - { - await _bookAppService.UpdateAsync(Id, Book); - return NoContent(); - } - } -} -```` - -* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. Used `SupportsGet` to be able to get Id value from query string parameter of the request. -* Mapped `BookDto` (received from the `BookAppService.GetAsync`) to `CreateUpdateBookDto` in the `GetAsync` method. -* The `OnPostAsync` simply uses `BookAppService.UpdateAsync` to update the entity. - -#### BookDto to CreateUpdateBookDto Mapping - -In order to perform `BookDto` to `CreateUpdateBookDto` object mapping, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below: - -````csharp -using AutoMapper; - -namespace Acme.BookStore.Web -{ - public class BookStoreWebAutoMapperProfile : Profile - { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } - } -} -```` - -* Just added `CreateMap();` as the mapping definition. - -#### EditModal.cshtml - -Replace `EditModal.cshtml` content with the following content: - -````html -@page -@inherits Acme.BookStore.Web.Pages.BookStorePage -@using Acme.BookStore.Web.Pages.Books -@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal -@model EditModalModel -@{ - Layout = null; -} - - - - - - - - - - -```` - -This page is very similar to the `CreateModal.cshtml` except; - -* It includes an `abp-input` for the `Id` property to store id of the editing book (which is a hidden input). -* It uses `Books/EditModal` as the post URL and *Update* text as the modal header. - -#### Add "Actions" Dropdown to the Table - -We will add a dropdown button ("Actions") for each row of the table. The final UI looks like this: - -![bookstore-books-table-actions](images/bookstore-books-table-actions.png) - -Open the `Pages/Books/Index.cshtml` page and change the table section as shown below: - -````html - - - - @L["Actions"] - @L["Name"] - @L["Type"] - @L["PublishDate"] - @L["Price"] - @L["CreationTime"] - - - -```` - -* Just added a new `th` tag for the "Actions". - -Open the `pages/books/index.js` and replace the content as below: - -````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({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -* Used `abp.localization.getResource('BookStore')` to be able to use the same localization texts defined on the server side. -* Added a new `ModalManager` named `createModal` to open the create modal dialog. -* Added a new `ModalManager` named `editModal` to open the edit modal dialog. -* Added a new column at the beginning of the `columnDefs` section. This column is used for the "Actions" dropdown button. -* "New Book" action simply calls `createModal.open` to open the create dialog. -* "Edit" action simply calls `editModal.open` to open the edit dialog. -` -You can run the application and edit any book by selecting the edit action. - -### Deleting an Existing Book - -Open the `pages/books/index.js` and add a new item to the `rowAction` `items`: - -````js -{ - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } -} -```` - -* `confirmMessage` option is used to ask a confirmation question before executing the `action`. -* Used `acme.bookStore.book.delete` javascript proxy function to perform an AJAX request to delete a book. -* `abp.notify.info` is used to show a toastr notification just after the deletion. - -The final `index.js` content is shown below: - -````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({ - processing: true, - serverSide: true, - paging: true, - searching: false, - autoWidth: false, - scrollCollapse: true, - order: [[1, "asc"]], - ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), - columnDefs: [ - { - rowAction: { - items: - [ - { - text: l('Edit'), - action: function (data) { - editModal.open({ id: data.record.id }); - } - }, - { - text: l('Delete'), - confirmMessage: function (data) { - return l('BookDeletionConfirmationMessage', data.record.name); - }, - action: function (data) { - acme.bookStore.book - .delete(data.record.id) - .then(function() { - abp.notify.info(l('SuccessfullyDeleted')); - dataTable.ajax.reload(); - }); - } - } - ] - } - }, - { data: "name" }, - { data: "type" }, - { data: "publishDate" }, - { data: "price" }, - { data: "creationTime" } - ] - })); - - createModal.onResult(function () { - dataTable.ajax.reload(); - }); - - editModal.onResult(function () { - dataTable.ajax.reload(); - }); - - $('#NewBookButton').click(function (e) { - e.preventDefault(); - createModal.open(); - }); -}); -```` - -Open the `en.json` in the `Acme.BookStore.Domain.Shared` project and add the following line: - -````json -"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", -"SuccessfullyDeleted": "Successfully deleted" -```` - -Run the application and try to delete a book. - -### Next Part - -See the [next part](Part-III.md) of this tutorial. +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md index f207d9df0c..65a7dc5714 100644 --- a/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md +++ b/docs/en/Tutorials/AspNetCore-Mvc/Part-III.md @@ -1,166 +1,6 @@ -## ASP.NET Core MVC Tutorial - Part III +# Tutorials -### About this Tutorial +## Application Development -This is the third part of the ASP.NET Core MVC tutorial series. See all parts: - -- [Part I: Create the project and a book list page](Part-I.md) -- [Part II: Create, Update and Delete books](Part-II.md) -- **Part III: Integration Tests (this tutorial)** - -You can access to the **source code** of the application from [the GitHub repository](https://github.com/volosoft/abp/tree/master/samples/BookStore). - -> You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial. - -### Test Projects in the Solution - -There are multiple test projects in the solution: - -![bookstore-test-projects-v2](images/bookstore-test-projects-v2.png) - -Each project is used to test the related application project. Test projects use the following libraries for testing: - -* [xunit](https://xunit.github.io/) as the main test framework. -* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as an assertion library. -* [NSubstitute](http://nsubstitute.github.io/) as a mocking library. - -### Adding Test Data - -Startup template contains the `BookStoreTestDataSeedContributor` class in the `Acme.BookStore.TestBase` project that creates some data to run tests on. - -Change the `BookStoreTestDataSeedContributor` class as show below: - -````C# -using System; -using System.Threading.Tasks; -using Volo.Abp.Data; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Domain.Repositories; -using Volo.Abp.Guids; - -namespace Acme.BookStore -{ - public class BookStoreTestDataSeedContributor - : IDataSeedContributor, ITransientDependency - { - private readonly IRepository _bookRepository; - private readonly IGuidGenerator _guidGenerator; - - public BookStoreTestDataSeedContributor( - IRepository bookRepository, - IGuidGenerator guidGenerator) - { - _bookRepository = bookRepository; - _guidGenerator = guidGenerator; - } - - public async Task SeedAsync(DataSeedContext context) - { - await _bookRepository.InsertAsync( - new Book(_guidGenerator.Create(), "Test book 1", BookType.Fantastic, new DateTime(2015, 05, 24), 21) - ); - - await _bookRepository.InsertAsync( - new Book(_guidGenerator.Create(), "Test book 2", BookType.Science, new DateTime(2014, 02, 11), 15) - ); - } - } -} -```` - -* Injected `IRepository` and used it in the `SeedAsync` to create two book entities as the test data. -* Used `IGuidGenerator` service to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases (see the [Guid generation document](../../Guid-Generation.md) for more). - -### Testing the BookAppService - -Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: - -````C# -using System.Threading.Tasks; -using Shouldly; -using Volo.Abp.Application.Dtos; -using Xunit; - -namespace Acme.BookStore -{ - public class BookAppService_Tests : BookStoreApplicationTestBase - { - private readonly IBookAppService _bookAppService; - - public BookAppService_Tests() - { - _bookAppService = GetRequiredService(); - } - - [Fact] - public async Task Should_Get_List_Of_Books() - { - //Act - var result = await _bookAppService.GetListAsync( - new PagedAndSortedResultRequestDto() - ); - - //Assert - result.TotalCount.ShouldBeGreaterThan(0); - result.Items.ShouldContain(b => b.Name == "Test book 1"); - } - } -} -```` - -* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. - -Add a new test that creates a valid new book: - -````C# -[Fact] -public async Task Should_Create_A_Valid_Book() -{ - //Act - var result = await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "New test book 42", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - - //Assert - result.Id.ShouldNotBe(Guid.Empty); - result.Name.ShouldBe("New test book 42"); -} -```` - -Add a new test that tries to create an invalid book and fails: - -````C# -[Fact] -public async Task Should_Not_Create_A_Book_Without_Name() -{ - var exception = await Assert.ThrowsAsync(async () => - { - await _bookAppService.CreateAsync( - new CreateUpdateBookDto - { - Name = "", - Price = 10, - PublishDate = DateTime.Now, - Type = BookType.ScienceFiction - } - ); - }); - - exception.ValidationErrors - .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); -} -```` - -* Since the `Name` is empty, ABP throws an `AbpValidationException`. - -Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: - -![bookstore-appservice-tests](images/bookstore-appservice-tests.png) - -Congratulations, green icons show that tests have been successfully passed! \ No newline at end of file +* [With ASP.NET Core MVC / Razor Pages UI](../Part-1?UI=MVC) +* [With Angular UI](../Part-1?UI=NG) diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png deleted file mode 100644 index f531e6f457..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png deleted file mode 100644 index 79314dd2dc..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-localization-files-v2.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png deleted file mode 100644 index edf2826361..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration-v2.png and /dev/null differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png b/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png deleted file mode 100644 index 437c772503..0000000000 Binary files a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-swagger.png and /dev/null differ diff --git a/docs/en/Tutorials/Index.md b/docs/en/Tutorials/Index.md deleted file mode 100644 index 7391b6303e..0000000000 --- a/docs/en/Tutorials/Index.md +++ /dev/null @@ -1,6 +0,0 @@ -# Tutorials - -## Application Development - -* [With ASP.NET Core MVC / Razor Pages UI](AspNetCore-Mvc/Part-I.md) -* [With Angular UI](Angular/Part-I.md) diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md new file mode 100644 index 0000000000..67b3195835 --- /dev/null +++ b/docs/en/Tutorials/Part-1.md @@ -0,0 +1,1117 @@ +## ASP.NET Core {{UI_Value}} Tutorial - Part 1 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### About this tutorial: + +In this tutorial series, you will build an ABP Commercial application named `Acme.BookStore`. In this sample project, we will manage a list of books and authors. **{{DB_Text}}** will be used as the ORM provider. And on the front-end side {{UI_Value}} and JavaScript will be used. + +The ASP.NET Core {{UI_Value}} tutorial series consists of 3 parts: + +- **Part-1: Creating the project and book list page (this tutorial)** +- [Part-2: Creating, updating and deleting books](part-2.md) +- [Part-3: Integration tests](part-3.md) + +*You can also check out [the video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by the community, based on this tutorial.* + +### Creating the project + +Create a new project named `Acme.BookStore` where `Acme` is the company name and `BookStore` is the project name. You can check out [creating a new project](../Getting-Started-{{if UI == 'NG'}}Angular{{else}}AspNetCore-MVC{{end}}-Template#creating-a-new-project) document to see how you can create a new project. We will create the project with ABP CLI. But first of all, we need to login to the ABP Platform to create a commercial project. + +#### Create the project + +By running the below command, it creates a new ABP Commercial project with the database provider `{{DB_Text}}` and UI option `MVC`. To see the other CLI options, check out [ABP CLI](https://docs.abp.io/en/abp/latest/CLI) document. + +```bash +abp new Acme.BookStore --template app --database-provider {{DB}} --ui {{UI_Text}} +``` +![Creating project](./images/bookstore-create-project-{{UI_Text}}.png) + +### Apply migrations + +After creating the project, you need to apply the initial migrations and create the database. To apply migrations, right click on the `Acme.BookStore.DbMigrator` and click **Debug** > **Start New Instance**. This will run the application and apply all migrations. You will see the below result when it successfully completes the process. The application database is ready! + +![Migrations applied](./images/bookstore-migrations-applied-{{UI_Text}}.png) + +> Alternatively, you can run `Update-Database` command in the Visual Studio > Package Manager Console to apply migrations. + +#### Initial database tables + +![Initial database tables](./images/bookstore-database-tables-{{DB}}.png) + +### Run the application + +To run the project, right click to the {{if UI == "MVC"}} `Acme.BookStore.Web`{{end}} {{if UI == "NG"}} `Acme.BookStore.HttpApi.Host` {{end}} project and click **Set As StartUp Project**. And run the web project by pressing **CTRL+F5** (*without debugging and fast*) or press **F5** (*with debugging and slow*). {{if UI == "NG"}}You will see the Swagger UI for BookStore API.{{end}} + +Further information, see the [running the application section](../../Getting-Started-{{if UI == "NG"}}Angular{{else}}AspNetCore-MVC{{end}}-Template#running-the-application).Getting-Started-AspNetCore-MVC-Template#running-the-application + +![Set as startup project](./images/bookstore-start-project-{{UI_Text}}.png) + +{{if UI == "NG"}} + +To start Angular project, go to the `angular` folder, open a command line terminal, execute the `yarn` command: + +```bash +yarn +``` + +Once all node modules are loaded, execute the `yarn start` command: + +```bash +yarn start +``` + +The website will be accessible from the following default URL: + +http://localhost:4200/ + +If you see the website's landing page successfully, you can exit Angular hosting by pressing `ctrl-c`. (We'll later start it again.) + +> Be aware that, Firefox does not use the Windows Certificate Store, so you'll need to add the self-signed developer certificate to Firefox manually. To do this, open Firefox and navigate to the below URL: +> +> https://localhost:44322/api/abp/application-configuration +> +> If you see the below screen, click the **Accept the Risk and Continue** button to bypass this warning. +> +> ![Set as startup project](./images/mozilla-self-signed-cert-error.png) + +{{end}} + +The default login credentials are; + +* **Username**: admin +* **Password**: 1q2w3E* + +### Solution structure + +This is how the layered solution structure looks like: + +![bookstore-visual-studio-solution](./images/bookstore-solution-structure-{{UI_Text}}.png) + +Check out the [solution structure](../startup-templates/application#solution-structure) section to understand the structure in details. + +### Create the book entity + +Domain layer in the startup template is separated into two projects: + +- `Acme.BookStore.Domain` contains your [entities](https://docs.abp.io/en/abp/latest/Entities), [domain services](https://docs.abp.io/en/abp/latest/Domain-Services) and other core domain objects. +- `Acme.BookStore.Domain.Shared` contains `constants`, `enums` or other domain related objects those can be shared with clients. + +Define [entities](https://docs.abp.io/en/abp/latest/Entities) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`. Create a class, named `Book`, in the `Acme.BookStore.Domain` project as shown below: + +````csharp +using System; +using Volo.Abp.Domain.Entities.Auditing; + +namespace Acme.BookStore +{ + public class Book : AuditedAggregateRoot + { + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + + protected Book() + { + + } + + public Book(Guid id, string name, BookType type, DateTime publishDate, float price) : + base(id) + { + Name = name; + Type = type; + PublishDate = publishDate; + Price = price; + } + } +} +```` + +* ABP has 2 fundamental base classes for entities: `AggregateRoot` and `Entity`. **Aggregate Root** is one of the **Domain Driven Design (DDD)** concepts. See [entity document](https://docs.abp.io/en/abp/latest/Entities) for details and best practices. +* `Book` entity inherits `AuditedAggregateRoot` which adds some auditing properties (`CreationTime`, `CreatorId`, `LastModificationTime`... etc.) on top of the `AggregateRoot` class. +* `Guid` is the **primary key type** of the `Book` entity. + +#### BookType enum + +Create the `BookType` enum in the `Acme.BookStore.Domain.Shared` project: + +````csharp +namespace Acme.BookStore +{ + public enum BookType + { + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry + } +} +```` + +#### Add book entity to the DbContext + +{{if DB == "ef"}} + +EF Core requires to relate entities with your `DbContext`. The easiest way to do this is to add a `DbSet` property to the `BookStoreDbContext` class in the `Acme.BookStore.EntityFrameworkCore` project, as shown below: + +````csharp + public class BookStoreDbContext : AbpDbContext + { + public DbSet Users { get; set; } + public DbSet Books { get; set; } //<--added this line--> + //... + } +```` + +{{end}} + +{{if DB == "mongodb"}} + +Add a `IMongoCollection Books` property to the `BookStoreMongoDbContext` inside the `Acme.BookStore.MongoDB` project: + +```csharp +public class BookStoreMongoDbContext : AbpMongoDbContext +{ + public IMongoCollection Users => Collection(); + public IMongoCollection Books => Collection();//<--added this line--> + //... +} +``` + +{{end}} + +{{if DB == "ef"}} + +#### Configure the book entity + +Open `BookStoreDbContextModelCreatingExtensions.cs` file in the `Acme.BookStore.EntityFrameworkCore` project and add following code to the end of the `ConfigureBookStore` method to configure the Book entity: + +````csharp +builder.Entity(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); +}); +```` + +Add the `using Volo.Abp.EntityFrameworkCore.Modeling;` statement to resolve `ConfigureByConvention` extension method. + +{{end}} + +{{if DB == "mongodb"}} + +#### Add seed (sample) data + +Adding sample data is optional, but it's good to have initial data in the database for the first run. ABP provides a [data seed system](https://docs.abp.io/en/abp/latest/Data-Seeding). Create a class deriving from the `IDataSeedContributor` in the `*.Domain` project: + +```csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; + +namespace Acme.BookStore +{ + public class BookStoreDataSeederContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public BookStoreDataSeederContributor( + IRepository bookRepository, + IGuidGenerator guidGenerator) + { + _bookRepository = bookRepository; + _guidGenerator = guidGenerator; + } + + public async Task SeedAsync(DataSeedContext context) + { + if (await _bookRepository.GetCountAsync() > 0) + { + return; + } + + await _bookRepository.InsertAsync( + new Book( + id: _guidGenerator.Create(), + name: "1984", + type: BookType.Dystopia, + publishDate: new DateTime(1949, 6, 8), + price: 19.84f + ) + ); + + await _bookRepository.InsertAsync( + new Book( + id: _guidGenerator.Create(), + name: "The Hitchhiker's Guide to the Galaxy", + type: BookType.ScienceFiction, + publishDate: new DateTime(1995, 9, 27), + price: 42.0f + ) + ); + } + } +} +``` + +{{end}} + +{{if DB == "ef"}} + +#### Add new migration & update the database + +The startup template uses [EF Core Code First Migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/) to create and maintain the database schema. Open the **Package Manager Console (PMC)** under the menu *Tools > NuGet Package Manager*. + +![Open Package Manager Console](./images/bookstore-open-package-manager-console.png) + +Select the `Acme.BookStore.EntityFrameworkCore.DbMigrations` as the **default project** and execute the following command: + +```bash +Add-Migration "Created_Book_Entity" +``` + +![bookstore-pmc-add-book-migration](./images/bookstore-pmc-add-book-migration-v2.png) + +This will create a new migration class inside the `Migrations` folder of the `Acme.BookStore.EntityFrameworkCore.DbMigrations` project. Then execute the `Update-Database` command to update the database schema: + +````bash +Update-Database +```` + +![bookstore-update-database-after-book-entity](./images/bookstore-update-database-after-book-entity.png) + +#### Add initial (sample) data + +`Update-Database` command has created the `AppBooks` table in the database. Open your database and enter a few sample rows, so you can show them on the listing page. + +```mssql +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('f3c04764-6bfd-49e2-859e-3f9bfda6183e', '2018-07-01', '1984',3,'1949-06-08','19.84') + +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('13024066-35c9-473c-997b-83cd8d3e29dc', '2018-07-01', 'The Hitchhiker`s Guide to the Galaxy',7,'1995-09-27','42') + +INSERT INTO AppBooks (Id,CreationTime,[Name],[Type],PublishDate,Price) VALUES +('4fa024a1-95ac-49c6-a709-6af9e4d54b54', '2018-07-02', 'Pet Sematary',5,'1983-11-14','23.7') +``` + +![bookstore-books-table](./images/bookstore-books-table.png) + +{{end}} + +### Create the application service + +The next step is to create an [application service](../../Application-Services.md) to manage the books which will allow us the four basic functions: creating, reading, updating and deleting. Application layer is separated into two projects: + +* `Acme.BookStore.Application.Contracts` mainly contains your `DTO`s and application service interfaces. +* `Acme.BookStore.Application` contains the implementations of your application services. + +#### BookDto + +Create a DTO class named `BookDto` into the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System; +using Volo.Abp.Application.Dtos; + +namespace Acme.BookStore +{ + public class BookDto : AuditedEntityDto + { + public string Name { get; set; } + + public BookType Type { get; set; } + + public DateTime PublishDate { get; set; } + + public float Price { get; set; } + } +} +```` + +* **DTO** classes are used to **transfer data** between the *presentation layer* and the *application layer*. See the [Data Transfer Objects document](https://docs.abp.io/en/abp/latest/Data-Transfer-Objects) for more details. +* `BookDto` is used to transfer book data to the presentation layer in order to show the book information on the UI. +* `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` class defined above. + +It will be needed to map `Book` entities to `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. The startup template comes with AutoMapper configured, so you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: + +````csharp +using AutoMapper; + +namespace Acme.BookStore +{ + public class BookStoreApplicationAutoMapperProfile : Profile + { + public BookStoreApplicationAutoMapperProfile() + { + CreateMap(); + } + } +} +```` + +#### CreateUpdateBookDto + +Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System; +using System.ComponentModel.DataAnnotations; + +namespace Acme.BookStore +{ + public class CreateUpdateBookDto + { + [Required] + [StringLength(128)] + public string Name { get; set; } + + [Required] + public BookType Type { get; set; } = BookType.Undefined; + + [Required] + public DateTime PublishDate { get; set; } + + [Required] + public float Price { get; set; } + } +} +```` + +* This `DTO` class is used to get book information from the user interface while creating or updating a book. +* It defines data annotation attributes (like `[Required]`) to define validations for the properties. `DTO`s are [automatically validated](https://docs.abp.io/en/abp/latest/Validation) by the ABP framework. + +Next, add a mapping in `BookStoreApplicationAutoMapperProfile` from the `CreateUpdateBookDto` object to the `Book` entity with the `CreateMap();` command: + +````csharp +using AutoMapper; + +namespace Acme.BookStore +{ + public class BookStoreApplicationAutoMapperProfile : Profile + { + public BookStoreApplicationAutoMapperProfile() + { + CreateMap(); + CreateMap(); //<--added this line--> + } + } +} +```` + +#### IBookAppService + +Create an interface named `IBookAppService` in the `Acme.BookStore.Application.Contracts` project: + +````csharp +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; + +namespace Acme.BookStore +{ + public interface IBookAppService : + ICrudAppService< //Defines CRUD methods + BookDto, //Used to show books + Guid, //Primary key of the book entity + PagedAndSortedResultRequestDto, //Used for paging/sorting on getting a list of books + CreateUpdateBookDto, //Used to create a new book + CreateUpdateBookDto> //Used to update a book + { + + } +} +```` + +* Defining interfaces for the application services **are not required** by the framework. However, it's suggested as a best practice. +* `ICrudAppService` defines common **CRUD** methods: `GetAsync`, `GetListAsync`, `CreateAsync`, `UpdateAsync` and `DeleteAsync`. It's not required to extend it. Instead, you could inherit from the empty `IApplicationService` interface and define your own methods manually. +* There are some variations of the `ICrudAppService` where you can use separated DTOs for each method. + +#### BookAppService + +Implement the `IBookAppService` as named `BookAppService` in the `Acme.BookStore.Application` project: + +````csharp +using System; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Application.Services; +using Volo.Abp.Domain.Repositories; + +namespace Acme.BookStore +{ + public class BookAppService : + CrudAppService, + IBookAppService + { + public BookAppService(IRepository repository) + : base(repository) + { + + } + } +} +```` + +* `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD (create, read, update, delete) methods defined above. +* `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](https://docs.abp.io/en/abp/latest/Repositories). +* `BookAppService` uses `IObjectMapper` to map `Book` objects to `BookDto` objects and `CreateUpdateBookDto` objects to `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. We have defined the mappings before, so it will work as expected. + +### Auto API Controllers + +We normally create **Controllers** to expose application services as **HTTP API** endpoints. This allows browsers or 3rd-party clients to call them via AJAX. ABP can [**automagically**](https://docs.abp.io/en/abp/latest/AspNetCore/Auto-API-Controllers) configures your application services as MVC API Controllers by convention. + +#### Swagger UI + +The startup template is configured to run the [Swagger UI](https://swagger.io/tools/swagger-ui/) using the [Swashbuckle.AspNetCore](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) library. Run the application by pressing `CTRL+F5` and navigate to `https://localhost:/swagger/` on your browser. (Replace `` with your own port number.) + +You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints: + +![bookstore-swagger](./images/bookstore-swagger.png) + +Swagger has a nice interface to test the APIs. You can try to execute the `[GET] /api/app/book` API to get a list of books. + +{{if UI == "MVC"}} + +### Dynamic JavaScript proxies + +It's common to call HTTP API endpoints via AJAX from the **JavaScript** side. You can use `$.ajax` or another tool to call the endpoints. However, ABP offers a better way. + +ABP **dynamically** creates JavaScript **proxies** for all API endpoints. So, you can use any **endpoint** just like calling a **JavaScript function**. + +#### Testing in developer console of the browser + +You can easily test the JavaScript proxies using your favorite browser's **Developer Console**. Run the application, open your browser's **developer tools** (*shortcut is F12 for Chrome*), switch to the **Console** tab, type the following code and press enter: + +````js +acme.bookStore.book.getList({}).done(function (result) { console.log(result); }); +```` + +* `acme.bookStore` is the namespace of the `BookAppService` converted to [camelCase](https://en.wikipedia.org/wiki/Camel_case). +* `book` is the conventional name for the `BookAppService` (removed `AppService` postfix and converted to camelCase). +* `getList` is the conventional name for the `GetListAsync` method defined in the `AsyncCrudAppService` base class (removed `Async` postfix and converted to camelCase). +* `{}` argument is used to send an empty object to the `GetListAsync` method which normally expects an object of type `PagedAndSortedResultRequestDto` that is used to send paging and sorting options to the server (all properties are optional, so you can send an empty object). +* `getList` function returns a `promise`. You can pass a callback to the `done` (or `then`) function to get the result from the server. + +Running this code produces the following output: + +![bookstore-test-js-proxy-getlist](./images/bookstore-test-js-proxy-getlist.png) + +You can see the **book list** returned from the server. You can also check the **network** tab of the developer tools to see the client to server communication: + +![bookstore-test-js-proxy-getlist-network](./images/bookstore-test-js-proxy-getlist-network.png) + +Let's **create a new book** using the `create` function: + +````js +acme.bookStore.book.create({ name: 'Foundation', type: 7, publishDate: '1951-05-24', price: 21.5 }).done(function (result) { console.log('successfully created the book with id: ' + result.id); }); +```` + +You should see a message in the console something like that: + +````js +successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246 +```` + +Check the `Books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions yourself. + +### Create the books page + +It's time to create something visible and usable! Instead of classic MVC, we will use the new [Razor Pages UI](https://docs.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/razor-pages-start) approach which is recommended by Microsoft. + +Create `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project. Add a new Razor Page by right clicking the Books folder then selecting **Add > Razor Page** menu item. Name it as `Index`: + +![bookstore-add-index-page](./images/bookstore-add-index-page-v2.png) + +Open the `Index.cshtml` and change the whole content as shown below: + +**Index.cshtml:** + +````html +@page +@using Acme.BookStore.Web.Pages.Books +@inherits Acme.BookStore.Web.Pages.BookStorePage +@model IndexModel + +

Books

+```` + +* This code changes the default inheritance of the Razor View Page Model so it **inherits** from the `BookStorePage` class (instead of `PageModel`). The `BookStorePage` class which comes with the startup template, provides some shared properties/methods used by all pages. + +* Set the `IndexModel`'s namespace to `Acme.BookStore.Pages.Books` in `Index.cshtml.cs`. + + + +**Index.cshtml.cs:** + +```csharp +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class IndexModel : PageModel + { + public void OnGet() + { + + } + } +} +``` + +#### Add books page to the main menu + +Open the `BookStoreMenuContributor` class in the `Menus` folder and add the following code to the end of the `ConfigureMainMenuAsync` method: + +````csharp +//... +namespace Acme.BookStore.Web.Menus +{ + public class BookStoreMenuContributor : IMenuContributor + { + private async Task ConfigureMainMenuAsync(MenuConfigurationContext context) + { + //<-- added the below code + context.Menu.AddItem( + new ApplicationMenuItem("BooksStore", l["Menu:BookStore"]) + .AddItem( + new ApplicationMenuItem("BooksStore.Books", l["Menu:Books"], url: "/Books") + ) + ); + //--> + } + } +} +```` + +{{end}} + +#### Localize the menu items + +Localization texts are located under the `Localization/BookStore` folder of the `Acme.BookStore.Domain.Shared` project: + +![bookstore-localization-files](./images/bookstore-localization-files-v2.png) + +Open the `en.json` (*English translations*) file and add the below localization texts to the end of the file: + +````json +{ + "Culture": "en", + "Texts": { + "Menu:Home": "Home", + "Welcome": "Welcome", + "LongWelcomeMessage": "Welcome to the application. This is a startup project based on the ABP framework. For more information, visit abp.io.", + + "Menu:BookStore": "Book Store", + "Menu:Books": "Books", + "Actions": "Actions", + "Edit": "Edit", + "PublishDate": "Publish date", + "NewBook": "New book", + "Name": "Name", + "Type": "Type", + "Price": "Price", + "CreationTime": "Creation time", + "AreYouSureToDelete": "Are you sure you want to delete this item?" + } +} +```` + +* ABP's localization system is built on [ASP.NET Core's standard localization](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization) system and extends it in many ways. See the [localization document](https://docs.abp.io/en/abp/latest/Localization) for details. +* Localization key names are arbitrary. You can set any name. As a best practice, we prefer to add `Menu:` prefix for menu items to distinguish from other texts. If a text is not defined in the localization file, it **fallbacks** to the localization key (as ASP.NET Core's standard behavior). + +{{if UI == "MVC"}} + +Run the project, login to the application with the username `admin` and password `1q2w3E*` and see the new menu item has been added to the menu. + +![bookstore-menu-items](./images/bookstore-new-menu-item.png) + +When you click to the Books menu item under the Book Store parent, you are being redirected to the new Books page. + +#### Book list + +We will use the [Datatables.net](https://datatables.net/) jQuery plugin to show the book list. [Datatables](https://datatables.net/) can completely work via AJAX, it is fast, popular and provides a good user experience. [Datatables](https://datatables.net/) plugin is configured in the startup template, so you can directly use it in any page without including any style or script file to your page. + +##### Index.cshtml + +Change the `Pages/Books/Index.cshtml` as following: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@model Acme.BookStore.Web.Pages.Books.IndexModel +@section scripts +{ + +} + + +

@L["Books"]

+
+ + + + + @L["Name"] + @L["Type"] + @L["PublishDate"] + @L["Price"] + @L["CreationTime"] + + + + +
+```` + +* `abp-script` [tag helper](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro) is used to add external **scripts** to the page. It has many additional features compared to standard `script` tag. It handles **minification** and **versioning**. See the [bundling & minification document](https://docs.abp.io/en/abp/latest/AspNetCore/Bundling-Minification) for details. +* `abp-card` and `abp-table` are **tag helpers** for Twitter Bootstrap's [card component](http://getbootstrap.com/docs/4.1/components/card/). There are other useful tag helpers in ABP to easily use most of the [bootstrap](https://getbootstrap.com/) components. You can also use regular HTML tags instead of these tag helpers, but using tag helpers reduces HTML code and prevents errors by help the of IntelliSense and compile time type checking. Further information, see the [tag helpers](https://docs.abp.io/en/abp/latest/AspNetCore/Tag-Helpers/Index) document. +* You can **localize** the column names in the localization file as you did for the menu items above. + +##### Add a Script File + +Create `index.js` JavaScript file under the `Pages/Books/` folder: + +![bookstore-index-js-file](./images/bookstore-index-js-file-v2.png) + +`index.js` content is shown below: + +````js +$(function () { + var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({ + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); +}); +```` + +* `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to [Datatable](https://datatables.net/)'s format. +* `abp.libs.datatables.normalizeConfiguration` is another helper function. There's no requirement to use it, but it simplifies the [Datatables](https://datatables.net/) configuration by providing conventional values for missing options. +* `acme.bookStore.book.getList` is the function to get list of books (as described in [dynamic JavaScript proxies](#Dynamic JavaScript proxies)). +* See [Datatables documentation](https://datatables.net/manual/) for all configuration options. + +It's end of this part. The final UI of this work is shown as below: + +![Book list](./images/bookstore-book-list-2.png) + +{{end}} + +{{if UI == "NG"}} + +### Angular development +#### Create the books page + +It's time to create something visible and usable! There are some tools that we will use when developing ABP Angular frontend application: + +- [Angular CLI](https://angular.io/cli) will be used to create modules, components and services. +- [NGXS](https://ngxs.gitbook.io/ngxs/) will be used as the state management library. +- [Ng Bootstrap](https://ng-bootstrap.github.io/#/home) will be used as the UI component library. +- [Visual Studio Code](https://code.visualstudio.com/) will be used as the code editor (you can use your favorite editor). + +#### Install NPM packages + +Open a new command line interface (terminal window) and go to your `angular` folder and then run `yarn` command to install NPM packages: + +```bash +yarn +``` + +#### BooksModule + +Run the following command line to create a new module, named `BooksModule`: + +```bash +yarn ng generate module books --route books --module app.module +``` + +![Generating books module](./images/bookstore-creating-books-module-terminal.png) + +#### Routing + +Open the `app-routing.module.ts` file in `src\app` folder. Add the new `import` and replace `books` path as shown below + +```js +import { ApplicationLayoutComponent } from '@abp/ng.theme.basic'; //==> added this line to imports <== + +//...replaced original books path with the below +{ + path: 'books', + component: ApplicationLayoutComponent, + loadChildren: () => import('./books/books.module').then(m => m.BooksModule), + data: { + routes: { + name: '::Menu:Books', + iconClass: 'fas fa-book' + } as ABP.Route + }, +} +``` + +* The `ApplicationLayoutComponent` configuration sets the application layout to the new page. We added the `data` object. The `name` is the menu item name and the `iconClass` is the icon of the menu item. + +Run `yarn start` and wait for Angular to serve the application: + +```bash +yarn start +``` + +Open the browser and navigate to http://localhost:4200/books. You'll see a blank page saying "*books works!*". + +![initial-books-page](./images/bookstore-initial-books-page-with-layout.png) + +#### Book list component + +Replace the `books.component.html` in the `app\books` folder with the following content: + +```html + +``` + +Then run the command below on the terminal in the root folder to generate a new component, named book-list: + +```bash +yarn ng generate component books/book-list +``` + +![Creating books list](./images/bookstore-creating-book-list-terminal.png) + +Open `books.module.ts` file in the `app\books` folder and replace the content as below: + +```js +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BooksRoutingModule } from './books-routing.module'; +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; +import { SharedModule } from '../shared/shared.module'; //<== added this line ==> + +@NgModule({ + declarations: [BooksComponent, BookListComponent], + imports: [ + CommonModule, + BooksRoutingModule, + SharedModule, //<== added this line ==> + ] +}) +export class BooksModule { } +``` + +* We imported `SharedModule` and added to `imports` array. + +Open `books-routing.module.ts` file in the `app\books` folder and replace the content as below: + +```js +import { NgModule } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; //<== added this line ==> + +//<== replaced routes ==> +const routes: Routes = [ + { + path: '', + component: BooksComponent, + children: [{ path: '', component: BookListComponent }], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class BooksRoutingModule { } +``` + +* We imported `BookListComponent` and replaced `routes` const. + +We'll see **book-list works!** text on the books page: + +![Initial book list page](./images/bookstore-initial-book-list-page.png) + +#### Create BooksState + +Run the following command in the terminal to create a new state, named `BooksState`: + +![Initial book list page](./images/bookstore-generate-state-books.png) + +```bash +yarn ng generate ngxs-schematic:state books +``` + +* This command creates several new files and updates `app.modules.ts` file to import the `NgxsModule` with the new state. + +#### Get books data from backend + +Create data types to map the data from the backend (you can check Swagger UI or your backend API to see the data format). + +![BookDto properties](./images/bookstore-swagger-book-dto-properties.png) + +Open the `books.ts` file in the `app\store\models` folder and replace the content as below: + +```js +export namespace Books { + export interface State { + books: Response; + } + + export interface Response { + items: Book[]; + totalCount: number; + } + + export interface Book { + name: string; + type: BookType; + publishDate: string; + price: number; + lastModificationTime: string; + lastModifierId: string; + creationTime: string; + creatorId: string; + id: string; + } + + export enum BookType { + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry, + } +} +``` + +* Added `Book` interface that represents a book object and `BookType` enum which represents a book category. + +#### BooksService + +Create a new service, named `BooksService` to perform `HTTP` calls to the server: + +```bash +yarn ng generate service books/shared/books +``` + +![service-terminal-output](./images/bookstore-service-terminal-output.png) + +Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below: + +```js +import { Injectable } from '@angular/core'; +import { RestService } from '@abp/ng.core'; +import { Books } from '../../store/models'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class BooksService { + constructor(private restService: RestService) {} + + get(): Observable { + return this.restService.request({ + method: 'GET', + url: '/api/app/book' + }); + } +} +``` + +* We added the `get` method to get the list of books by performing an HTTP request to the related endpoint. + +Open the`books.actions.ts` file in `app\store\actions` folder and replace the content below: + +```js +export class GetBooks { + static readonly type = '[Books] Get'; +} +``` + +#### Implement BooksState + +Open the `books.state.ts` file in `app\store\states` folder and replace the content below: + +```js +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks } from '../actions/books.actions'; +import { Books } from '../models/books'; +import { BooksService } from '../../books/shared/books.service'; +import { tap } from 'rxjs/operators'; + +@State({ + name: 'BooksState', + defaults: { books: {} } as Books.State, +}) +export class BooksState { + @Selector() + static getBooks(state: Books.State) { + return state.books.items || []; + } + + constructor(private booksService: BooksService) {} + + @Action(GetBooks) + get(ctx: StateContext) { + return this.booksService.get().pipe( + tap(booksResponse => { + ctx.patchState({ + books: booksResponse, + }); + }), + ); + } +} +``` + +* We added the `GetBooks` action that retrieves the books data via `BooksService` and patches the state. +* `NGXS` requires to return the observable without subscribing it in the get function. + +#### BookListComponent + +Open the `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks } from '../../store/actions'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + loading = false; + + constructor(private store: Store) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } +} +``` + +* We added the `get` function that updates store to get the books. +* See the [Dispatching actions](https://ngxs.gitbook.io/ngxs/concepts/store#dispatching-actions) and [Select](https://ngxs.gitbook.io/ngxs/concepts/select) on the `NGXS` documentation for more information on these `NGXS` features. + +Open the `book-list.component.html` file in `app\books\book-list` folder and replace the content as below: + +```html +
+
+
+
+
+ {%{{{ "::Menu:Books" | abpLocalization }}}%} +
+
+
+
+
+
+ + + + + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + + +
+
+``` + +* We added HTML code of book list page. + +Now you can see the final result on your browser: + +![Book list final result](./images/bookstore-book-list.png) + +The file system structure of the project: + +![Book list final result](./images/bookstore-angular-file-tree.png) + +In this tutorial we have applied the rules of official [Angular Style Guide](https://angular.io/guide/styleguide#file-tree). + +{{end}} + +### Next Part + +See the [part 2](part-2.md) for creating, updating and deleting books. diff --git a/docs/en/Tutorials/Part-2.md b/docs/en/Tutorials/Part-2.md new file mode 100644 index 0000000000..8408cf6bfb --- /dev/null +++ b/docs/en/Tutorials/Part-2.md @@ -0,0 +1,1447 @@ +## ASP.NET Core {{UI_Value}} Tutorial - Part 2 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` + +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### About this tutorial + +This is the second part of the ASP.NET Core {{UI_Value}} tutorial series. All parts: + +* [Part I: Creating the project and book list page](part-1.md) +* **Part II: Creating, updating and deleting books (this tutorial)** +* [Part III: Integration tests](part-3.md) + +*You can also watch [this video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by an ABP community member, based on this tutorial.* + +{{if UI == "MVC"}} + +### Creating a new book + +In this section, you will learn how to create a new modal dialog form to create a new book. The modal dialog will look like in the below image: + +![bookstore-create-dialog](./images/bookstore-create-dialog-2.png) + +#### Create the modal form + +Create a new razor page, named `CreateModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project. + +![bookstore-add-create-dialog](./images/bookstore-add-create-dialog-v2.png) + +##### CreateModal.cshtml.cs + +Open the `CreateModal.cshtml.cs` file (`CreateModalModel` class) and replace with the following code: + +````C# +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class CreateModalModel : BookStorePageModel + { + [BindProperty] + public CreateUpdateBookDto Book { get; set; } + + private readonly IBookAppService _bookAppService; + + public CreateModalModel(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + public async Task OnPostAsync() + { + await _bookAppService.CreateAsync(Book); + return NoContent(); + } + } +} +```` + +* This class is derived from the `BookStorePageModel` instead of standard `PageModel`. `BookStorePageModel` inherits the `PageModel` and adds some common properties & methods that can be used in your page model classes. +* `[BindProperty]` attribute on the `Book` property binds post request data to this property. +* This class simply injects the `IBookAppService` in the constructor and calls the `CreateAsync` method in the `OnPostAsync` handler. + +##### CreateModal.cshtml + +Open the `CreateModal.cshtml` file and paste the code below: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model Acme.BookStore.Web.Pages.Books.CreateModalModel +@{ + Layout = null; +} + + + + + + + + + +```` + +* This modal uses `abp-dynamic-form` tag helper to automatically create the form from the model `CreateBookViewModel`. + * `abp-model` attribute indicates the model object where it's the `Book` property in this case. + * `data-ajaxForm` attribute sets the form to submit via AJAX, instead of a classic page post. + * `abp-form-content` tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the `abp-dynamic-form` tag, just like in this page). + +#### Add the "New book" button + +Open the `Pages/Books/Index.cshtml` and set the content of `abp-card-header` tag as below: + +````html + + + +

@L["Books"]

+
+ + + +
+
+```` + +This adds a new button called **New book** to the **top-right** of the table: + +![bookstore-new-book-button](./images/bookstore-new-book-button.png) + +Open the `pages/books/index.js` and add the following code just after the `Datatable` configuration: + +````js +var createModal = new abp.ModalManager(abp.appPath + 'Books/CreateModal'); + +createModal.onResult(function () { + dataTable.ajax.reload(); +}); + +$('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); +}); +```` + +* `abp.ModalManager` is a helper class to manage modals in the client side. It internally uses Twitter Bootstrap's standard modal, but abstracts many details by providing a simple API. + +Now, you can **run the application** and add new books using the new modal form. + +### Updating a book + +Create a new razor page, named `EditModal.cshtml` under the `Pages/Books` folder of the `Acme.BookStore.Web` project: + +![bookstore-add-edit-dialog](./images/bookstore-add-edit-dialog.png) + +#### EditModal.cshtml.cs + +Open the `EditModal.cshtml.cs` file (`EditModalModel` class) and replace with the following code: + +````csharp +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Acme.BookStore.Web.Pages.Books +{ + public class EditModalModel : BookStorePageModel + { + [HiddenInput] + [BindProperty(SupportsGet = true)] + public Guid Id { get; set; } + + [BindProperty] + public CreateUpdateBookDto 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); + } + + public async Task OnPostAsync() + { + await _bookAppService.UpdateAsync(Id, Book); + return NoContent(); + } + } +} +```` + +* `[HiddenInput]` and `[BindProperty]` are standard ASP.NET Core MVC attributes. `SupportsGet` is used to be able to get `Id` value from query string parameter of the request. +* In the `GetAsync` method, we get `BookDto `from `BookAppService` and this is being mapped to the DTO object `CreateUpdateBookDto`. +* The `OnPostAsync` uses `BookAppService.UpdateAsync()` to update the entity. + +#### Mapping from BookDto to CreateUpdateBookDto + +To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mapping. To do this, open the `BookStoreWebAutoMapperProfile.cs` in the `Acme.BookStore.Web` project and change it as shown below: + +````csharp +using AutoMapper; + +namespace Acme.BookStore.Web +{ + public class BookStoreWebAutoMapperProfile : Profile + { + public BookStoreWebAutoMapperProfile() + { + CreateMap(); + } + } +} +```` + +* We have just added `CreateMap();` to define this mapping. + +#### EditModal.cshtml + +Replace `EditModal.cshtml` content with the following content: + +````html +@page +@inherits Acme.BookStore.Web.Pages.BookStorePage +@using Acme.BookStore.Web.Pages.Books +@using Volo.Abp.AspNetCore.Mvc.UI.Bootstrap.TagHelpers.Modal +@model EditModalModel +@{ + Layout = null; +} + + + + + + + + + + +```` + +This page is very similar to the `CreateModal.cshtml`, except: + +* It includes an `abp-input` for the `Id` property to store `Id` of the editing book (which is a hidden input). +* It uses `Books/EditModal` as the post URL and *Update* text as the modal header. + +#### Add "Actions" dropdown to the table + +We will add a dropdown button to the table named *Actions*. + +Open the `Pages/Books/Index.cshtml` page and change the `` section as shown below: + +````html + + + + @L["Actions"] + @L["Name"] + @L["Type"] + @L["PublishDate"] + @L["Price"] + @L["CreationTime"] + + + +```` + +* We just added a new `th` tag for the "*Actions*" button. + +Open the `pages/books/index.js` and replace the content as below: + +````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({ + processing: true, + serverSide: true, + paging: true, + searching: false, + autoWidth: false, + scrollCollapse: true, + order: [[1, "asc"]], + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + } + ] + } + }, + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +* Used `abp.localization.getResource('BookStore')` to be able to use the same localization texts defined on the server-side. +* Added a new `ModalManager` named `createModal` to open the create modal dialog. +* Added a new `ModalManager` named `editModal` to open the edit modal dialog. +* Added a new column at the beginning of the `columnDefs` section. This column is used for the "*Actions*" dropdown button. +* "*New Book*" action simply calls `createModal.open()` to open the create dialog. +* "*Edit*" action simply calls `editModal.open()` to open the edit dialog. + +You can run the application and edit any book by selecting the edit action. The final UI looks as below: + +![bookstore-books-table-actions](./images/bookstore-edit-button.png) + +### Deleting a book + +Open the `pages/books/index.js` and add a new item to the `rowAction` `items`: + +````js +{ + text: l('Delete'), + confirmMessage: function (data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.bookStore.book + .delete(data.record.id) + .then(function() { + abp.notify.info(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } +} +```` + +* `confirmMessage` option is used to ask a confirmation question before executing the `action`. +* `acme.bookStore.book.delete()` method makes an AJAX request to JavaScript proxy function to delete a book. +* `abp.notify.info()` shows a notification after the delete operation. + +The final `index.js` content is shown below: + +````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({ + processing: true, + serverSide: true, + paging: true, + searching: false, + autoWidth: false, + scrollCollapse: true, + order: [[1, "asc"]], + ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList), + columnDefs: [ + { + rowAction: { + items: + [ + { + text: l('Edit'), + action: function (data) { + editModal.open({ id: data.record.id }); + } + }, + { + text: l('Delete'), + confirmMessage: function (data) { + return l('BookDeletionConfirmationMessage', data.record.name); + }, + action: function (data) { + acme.bookStore.book + .delete(data.record.id) + .then(function() { + abp.notify.info(l('SuccessfullyDeleted')); + dataTable.ajax.reload(); + }); + } + } + ] + } + }, + { data: "name" }, + { data: "type" }, + { data: "publishDate" }, + { data: "price" }, + { data: "creationTime" } + ] + })); + + createModal.onResult(function () { + dataTable.ajax.reload(); + }); + + editModal.onResult(function () { + dataTable.ajax.reload(); + }); + + $('#NewBookButton').click(function (e) { + e.preventDefault(); + createModal.open(); + }); +}); +```` + +Open the `en.json` in the `Acme.BookStore.Domain.Shared` project and add the following translations: + +````json +"BookDeletionConfirmationMessage": "Are you sure to delete the book {0}?", +"SuccessfullyDeleted": "Successfully deleted" +```` + +Run the application and try to delete a book. + +{{end}} + +{{if UI == "NG"}} + +### Creating a new book + +In this section, you will learn how to create a new modal dialog form to create a new book. + +#### Type definition + +Open `books.ts` file in `app\store\models` folder and replace the content as below: + +```js +export namespace Books { + export interface State { + books: Response; + } + + export interface Response { + items: Book[]; + totalCount: number; + } + + export interface Book { + name: string; + type: BookType; + publishDate: string; + price: number; + lastModificationTime: string; + lastModifierId: string; + creationTime: string; + creatorId: string; + id: string; + } + + export enum BookType { + Undefined, + Adventure, + Biography, + Dystopia, + Fantastic, + Horror, + Science, + ScienceFiction, + Poetry, + } + + //<== added CreateUpdateBookInput interface ==> + export interface CreateUpdateBookInput { + name: string; + type: BookType; + publishDate: string; + price: number; + } +} +``` + +* We added `CreateUpdateBookInput` interface. +* You can see the properties of this interface from Swagger UI. +* The `CreateUpdateBookInput` interface matches with the `CreateUpdateBookDto` in the backend. + +#### Service method + +Open the `books.service.ts` file in `app\books\shared` folder and replace the content as below: + +```js +import { Injectable } from '@angular/core'; +import { RestService } from '@abp/ng.core'; +import { Books } from '../../store/models'; +import { Observable } from 'rxjs'; + +@Injectable({ + providedIn: 'root', +}) +export class BooksService { + constructor(private restService: RestService) {} + + get(): Observable { + return this.restService.request({ + method: 'GET', + url: '/api/app/book' + }); + } + + //<== added create method ==> + create(createBookInput: Books.CreateUpdateBookInput): Observable { + return this.restService.request({ + method: 'POST', + url: '/api/app/book', + body: createBookInput + }); + } +} +``` + +- We added the `create` method to perform an HTTP Post request to the server. +- `restService.request` function gets generic parameters for the types sent to and received from the server. This example sends a `CreateUpdateBookInput` object and receives a `Book` object (you can set `void` for request or return type if not used). + +#### State definitions + +Open `books.action.ts` in `app\store\actions` folder and replace the content as below: + +```js +import { Books } from '../models'; //<== added this line ==> + +export class GetBooks { + static readonly type = '[Books] Get'; +} + +//added CreateUpdateBook class +export class CreateUpdateBook { + static readonly type = '[Books] Create Update Book'; + constructor(public payload: Books.CreateUpdateBookInput) { } +} +``` + +* We imported the Books namespace and created the `CreateUpdateBook` action. + +Open `books.state.ts` file in `app\store\states` and replace the content as below: + +```js +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks, CreateUpdateBook } from '../actions/books.actions'; //<== added CreateUpdateBook==> +import { Books } from '../models/books'; +import { BooksService } from '../../books/shared/books.service'; +import { tap } from 'rxjs/operators'; + +@State({ + name: 'BooksState', + defaults: { books: {} } as Books.State, +}) +export class BooksState { + @Selector() + static getBooks(state: Books.State) { + return state.books.items || []; + } + + constructor(private booksService: BooksService) { } + + @Action(GetBooks) + get(ctx: StateContext) { + return this.booksService.get().pipe( + tap(booksResponse => { + ctx.patchState({ + books: booksResponse, + }); + }), + ); + } + + //added CreateUpdateBook action listener + @Action(CreateUpdateBook) + save(ctx: StateContext, action: CreateUpdateBook) { + return this.booksService.create(action.payload); + } +} +``` + +* We imported `CreateUpdateBook` action and defined the `save` method that will listen to a `CreateUpdateBook` action to create a book. + +When the `SaveBook` action dispatched, the save method is being executed. It calls `create` method of the `BooksService`. + +#### Add a modal to BookListComponent + +Open `book-list.component.html` file in `books\book-list` folder and replace the content as below: + +```html +
+
+
+
+
+ {%{{{ '::Menu:Books' | abpLocalization }}}%} +
+
+ +
+
+ +
+
+
+
+
+ + + + + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + + +
+
+ + + + +

{%{{{ '::NewBook' | abpLocalization }}}%}

+
+ + + + + + +
+``` + +* We added the `abp-modal` which renders a modal to allow user to create a new book. +* `abp-modal` is a pre-built component to show modals. While you could use another approach to show a modal, `abp-modal` provides additional benefits. +* We added `New book` button to the `AbpContentToolbar`. + +Open `book-list.component.` file in `books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks } from '../../store/actions'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + loading = false; + + isModalOpen = false; //<== added this line ==> + + constructor(private store: Store) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + //added createBook method + createBook() { + this.isModalOpen = true; + } +} +``` + +* We added `isModalOpen = false` and `createBook` method. + +You can open your browser and click **New book** button to see the new modal. + +![Empty modal for new book](./images/bookstore-empty-new-book-modal.png) + +#### Create a reactive form + +[Reactive forms](https://angular.io/guide/reactive-forms) provide a model-driven approach to handling form inputs whose values change over time. + +Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks } from '../../store/actions'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; //<== added this line ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + constructor(private store: Store, private fb: FormBuilder) { } //<== added FormBuilder ==> + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + createBook() { + this.buildForm(); //<== added this line ==> + this.isModalOpen = true; + } + + //added buildForm method + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } +} +``` + +* We imported `FormGroup, FormBuilder and Validators`. +* We injected `fb: FormBuilder` service to the constructor. The [FormBuilder](https://angular.io/api/forms/FormBuilder) service provides convenient methods for generating controls. It reduces the amount of boilerplate needed to build complex forms. +* We added `buildForm` method to the end of the file and executed `buildForm()` in the `createBook` method. This method creates a reactive form to be able to create a new book. + * The `group` method of `FormBuilder`, `fb` creates a `FormGroup`. + * Added `Validators.required` static method which validates the relevant form element. + +#### Create the DOM elements of the form + +Open `book-list.component.html` in `app\books\book-list` folder and replace ` ` with the following code part: + +```html + +
+
+ * + +
+ +
+ * + +
+ +
+ * + +
+ +
+ * + +
+
+
+``` + +- This template creates a form with `Name`, `Price`, `Type` and `Publish` date fields. +- We've used [NgBootstrap datepicker](https://ng-bootstrap.github.io/#/components/datepicker/overview) in this component. + +#### Datepicker requirements + +Open `books.module.ts` file in `app\books` folder and replace the content as below: + +```js +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { BooksRoutingModule } from './books-routing.module'; +import { BooksComponent } from './books.component'; +import { BookListComponent } from './book-list/book-list.component'; +import { SharedModule } from '../shared/shared.module'; +import { NgbDatepickerModule } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==> + +@NgModule({ + declarations: [BooksComponent, BookListComponent], + imports: [ + CommonModule, + BooksRoutingModule, + SharedModule, + NgbDatepickerModule //<== added this line ==> + ] +}) +export class BooksModule { } +``` + +* We imported `NgbDatepickerModule` to be able to use the date picker. + + + +Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks } from '../../store/actions'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; //<== added this line ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] //<== added this line ==> +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + //added bookTypeArr array + bookTypeArr = Object.keys(Books.BookType).filter( + bookType => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + constructor(private store: Store, private fb: FormBuilder) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + createBook() { + this.buildForm(); + this.isModalOpen = true; + } + + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } +} +``` + +* We imported ` NgbDateNativeAdapter, NgbDateAdapter` + +* We added a new provider `NgbDateAdapter` that converts Datepicker value to `Date` type. See the [datepicker adapters](https://ng-bootstrap.github.io/#/components/datepicker/overview) for more details. + +* We added `bookTypeArr` array to be able to use it in the combobox values. The `bookTypeArr` contains the fields of the `BookType` enum. Resulting array is shown below: + + ```js + ['Adventure', 'Biography', 'Dystopia', 'Fantastic' ...] + ``` + + This array was used in the previous form template in the `ngFor` loop. + +Now, you can open your browser to see the changes: + + +![New book modal](./images/bookstore-new-book-form.png) + +#### Saving the book + +Open `book-list.component.html` in `app\books\book-list` folder and add the following `abp-button` to save the new book. + +```html + + + + + + +``` + +* This adds a save button to the bottom area of the modal: + +![Save button to the modal](./images/bookstore-new-book-form-v2.png) + +Open `book-list.component.ts` file in `app\books\book-list` folder and replace the content as below: + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks, CreateUpdateBook } from '../../store/actions'; //<== added CreateUpdateBook ==> +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + bookTypeArr = Object.keys(Books.BookType).filter( + bookType => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + constructor(private store: Store, private fb: FormBuilder) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + createBook() { + this.buildForm(); + this.isModalOpen = true; + } + + buildForm() { + this.form = this.fb.group({ + name: ['', Validators.required], + type: [null, Validators.required], + publishDate: [null, Validators.required], + price: [null, Validators.required], + }); + } + + //<== added save ==> + save() { + if (this.form.invalid) { + return; + } + + this.store.dispatch(new CreateUpdateBook(this.form.value)).subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.get(); + }); + } +} +``` + +* We imported `CreateUpdateBook`. +* We added `save` method + +### Updating an existing book + +#### BooksService + +Open the `books.service.ts` in `app\books\shared` folder and add the `getById` and `update` methods. + +```js +getById(id: string): Observable { + return this.restService.request({ + method: 'GET', + url: `/api/app/book/${id}` + }); +} + +update(updateBookInput: Books.CreateUpdateBookInput, id: string): Observable { + return this.restService.request({ + method: 'PUT', + url: `/api/app/book/${id}`, + body: updateBookInput + }); +} +``` + +#### CreateUpdateBook action + +Open the `books.actions.ts` in `app\store\actions` folder and replace the content as below: + +```js +import { Books } from '../models'; + +export class GetBooks { + static readonly type = '[Books] Get'; +} + +export class CreateUpdateBook { + static readonly type = '[Books] Create Update Book'; + constructor(public payload: Books.CreateUpdateBookInput, public id?: string) { } //<== added id parameter ==> +} +``` + +* We added `id` parameter to the `CreateUpdateBook` action's constructor. + +Open the `books.state.ts` in `app\store\states` folder and replace the `save` method as below: + +```js +@Action(CreateUpdateBook) +save(ctx: StateContext, action: CreateUpdateBook) { + if (action.id) { + return this.booksService.update(action.payload, action.id); + } else { + return this.booksService.create(action.payload); + } +} +``` + +#### BookListComponent + +Open `book-list.component.ts` in `app\books\book-list` folder and inject `BooksService` dependency by adding it to the constructor and add a variable named `selectedBook`. + +```js +import { Component, OnInit } from '@angular/core'; +import { Store, Select } from '@ngxs/store'; +import { BooksState } from '../../store/states'; +import { Observable } from 'rxjs'; +import { Books } from '../../store/models'; +import { GetBooks, CreateUpdateBook } from '../../store/actions'; +import { FormGroup, FormBuilder, Validators } from '@angular/forms'; +import { NgbDateNativeAdapter, NgbDateAdapter } from '@ng-bootstrap/ng-bootstrap'; +import { BooksService } from '../shared/books.service'; //<== imported BooksService ==> + +@Component({ + selector: 'app-book-list', + templateUrl: './book-list.component.html', + styleUrls: ['./book-list.component.scss'], + providers: [{ provide: NgbDateAdapter, useClass: NgbDateNativeAdapter }] +}) +export class BookListComponent implements OnInit { + @Select(BooksState.getBooks) + books$: Observable; + + booksType = Books.BookType; + + bookTypeArr = Object.keys(Books.BookType).filter( + bookType => typeof this.booksType[bookType] === 'number' + ); + + loading = false; + + isModalOpen = false; + + form: FormGroup; + + selectedBook = {} as Books.Book; //<== declared selectedBook ==> + + constructor(private store: Store, private fb: FormBuilder, private booksService: BooksService) { } + + ngOnInit() { + this.get(); + } + + get() { + this.loading = true; + this.store.dispatch(new GetBooks()).subscribe(() => { + this.loading = false; + }); + } + + //<== this method is replaced ==> + createBook() { + this.selectedBook = {} as Books.Book; //<== added ==> + this.buildForm(); + this.isModalOpen = true; + } + + //<== added editBook method ==> + editBook(id: string) { + this.booksService.getById(id).subscribe(book => { + this.selectedBook = book; + this.buildForm(); + this.isModalOpen = true; + }); + } + + //<== this method is replaced ==> + buildForm() { + this.form = this.fb.group({ + name: [this.selectedBook.name || "", Validators.required], + type: [this.selectedBook.type || null, Validators.required], + publishDate: [ + this.selectedBook.publishDate + ? new Date(this.selectedBook.publishDate) + : null, + Validators.required + ], + price: [this.selectedBook.price || null, Validators.required] + }); + } + + save() { + if (this.form.invalid) { + return; + } + + //<== added this.selectedBook.id ==> + this.store.dispatch(new CreateUpdateBook(this.form.value, this.selectedBook.id)) + .subscribe(() => { + this.isModalOpen = false; + this.form.reset(); + this.get(); + }); + } +} +``` + +* We imported `BooksService`. +* We declared a variable named `selectedBook` as `Books.Book`. +* We injected `BooksService` to the constructor. `BooksService` is being used to retrieve the book data which is being edited. +* We added `editBook` method. This method fetches the book with the given `Id` and sets it to `selectedBook` object. +* We replaced the `buildForm` method so that it creates the form with the `selectedBook` data. +* We replaced the `createBook` method so it sets `selectedBook` to an empty object. +* We added `selectedBook.id` to the constructor of the new `CreateUpdateBook`. + +#### Add "Actions" dropdown to the table + +Open the `book-list.component.html` in `app\books\book-list` folder and replace the `
` tag as below: + +```html +
+ + + + + {%{{{ "::Actions" | abpLocalization }}}%} + {%{{{ "::Name" | abpLocalization }}}%} + {%{{{ "::Type" | abpLocalization }}}%} + {%{{{ "::PublishDate" | abpLocalization }}}%} + {%{{{ "::Price" | abpLocalization }}}%} + + + + + +
+ +
+ +
+
+ + {%{{{ data.name }}}%} + {%{{{ booksType[data.type] }}}%} + {%{{{ data.publishDate | date }}}%} + {%{{{ data.price }}}%} + +
+
+``` + +- We added a `th` for the "Actions" column. +- We added `button` with `ngbDropdownToggle` to open actions when clicked the button. +- We have used to [NgbDropdown](https://ng-bootstrap.github.io/#/components/dropdown/examples) for the dropdown menu of actions. + +The final UI looks like as below: + +![Action buttons](./images/bookstore-actions-buttons.png) + +Open `book-list.component.html` in `app\books\book-list` folder and find the `` tag and replace the content as below. + +```html + +

{%{{{ (selectedBook.id ? 'AbpIdentity::Edit' : '::NewBook' ) | abpLocalization }}}%}

+
+``` + +* This template will show **Edit** text for edit record operation, **New Book** for new record operation in the title. + +### Deleting a book + +#### BooksService + +Open `books.service.ts` in `app\books\shared` folder and add the below `delete` method to delete a book. + +```js +delete(id: string): Observable { + return this.restService.request({ + method: 'DELETE', + url: `/api/app/book/${id}` + }); +} +``` + +* `Delete` method gets `id` parameter and makes a `DELETE` HTTP request to the relevant endpoint. + +#### DeleteBook action + +Open `books.actions.ts` in `app\store\actions `folder and add an action named `DeleteBook`. + +```js +export class DeleteBook { + static readonly type = '[Books] Delete'; + constructor(public id: string) {} +} +``` + +Open the `books.state.ts` in `app\store\states` folder and replace the content as below: + +```js +import { State, Action, StateContext, Selector } from '@ngxs/store'; +import { GetBooks, CreateUpdateBook, DeleteBook } from '../actions/books.actions'; //<== added DeleteBook==> +import { Books } from '../models/books'; +import { BooksService } from '../../books/shared/books.service'; +import { tap } from 'rxjs/operators'; + +@State({ + name: 'BooksState', + defaults: { books: {} } as Books.State, +}) +export class BooksState { + @Selector() + static getBooks(state: Books.State) { + return state.books.items || []; + } + + constructor(private booksService: BooksService) { } + + @Action(GetBooks) + get(ctx: StateContext) { + return this.booksService.get().pipe( + tap(booksResponse => { + ctx.patchState({ + books: booksResponse, + }); + }), + ); + } + + @Action(CreateUpdateBook) + save(ctx: StateContext, action: CreateUpdateBook) { + if (action.id) { + return this.booksService.update(action.payload, action.id); + } else { + return this.booksService.create(action.payload); + } + } + + //<== added DeleteBook ==> + @Action(DeleteBook) + delete(ctx: StateContext, action: DeleteBook) { + return this.booksService.delete(action.id); + } +} +``` + +- We imported `DeleteBook` . + +- We added `DeleteBook` action listener to the end of the file. + + + +#### Add a delete button + + +Open `book-list.component.html` in `app\books\book-list` folder and modify the `ngbDropdownMenu` to add the delete button as shown below: + +```html +
+ + +
+``` + +The final actions dropdown UI looks like below: + +![bookstore-final-actions-dropdown](./images/bookstore-final-actions-dropdown.png) + +#### Delete confirmation dialog + +Open `book-list.component.ts` in`app\books\book-list` folder and inject the `ConfirmationService`. + +Replace the constructor as below: + +```js +import { ConfirmationService } from '@abp/ng.theme.shared'; +//... + +constructor( + private store: Store, private fb: FormBuilder, + private booksService: BooksService, + private confirmationService: ConfirmationService // <== added this line ==> +) { } +``` + +* We imported `ConfirmationService`. +* We injected `ConfirmationService` to the constructor. + +In the `book-list.component.ts` add a delete method : + +```js +import { GetBooks, CreateUpdateBook, DeleteBook } from '../../store/actions'; //<== added DeleteBook ==> + +import { ConfirmationService, Confirmation } from '@abp/ng.theme.shared'; //<== added Confirmation ==> + +//... + +delete(id: string, name: string) { + this.confirmationService + .warn('::AreYouSureToDelete', 'AbpAccount::AreYouSure') + .subscribe(status => { + if (status === Confirmation.Status.confirm) { + this.store.dispatch(new DeleteBook(id)).subscribe(() => this.get()); + } + }); +} +``` + +The `delete` method shows a confirmation popup and subscribes for the user response. `DeleteBook` action dispatched only if user clicks to the `Yes` button. The confirmation popup looks like below: + +![bookstore-confirmation-popup](./images/bookstore-confirmation-popup.png) + +{{end}} + +### Next Part + +See the [next part](part-3.md) of this tutorial. diff --git a/docs/en/Tutorials/images/bookstore-actions-buttons.png b/docs/en/Tutorials/images/bookstore-actions-buttons.png new file mode 100644 index 0000000000..e8243fedc7 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-actions-buttons.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png b/docs/en/Tutorials/images/bookstore-add-create-dialog-v2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-create-dialog-v2.png rename to docs/en/Tutorials/images/bookstore-add-create-dialog-v2.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png b/docs/en/Tutorials/images/bookstore-add-edit-dialog.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-edit-dialog.png rename to docs/en/Tutorials/images/bookstore-add-edit-dialog.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png b/docs/en/Tutorials/images/bookstore-add-index-page-v2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-add-index-page-v2.png rename to docs/en/Tutorials/images/bookstore-add-index-page-v2.png diff --git a/docs/en/Tutorials/images/bookstore-angular-file-tree.png b/docs/en/Tutorials/images/bookstore-angular-file-tree.png new file mode 100644 index 0000000000..28e570f604 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-angular-file-tree.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png b/docs/en/Tutorials/images/bookstore-appservice-tests.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-appservice-tests.png rename to docs/en/Tutorials/images/bookstore-appservice-tests.png diff --git a/docs/en/Tutorials/Angular/images/bookstore-backend-solution-v2.png b/docs/en/Tutorials/images/bookstore-backend-solution-v2.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-backend-solution-v2.png rename to docs/en/Tutorials/images/bookstore-backend-solution-v2.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png b/docs/en/Tutorials/images/bookstore-book-list-2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-book-list-2.png rename to docs/en/Tutorials/images/bookstore-book-list-2.png diff --git a/docs/en/Tutorials/images/bookstore-book-list.png b/docs/en/Tutorials/images/bookstore-book-list.png new file mode 100644 index 0000000000..9e6cc9e010 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-book-list.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png b/docs/en/Tutorials/images/bookstore-books-table-actions.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table-actions.png rename to docs/en/Tutorials/images/bookstore-books-table-actions.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png b/docs/en/Tutorials/images/bookstore-books-table.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-books-table.png rename to docs/en/Tutorials/images/bookstore-books-table.png diff --git a/docs/en/Tutorials/Angular/images/bookstore-confirmation-popup.png b/docs/en/Tutorials/images/bookstore-confirmation-popup.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-confirmation-popup.png rename to docs/en/Tutorials/images/bookstore-confirmation-popup.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png b/docs/en/Tutorials/images/bookstore-create-dialog-2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog-2.png rename to docs/en/Tutorials/images/bookstore-create-dialog-2.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png b/docs/en/Tutorials/images/bookstore-create-dialog.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-dialog.png rename to docs/en/Tutorials/images/bookstore-create-dialog.png diff --git a/docs/en/Tutorials/images/bookstore-create-project-angular.png b/docs/en/Tutorials/images/bookstore-create-project-angular.png new file mode 100644 index 0000000000..b9eb38b8b7 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-create-project-angular.png differ diff --git a/docs/en/Tutorials/images/bookstore-create-project-mvc.png b/docs/en/Tutorials/images/bookstore-create-project-mvc.png new file mode 100644 index 0000000000..f453b20279 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-create-project-mvc.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png b/docs/en/Tutorials/images/bookstore-create-template.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-create-template.png rename to docs/en/Tutorials/images/bookstore-create-template.png diff --git a/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png new file mode 100644 index 0000000000..6f19dcc7bf Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-creating-book-list-terminal.png differ diff --git a/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png b/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png new file mode 100644 index 0000000000..ec9ef4c42f Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-creating-books-module-terminal.png differ diff --git a/docs/en/Tutorials/images/bookstore-database-tables-ef.png b/docs/en/Tutorials/images/bookstore-database-tables-ef.png new file mode 100644 index 0000000000..857b10de5b Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-database-tables-ef.png differ diff --git a/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png b/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png new file mode 100644 index 0000000000..8d78bd9a54 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-database-tables-mongodb.png differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-edit-modal.png b/docs/en/Tutorials/images/bookstore-edit-modal.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-edit-modal.png rename to docs/en/Tutorials/images/bookstore-edit-modal.png diff --git a/docs/en/Tutorials/Angular/images/bookstore-empty-new-book-modal.png b/docs/en/Tutorials/images/bookstore-empty-new-book-modal.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-empty-new-book-modal.png rename to docs/en/Tutorials/images/bookstore-empty-new-book-modal.png diff --git a/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png b/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png new file mode 100644 index 0000000000..4f41829f0d Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-final-actions-dropdown.png differ diff --git a/docs/en/Tutorials/images/bookstore-generate-state-books.png b/docs/en/Tutorials/images/bookstore-generate-state-books.png new file mode 100644 index 0000000000..be7a919017 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-generate-state-books.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png b/docs/en/Tutorials/images/bookstore-homepage.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-homepage.png rename to docs/en/Tutorials/images/bookstore-homepage.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png b/docs/en/Tutorials/images/bookstore-index-js-file-v2.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-index-js-file-v2.png rename to docs/en/Tutorials/images/bookstore-index-js-file-v2.png diff --git a/docs/en/Tutorials/images/bookstore-initial-book-list-page.png b/docs/en/Tutorials/images/bookstore-initial-book-list-page.png new file mode 100644 index 0000000000..591cffb121 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-initial-book-list-page.png differ diff --git a/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png b/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png new file mode 100644 index 0000000000..629ad46444 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-initial-books-page-with-layout.png differ diff --git a/docs/en/Tutorials/images/bookstore-localization-files-v2.png b/docs/en/Tutorials/images/bookstore-localization-files-v2.png new file mode 100644 index 0000000000..542cda209c Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-localization-files-v2.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png b/docs/en/Tutorials/images/bookstore-menu-items.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-menu-items.png rename to docs/en/Tutorials/images/bookstore-menu-items.png diff --git a/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png b/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png new file mode 100644 index 0000000000..0724e4ae8f Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-migrations-applied-angular.png differ diff --git a/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png b/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png new file mode 100644 index 0000000000..d59c0ce1d3 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-migrations-applied-mvc.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png b/docs/en/Tutorials/images/bookstore-new-book-button.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-new-book-button.png rename to docs/en/Tutorials/images/bookstore-new-book-button.png diff --git a/docs/en/Tutorials/Angular/images/bookstore-new-book-form-v2.png b/docs/en/Tutorials/images/bookstore-new-book-form-v2.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-new-book-form-v2.png rename to docs/en/Tutorials/images/bookstore-new-book-form-v2.png diff --git a/docs/en/Tutorials/Angular/images/bookstore-new-book-form.png b/docs/en/Tutorials/images/bookstore-new-book-form.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-new-book-form.png rename to docs/en/Tutorials/images/bookstore-new-book-form.png diff --git a/docs/en/Tutorials/images/bookstore-new-menu-item.png b/docs/en/Tutorials/images/bookstore-new-menu-item.png new file mode 100644 index 0000000000..97bf7fc7c1 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-new-menu-item.png differ diff --git a/docs/en/Tutorials/images/bookstore-open-package-manager-console.png b/docs/en/Tutorials/images/bookstore-open-package-manager-console.png new file mode 100644 index 0000000000..a640eb2681 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-open-package-manager-console.png differ diff --git a/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png new file mode 100644 index 0000000000..2baea20236 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration-v2.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png b/docs/en/Tutorials/images/bookstore-pmc-add-book-migration.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-pmc-add-book-migration.png rename to docs/en/Tutorials/images/bookstore-pmc-add-book-migration.png diff --git a/docs/en/Tutorials/images/bookstore-service-terminal-output.png b/docs/en/Tutorials/images/bookstore-service-terminal-output.png new file mode 100644 index 0000000000..cf6145e03f Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-service-terminal-output.png differ diff --git a/docs/en/Tutorials/images/bookstore-solution-structure-angular.png b/docs/en/Tutorials/images/bookstore-solution-structure-angular.png new file mode 100644 index 0000000000..07d064a880 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-solution-structure-angular.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png b/docs/en/Tutorials/images/bookstore-solution-structure-mvc.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-visual-studio-solution-v3.png rename to docs/en/Tutorials/images/bookstore-solution-structure-mvc.png diff --git a/docs/en/Tutorials/images/bookstore-start-project-angular.png b/docs/en/Tutorials/images/bookstore-start-project-angular.png new file mode 100644 index 0000000000..08abf845a8 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-start-project-angular.png differ diff --git a/docs/en/Tutorials/images/bookstore-start-project-mvc.png b/docs/en/Tutorials/images/bookstore-start-project-mvc.png new file mode 100644 index 0000000000..133dc6f131 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-start-project-mvc.png differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-swagger-api.png b/docs/en/Tutorials/images/bookstore-swagger-api.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-swagger-api.png rename to docs/en/Tutorials/images/bookstore-swagger-api.png diff --git a/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png b/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png new file mode 100644 index 0000000000..66d630bb56 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-swagger-book-dto-properties.png differ diff --git a/docs/en/Tutorials/images/bookstore-swagger.png b/docs/en/Tutorials/images/bookstore-swagger.png new file mode 100644 index 0000000000..3ce36a11bc Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-swagger.png differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-explorer.png b/docs/en/Tutorials/images/bookstore-test-explorer.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-test-explorer.png rename to docs/en/Tutorials/images/bookstore-test-explorer.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png b/docs/en/Tutorials/images/bookstore-test-js-proxy-getlist-network.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist-network.png rename to docs/en/Tutorials/images/bookstore-test-js-proxy-getlist-network.png diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png b/docs/en/Tutorials/images/bookstore-test-js-proxy-getlist.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-js-proxy-getlist.png rename to docs/en/Tutorials/images/bookstore-test-js-proxy-getlist.png diff --git a/docs/en/Tutorials/images/bookstore-test-projects-angular.png b/docs/en/Tutorials/images/bookstore-test-projects-angular.png new file mode 100644 index 0000000000..6a8947238e Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-test-projects-angular.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png b/docs/en/Tutorials/images/bookstore-test-projects-mvc.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-test-projects-v2.png rename to docs/en/Tutorials/images/bookstore-test-projects-mvc.png diff --git a/docs/en/Tutorials/images/bookstore-test-projects-v2.png b/docs/en/Tutorials/images/bookstore-test-projects-v2.png new file mode 100644 index 0000000000..8701164d75 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-test-projects-v2.png differ diff --git a/docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png b/docs/en/Tutorials/images/bookstore-test-projects-v3.png similarity index 100% rename from docs/en/Tutorials/Angular/images/bookstore-test-projects-v3.png rename to docs/en/Tutorials/images/bookstore-test-projects-v3.png diff --git a/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png b/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png new file mode 100644 index 0000000000..4889f4f757 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-update-database-after-book-entity.png differ diff --git a/docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png b/docs/en/Tutorials/images/bookstore-user-management.png similarity index 100% rename from docs/en/Tutorials/AspNetCore-Mvc/images/bookstore-user-management.png rename to docs/en/Tutorials/images/bookstore-user-management.png diff --git a/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png b/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png new file mode 100644 index 0000000000..307e3516a5 Binary files /dev/null and b/docs/en/Tutorials/images/bookstore-visual-studio-solution-v3.png differ diff --git a/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png b/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png new file mode 100644 index 0000000000..c9e2fc0e65 Binary files /dev/null and b/docs/en/Tutorials/images/mozilla-self-signed-cert-error.png differ diff --git a/docs/en/Tutorials/part-3.md b/docs/en/Tutorials/part-3.md new file mode 100644 index 0000000000..4cbefde69e --- /dev/null +++ b/docs/en/Tutorials/part-3.md @@ -0,0 +1,198 @@ +## ASP.NET Core {{UI_Value}} Tutorial - Part 3 +````json +//[doc-params] +{ + "UI": ["MVC","NG"] +} +```` + +{{ +if UI == "MVC" + DB="ef" + DB_Text="Entity Framework Core" + UI_Text="mvc" +else if UI == "NG" + DB="mongodb" + DB_Text="MongoDB" + UI_Text="angular" +else + DB ="?" + UI_Text="?" +end +}} + +### About this tutorial + +This is the third part of the ASP.NET Core {{UI_Value}} tutorial series. See all parts: + +- [Part I: Creating the project and book list page](part-1.md) +- [Part II: Creating, updating and deleting books](part-2.md) +- **Part III: Integration tests (this tutorial)** + +*You can also check out [the video course](https://amazingsolutions.teachable.com/p/lets-build-the-bookstore-application) prepared by the community, based on this tutorial.* + +### Test projects in the solution + +This part covers the **server side** tests. There are several test projects in the solution: + +![bookstore-test-projects-v2](./images/bookstore-test-projects-{{UI_Text}}.png) + +Each project is used to test the related project. Test projects use the following libraries for testing: + +* [Xunit](https://xunit.github.io/) as the main test framework. +* [Shoudly](http://shouldly.readthedocs.io/en/latest/) as the assertion library. +* [NSubstitute](http://nsubstitute.github.io/) as the mocking library. + +### Adding test data + +Startup template contains the `BookStoreTestDataBuilder` class in the `Acme.BookStore.TestBase` project which creates initial data to run tests. Change the content of `BookStoreTestDataSeedContributor` class as show below: + +````csharp +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Domain.Repositories; +using Volo.Abp.Guids; + +namespace Acme.BookStore +{ + public class BookStoreTestDataSeedContributor + : IDataSeedContributor, ITransientDependency + { + private readonly IRepository _bookRepository; + private readonly IGuidGenerator _guidGenerator; + + public BookStoreTestDataSeedContributor( + IRepository bookRepository, + IGuidGenerator guidGenerator) + { + _bookRepository = bookRepository; + _guidGenerator = guidGenerator; + } + + public async Task SeedAsync(DataSeedContext context) + { + await _bookRepository.InsertAsync( + new Book(id: _guidGenerator.Create(), + name: "Test book 1", + type: BookType.Fantastic, + publishDate: new DateTime(2015, 05, 24), + price: 21 + ) + ); + + await _bookRepository.InsertAsync( + new Book(id: _guidGenerator.Create(), + name: "Test book 2", + type: BookType.Science, + publishDate: new DateTime(2014, 02, 11), + price: 15 + ) + ); + } + } +} +```` + +* `IRepository` is injected and used it in the `SeedAsync` to create two book entities as the test data. + +### Testing the application service BookAppService +* `IGuidGenerator` is injected to create GUIDs. While `Guid.NewGuid()` would perfectly work for testing, `IGuidGenerator` has additional features especially important while using real databases. Further information, see the [Guid generation document](https://docs.abp.io/{{Document_Language_Code}}/abp/{{Document_Version}}/Guid-Generation). + +Create a test class named `BookAppService_Tests` in the `Acme.BookStore.Application.Tests` project: + +````csharp +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using Shouldly; +using Volo.Abp.Application.Dtos; +using Volo.Abp.Validation; +using Microsoft.EntityFrameworkCore.Internal; + +namespace Acme.BookStore +{ + public class BookAppService_Tests : BookStoreApplicationTestBase + { + private readonly IBookAppService _bookAppService; + + public BookAppService_Tests() + { + _bookAppService = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_List_Of_Books() + { + //Act + var result = await _bookAppService.GetListAsync( + new PagedAndSortedResultRequestDto() + ); + + //Assert + result.TotalCount.ShouldBeGreaterThan(0); + result.Items.ShouldContain(b => b.Name == "Test book 1"); + } + } +} +```` + +* `Should_Get_List_Of_Books` test simply uses `BookAppService.GetListAsync` method to get and check the list of users. + +Add a new test that creates a valid new book: + +````csharp +[Fact] +public async Task Should_Create_A_Valid_Book() +{ + //Act + var result = await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "New test book 42", + Price = 10, + PublishDate = System.DateTime.Now, + Type = BookType.ScienceFiction + } + ); + + //Assert + result.Id.ShouldNotBe(Guid.Empty); + result.Name.ShouldBe("New test book 42"); +} +```` + +Add a new test that tries to create an invalid book and fails: + +````csharp +[Fact] +public async Task Should_Not_Create_A_Book_Without_Name() +{ + var exception = await Assert.ThrowsAsync(async () => + { + await _bookAppService.CreateAsync( + new CreateUpdateBookDto + { + Name = "", + Price = 10, + PublishDate = DateTime.Now, + Type = BookType.ScienceFiction + } + ); + }); + + exception.ValidationErrors + .ShouldContain(err => err.MemberNames.Any(mem => mem == "Name")); +} +```` + +* Since the `Name` is empty, ABP will throw an `AbpValidationException`. + +Open the **Test Explorer Window** (use Test -> Windows -> Test Explorer menu if it is not visible) and **Run All** tests: + +![bookstore-appservice-tests](./images/bookstore-appservice-tests.png) + +Congratulations, the green icons show, the tests have been successfully passed! + diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 0de3be1651..3d8ece312f 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -33,18 +33,21 @@ }, { "text": "Tutorials", - "path": "Tutorials/Index.md", "items": [ { "text": "Application Development", "items": [ { - "text": "With ASP.NET Core MVC UI", - "path": "Tutorials/AspNetCore-Mvc/Part-I.md" + "text": "Part-1: Creating a new solution and listing items", + "path": "Tutorials/Part-1.md" }, { - "text": "With Angular UI", - "path": "Tutorials/Angular/Part-I.md" + "text": "Part-2: CRUD operations", + "path": "Tutorials/Part-2.md" + }, + { + "text": "Part-3: Integration tests", + "path": "Tutorials/Part-3.md" } ] } @@ -242,7 +245,7 @@ "items": [ { "text": "API", - "items": [ + "items": [ { "text": "Auto API Controllers", "path": "AspNetCore/Auto-API-Controllers.md" @@ -389,4 +392,4 @@ "path": "Contribution/Index.md" } ] -} \ No newline at end of file +} diff --git a/docs/en/docs-params.json b/docs/en/docs-params.json new file mode 100644 index 0000000000..23d079f9bb --- /dev/null +++ b/docs/en/docs-params.json @@ -0,0 +1,28 @@ +{ + "parameters": [ + { + "name": "UI", + "displayName": "UI", + "values": { + "MVC": "MVC / Razor Pages", + "NG": "Angular" + } + }, + { + "name": "DB", + "displayName": "Database", + "values": { + "EF": "Entity Framework Core", + "Mongo": "MongoDB" + } + }, + { + "name": "Tiered", + "displayName": "Tiered", + "values": { + "No": "Not Tiered", + "Yes": "Tiered" + } + } + ] +} \ No newline at end of file