You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
abp/docs/en/Tutorials/Part-1.md

36 KiB

ASP.NET Core {{UI_Value}} Tutorial - Part 1

//[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 based web application named Acme.BookStore. This application is used to manage a list of books and their authors. It is developed using the following technologies:

  • {{DB_Text}} as the ORM provider.
  • {{UI_Value}} as the UI Framework.

This tutorial is organized as the following parts;

Source Code

You can find the completed solution on {{if UI == "MVC"}}the GitHub repository{{else}}the GitHub repository{{end}}.

Creating the Solution

Before starting to the development, create a new solution named Acme.BookStore and run it by following the [getting started tutorial](../Getting-Started-{{if UI == 'NG'}}Angular{{else}}AspNetCore-MVC{{end}}-Template#creating-a-new-project).

Create the Book Entity

Domain layer in the startup template is separated into two projects:

  • Acme.BookStore.Domain contains your entities, 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.

So, define your entities in the domain layer (Acme.BookStore.Domain project) of the solution.

The main entity of the application is the Book. Create a Books folder (namespace) in the Acme.BookStore.Domain project and add a Book class inside it:

using System;
using Volo.Abp.Domain.Entities.Auditing;

namespace Acme.BookStore.Books
{
    public class Book : AuditedAggregateRoot<Guid>
    {
        public string Name { get; set; }

        public BookType Type { get; set; }

        public DateTime PublishDate { get; set; }

        public float Price { get; set; }
    }
}
  • ABP Framework has two fundamental base classes for entities: AggregateRoot and Entity. Aggregate Root is a Domain Driven Design concept which can be thought as a root entity that is directly queried and worked on (see the entities document for more).
  • Book entity inherits from the AuditedAggregateRoot which adds some base auditing properties (like CreationTime, CreatorId, LastModificationTime...) on top of the AggregateRoot class. ABP automatically manages these properties for you.
  • Guid is the primary key type of the Book entity.

This tutorials leaves the entity properties with public get/set for the sake of simplicity. See the entities document if you learn more about DDD best practices.

BookType Enum

The Book entity uses the BookType enum. Create the BookType in the Acme.BookStore.Domain.Shared project:

namespace Acme.BookStore
{
    public enum BookType
    {
        Undefined,
        Adventure,
        Biography,
        Dystopia,
        Fantastic,
        Horror,
        Science,
        ScienceFiction,
        Poetry
    }
}

The final folder/file structure should be as shown below:

bookstore-book-and-booktype

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:

public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
    public DbSet<Book> Books { get; set; }
    //...
}

{{end}}

{{if DB == "mongodb"}}

Add a IMongoCollection<Book> Books property to the BookStoreMongoDbContext inside the Acme.BookStore.MongoDB project:

public class BookStoreMongoDbContext : AbpMongoDbContext
{
    public IMongoCollection<Book> Books => Collection<Book>();
    //...
}

{{end}}

{{if DB == "ef"}}

Map the Book Entity to a Database Table

Open BookStoreDbContextModelCreatingExtensions.cs file in the Acme.BookStore.EntityFrameworkCore project and add the mapping code for the Book entity. The final class should be the following:

using Acme.BookStore.Books;
using Microsoft.EntityFrameworkCore;
using Volo.Abp;
using Volo.Abp.EntityFrameworkCore.Modeling;

namespace Acme.BookStore.EntityFrameworkCore
{
    public static class BookStoreDbContextModelCreatingExtensions
    {
        public static void ConfigureBookStore(this ModelBuilder builder)
        {
            Check.NotNull(builder, nameof(builder));

            /* Configure your own tables/entities inside here */

            builder.Entity<Book>(b =>
            {
                b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
                          BookStoreConsts.DbSchema);
                b.ConfigureByConvention(); //auto configure for the base class props
                b.Property(x => x.Name).IsRequired().HasMaxLength(128);
            });
        }
    }
}
  • BookStoreConsts has constant values for schema and table prefixes for your tables. You don't have to use it, but suggested to control the table prefixes in a single point.
  • ConfigureByConvention() method gracefully configures/maps the inherited properties. Always use it for all your entities.

Add Database Migration

The startup template uses EF Core Code First 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

Select the Acme.BookStore.EntityFrameworkCore.DbMigrations as the default project and execute the following command:

Add-Migration "Created_Book_Entity"

bookstore-pmc-add-book-migration

This will create a new migration class inside the Migrations folder of the Acme.BookStore.EntityFrameworkCore.DbMigrations project.

Before updating the database, read the section below to learn how to seed some initial data to the database.

If you are using another IDE than the Visual Studio, you can use dotnet-ef tool as documented here.

{{end}}

Add Sample Seed Data

It's good to have some initial data in the database before running the application. This section introduces the Data Seeding system of the ABP framework. You can skip this section if you don't want to create seed data, but it is suggested to follow it to learn this useful ABP Framework feature.

Create a class deriving from the IDataSeedContributor in the *.Domain project and copy the following code:

using System;
using System.Threading.Tasks;
using Acme.BookStore.Books;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore
{
    public class BookStoreDataSeederContributor
        : IDataSeedContributor, ITransientDependency
    {
        private readonly IRepository<Book, Guid> _bookRepository;

        public BookStoreDataSeederContributor(IRepository<Book, Guid> 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
                },
                autoSave: true
            );

            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
                },
                autoSave: true
            );
        }
    }
}
  • This code simply uses the IRepository<Book, Guid> (the default repository) to insert two books to the database, if there is no book currently in the database.

Update the Database

Run the Acme.BookStore.DbMigrator application to update the database:

bookstore-dbmigrator-on-solution

{{if DB == "ef"}}

.DbMigrator is a console application that can be run to migrate the database schema and seed the data on development and production environments.

{{end}}

{{if DB == "mongodb"}}

While MongoDB doesn't require a database schema migration, it is still good to run this application since it seeds the initial data on the database. This application can be used on development and production environments.

{{end}}

Create the Application Service

The application layer is separated into two projects:

  • Acme.BookStore.Application.Contracts contains your DTOs and application service interfaces.
  • Acme.BookStore.Application contains the implementations of your application services.

In this section, you will create an application service to get, create, update and delete books using the CrudAppService base class of the ABP Framework.

BookDto

CrudAppService base class requires to define the fundamental DTOs for the entity. Create a DTO class named BookDto into the Acme.BookStore.Application.Contracts project:

using System;
using Volo.Abp.Application.Dtos;

namespace Acme.BookStore.Books
{
    public class BookDto : AuditedEntityDto<Guid>
    {
        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 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<Guid> which has audit properties just like the Book entity defined above.

It will be needed to map Book entities to BookDto objects while returning books to the presentation layer. AutoMapper library can automate this conversion when you define the proper mapping. The startup template comes with AutoMapper pre-configured. So, you can just define the mapping in the BookStoreApplicationAutoMapperProfile class in the Acme.BookStore.Application project:

using Acme.BookStore.Books;
using AutoMapper;

namespace Acme.BookStore
{
    public class BookStoreApplicationAutoMapperProfile : Profile
    {
        public BookStoreApplicationAutoMapperProfile()
        {
            CreateMap<Book, BookDto>();
        }
    }
}

See the object to object mapping document for details.

CreateUpdateBookDto

Create another DTO class named CreateUpdateBookDto into the Acme.BookStore.Application.Contracts project:

using System;
using System.ComponentModel.DataAnnotations;

namespace Acme.BookStore.Books
{
    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 by the ABP framework.

Just like done for the BookDto above, we should define the mapping from the CreateUpdateBookDto object to the Book entity. The final class will be like shown below:

using Acme.BookStore.Books;
using AutoMapper;

namespace Acme.BookStore
{
    public class BookStoreApplicationAutoMapperProfile : Profile
    {
        public BookStoreApplicationAutoMapperProfile()
        {
            CreateMap<Book, BookDto>();
            CreateMap<CreateUpdateBookDto, Book>();
        }
    }
}

IBookAppService

Next step is to define an interface for the application service. Create an interface named IBookAppService in the Acme.BookStore.Application.Contracts project:

using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;

namespace Acme.BookStore.Books
{
    public interface IBookAppService :
        ICrudAppService< //Defines CRUD methods
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto> //Used to create/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 (which will be done for the authors in the next parts).
  • There are some variations of the ICrudAppService where you can use separated DTOs for each method (like using different DTOs for create and update).

BookAppService

Implement the IBookAppService, as named BookAppService, in the Acme.BookStore.Application project:

using System;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace Acme.BookStore.Books
{
    public class BookAppService :
        CrudAppService<
            Book, //The Book entity
            BookDto, //Used to show books
            Guid, //Primary key of the book entity
            PagedAndSortedResultRequestDto, //Used for paging/sorting
            CreateUpdateBookDto>, //Used to create/update a book
        IBookAppService //implement the IBookAppService
    {
        public BookAppService(IRepository<Book, Guid> repository)
            : base(repository)
        {

        }
    }
}
  • BookAppService is derived from CrudAppService<...> which implements all the CRUD (create, read, update, delete) methods defined by the ICrudAppService.
  • BookAppService injects IRepository<Book, Guid> which is the default repository for the Book entity. ABP automatically creates default repositories for each aggregate root (or entity). See the repository document.
  • BookAppService uses IObjectMapper service (see) to map Book objects to BookDto objects and CreateUpdateBookDto objects to Book objects. The Startup template uses the AutoMapper library as the object mapping provider. We have defined the mappings before, so it will work as expected.

Auto API Controllers

In a typical ASP.NET Core application, you create API Controllers to expose application services as HTTP API endpoints. This allows browsers or 3rd-party clients to call them over HTTP.

ABP can automagically configures your application services as MVC API Controllers by convention.

Swagger UI

The startup template is configured to run the Swagger UI using the Swashbuckle.AspNetCore library. Run the application by pressing CTRL+F5 and navigate to https://localhost:<port>/swagger/ on your browser. (Replace <port> 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

Swagger has a nice interface to test the APIs.

If you try to execute the [GET] /api/app/book API to get a list of books, the server returns such a JSON result:

{
  "totalCount": 2,
  "items": [
    {
      "name": "The Hitchhiker's Guide to the Galaxy",
      "type": 7,
      "publishDate": "1995-09-27T00:00:00",
      "price": 42,
      "lastModificationTime": null,
      "lastModifierId": null,
      "creationTime": "2020-07-03T21:04:18.4607218",
      "creatorId": null,
      "id": "86100bb6-cbc1-25be-6643-39f62806969c"
    },
    {
      "name": "1984",
      "type": 3,
      "publishDate": "1949-06-08T00:00:00",
      "price": 19.84,
      "lastModificationTime": null,
      "lastModifierId": null,
      "creationTime": "2020-07-03T21:04:18.3174016",
      "creatorId": null,
      "id": "41055277-cce8-37d7-bb37-39f62806960b"
    }
  ]
}

That's pretty cool since we haven't written a single line of code to create the API controller, but now we have a fully working REST API!

{{if UI == "MVC"}}

Dynamic JavaScript Proxies

It's common to call the 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 generally F12), switch to the Console tab, type the following code and press enter:

acme.bookStore.books.book.getList({}).done(function (result) { console.log(result); });
  • acme.bookStore.books is the namespace of the BookAppService converted to camelCase.
  • 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 CrudAppService 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 with default values, so you can send an empty object).
  • getList function returns a promise. You can pass a callback to the then (or done) function to get the result returned from the server.

Running this code produces the following output:

bookstore-javascript-proxy-console

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-getlist-result-network

Let's create a new book using the create function:

acme.bookStore.books.book.create({ 
        name: 'Foundation', 
        type: 7, 
        publishDate: '1951-05-24', 
        price: 21.5 
    }).then(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: 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.

We will use these dynamic proxy functions in the next sections to communicate to the server.

Create a Books Page

It's time to create something visible and usable! Instead of classic MVC, we will use the Razor Pages UI 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

Open the Index.cshtml and change the whole content as shown below:

@page
@using Acme.BookStore.Web.Pages.Books
@model IndexModel

<h2>Books</h2>

Index.cshtml.cs content should be like that:

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:

context.Menu.AddItem(
    new ApplicationMenuItem(
        "BooksStore",
        l["Menu:BookStore"],
        icon: "fa fa-book"
    ).AddItem(
        new ApplicationMenuItem(
            "BooksStore.Books",
            l["Menu:Books"],
            url: "/Books"
        )
    )
);

{{end}}

Localization

Localization texts are located under the Localization/BookStore folder of the Acme.BookStore.Domain.Shared project:

bookstore-localization-files

Open the en.json (the English translations) file and change the content as below:

{
  "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?",
    "Enum:BookType:0": "Undefined",
    "Enum:BookType:1": "Adventure",
    "Enum:BookType:2": "Biography",
    "Enum:BookType:3": "Dystopia",
    "Enum:BookType:4": "Fantastic",
    "Enum:BookType:5": "Horror",
    "Enum:BookType:6": "Science",
    "Enum:BookType:7": "ScienceFiction",
    "Enum:BookType:8": "Poetry"
  }
}
  • Localization key names are arbitrary. You can set any name. We prefer some conventions for specific text types;
    • Add Menu: prefix for menu items.
    • Use Enum:<enum-type>:<enum-value> naming convention to localize the enum members. When you do it like that, ABP can automatically localize the enums.

If a text is not defined in the localization file, it fallbacks to the localization key (as ASP.NET Core's standard behavior).

ABP's localization system is built on ASP.NET Core's standard localization system and extends it in many ways. See the localization document for details.

{{if UI == "MVC"}}

Run the project, login to the application with the username admin and the password 1q2w3E* and see the new menu item has been added to the main menu:

bookstore-menu-items

When you click to the Books menu item under the Book Store parent, you are being redirected to the new empty Books Page.

Book list

We will use the Datatables.net jQuery plugin to show the book list. Datatables can completely work via AJAX, it is fast, popular 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:

@page
@model Acme.BookStore.Web.Pages.Books.IndexModel
@section scripts
{
    <abp-script src="/Pages/Books/index.js" />
}
<abp-card>
    <abp-card-header>
        <h2>@L["Books"]</h2>
    </abp-card-header>
    <abp-card-body>
        <abp-table striped-rows="true" id="BooksTable">
            <thead>
                <tr>
                    <th>@L["Name"]</th>
                    <th>@L["Type"]</th>
                    <th>@L["PublishDate"]</th>
                    <th>@L["Price"]</th>
                    <th>@L["CreationTime"]</th>
                </tr>
            </thead>
        </abp-table>
    </abp-card-body>
</abp-card>
  • abp-script tag helper 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 for details.
  • abp-card and abp-table are tag helpers for Twitter Bootstrap's card component. There are other useful tag helpers in ABP to easily use most of the bootstrap 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 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

index.js content is shown below:

$(function () {
    var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
        ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
        columnDefs: [
            { data: "name" },
            { data: "type",
              render: function(data){
                return l('Enum:BookType:' + data);
              }
            },
            { 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 (as described in dynamic JavaScript proxies).
  • See Datatables documentation for all configuration options.

It's end of this part. The final UI of this work is shown as below:

Book list

{{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:

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:

yarn

BookModule

Run the following command line to create a new module, named BookModule:

yarn ng generate module book --routing true

Generating books module

Routing

Open the app-routing.module.ts file in src\app folder and add a route as shown below:

const routes: Routes = [
// ...
// added a new route to the routes array
  {
    path: 'books',
    loadChildren: () => import('./book/book.module').then(m => m.BookModule)
  }
]

Open the route.provider.ts file in src\app folder and replace the content as below:

import { RoutesService, eLayoutType } from '@abp/ng.core';
import { APP_INITIALIZER } from '@angular/core';

export const APP_ROUTE_PROVIDER = [
  { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true },
];

function configureRoutes(routes: RoutesService) {
  return () => {
    routes.add([
      //...
      // added below element
      {
        path: '/books',
        name: '::Menu:Books',
        iconClass: 'fas fa-book',
        order: 101,
        layout: eLayoutType.application,
      },
    ]);
  };
}
  • We added a new route element to show a navigation element labeled "Books" on the menu.
    • path is the URL of the route.
    • name is the menu item name. A Localization key can be passed.
    • iconClass is the icon of the menu item.
    • order is the order of the menu item. We define 101 to show the route after the "Administration" item.
    • layout is the layout of the BooksModule's routes. eLayoutType.application, eLayoutType.account or eLayoutType.empty can be defined.

For more information, see the RoutesService document.

Book list component

Run the command below on the terminal in the root folder to generate a new component, named book-list:

yarn ng generate component book/book-list

Creating books list

Open book.module.ts file in the app\book folder and replace the content as below:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BookRoutingModule } from './book-routing.module';
import { BookListComponent } from './book-list/book-list.component';
import { SharedModule } from '../shared/shared.module'; //<== added this line ==>

@NgModule({
  declarations: [BookListComponent],
  imports: [
    CommonModule,
    BookRoutingModule,
    SharedModule, //<== added this line ==>
  ],
})
export class BookModule {}
  • We imported SharedModule and added to imports array.

Open book-routing.module.ts file in the app\book folder and replace the content as below:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { BookListComponent } from './book-list/book-list.component'; // <== added this line ==>

// <== replaced routes ==>
const routes: Routes = [
  {
    path: '',
    component: BookListComponent,
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class BookRoutingModule { }
  • We imported BookListComponent and replaced routes const.

Run yarn start and wait for Angular to serve the application:

yarn start

Open the browser and navigate to http://localhost:4200/books. We'll see book-list works! text on the books page:

Initial book list page

Generate proxies

ABP CLI provides generate-proxy command that generates client proxies for your HTTP APIs to make easy to consume your services from the client side. Before running generate-proxy command, your host must be up and running. See the CLI documentation

Run the following command in the angular folder:

abp generate-proxy --module app

Generate proxy command

The generated files looks like below:

Generated files

BookListComponent

Open the book-list.component.ts file in app\book\book-list folder and replace the content as below:

import { ListService, PagedResultDto } from '@abp/ng.core';
import { Component, OnInit } from '@angular/core';
import { BookDto, BookType } from '../models';
import { BookService } from '../services';

@Component({
  selector: 'app-book-list',
  templateUrl: './book-list.component.html',
  styleUrls: ['./book-list.component.scss'],
  providers: [ListService],
})
export class BookListComponent implements OnInit {
  book = { items: [], totalCount: 0 } as PagedResultDto<BookDto>;

  booksType = BookType;

  constructor(public readonly list: ListService, private bookService: BookService) {}

  ngOnInit() {
    const bookStreamCreator = (query) => this.bookService.getListByInput(query);

    this.list.hookToQuery(bookStreamCreator).subscribe((response) => {
      this.book = response;
    });
  }
}
  • We imported and injected the generated BookService.
  • We implemented the ListService that is a utility service to provide easy pagination, sorting, and search implementation.

Open the book-list.component.html file in app\book\book-list folder and replace the content as below:

<div class="card">
  <div class="card-header">
    <div class="row">
      <div class="col col-md-6">
        <h5 class="card-title">
          {%{{{ '::Menu:Books' | abpLocalization }}}%}
        </h5>
      </div>
      <div class="text-right col col-md-6"></div>
    </div>
  </div>
  <div class="card-body">
    <ngx-datatable [rows]="book.items" [count]="book.totalCount" [list]="list" default>
      <ngx-datatable-column [name]="'::Name' | abpLocalization" prop="name"></ngx-datatable-column>
      <ngx-datatable-column [name]="'::Type' | abpLocalization" prop="type">
        <ng-template let-row="row" ngx-datatable-cell-template>
          {%{{{ booksType[row.type] }}}%}
        </ng-template>
      </ngx-datatable-column>
      <ngx-datatable-column [name]="'::PublishDate' | abpLocalization" prop="publishDate">
        <ng-template let-row="row" ngx-datatable-cell-template>
          {%{{{ row.publishDate | date }}}%}
        </ng-template>
      </ngx-datatable-column>
      <ngx-datatable-column [name]="'::Price' | abpLocalization" prop="price">
        <ng-template let-row="row" ngx-datatable-cell-template>
          {%{{{ row.price | currency }}}%}
        </ng-template>
      </ngx-datatable-column>
    </ngx-datatable>
  </div>
</div>
  • We added HTML code of book list page.

Now you can see the final result on your browser:

Book list final result

The file system structure of the project:

Book list final result

In this tutorial we have applied the rules of official Angular Style Guide.

{{end}}

Next Part

See the part 2 for creating, updating and deleting books.