docs/en/tutorials/book-store/part-01.md
//[doc-seo]
{
"Description": "Learn how to create the server side of your web application with ABP Framework, including setting up your solution and installing client-side packages."
}
//[doc-params]
{
"UI": ["MVC","Blazor","BlazorServer", "BlazorWebApp","NG","MAUIBlazor"],
"DB": ["EF","Mongo"]
}
//[doc-nav]
{
"Next": {
"Name": "The Book List Page",
"Path": "tutorials/book-store/part-02"
}
}
Before starting the development, create a new solution named Acme.BookStore and run it by following the getting started tutorial.
ABP CLI runs the abp install-libs command behind the scenes to install the required NPM packages for your solution while creating the application.
However, sometimes this command might need to be manually run. For example, you need to run this command, if you have cloned the application, or the resources from node_modules folder didn't copy to wwwroot/libs folder, or if you have added a new client-side package dependency to your solution.
For such cases, run the abp install-libs command on the root directory of your solution to install all required NPM packages:
abp install-libs
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 that 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; }
}
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 tutorial leaves the entity properties with public get/set for the sake of simplicity. See the entities document if you want to learn more about DDD best practices.
The Book entity uses the BookType enum. Create a Books folder (namespace) in the Acme.BookStore.Domain.Shared project and add a BookType inside it:
namespace Acme.BookStore.Books;
public enum BookType
{
Undefined,
Adventure,
Biography,
Dystopia,
Fantastic,
Horror,
Science,
ScienceFiction,
Poetry
}
The final folder/file structure should be as shown below:
{{if DB == "EF"}}
EF Core requires that you relate the entities with your DbContext. The easiest way to do so is adding a DbSet property to the BookStoreDbContext class in the Acme.BookStore.EntityFrameworkCore project, as shown below:
using Acme.BookStore.Books;
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
{
public DbSet<Book> Books { get; set; }
//...
}
{{end}}
{{if DB == "Mongo"}}
Add an IMongoCollection<Book> Books property to the BookStoreMongoDbContext inside the Acme.BookStore.MongoDB project:
using Acme.BookStore.Books;
public class BookStoreMongoDbContext : AbpMongoDbContext
{
public IMongoCollection<Book> Books => Collection<Book>();
//...
}
{{end}}
{{if DB == "EF"}}
Navigate to the OnModelCreating method in the BookStoreDbContext class and add the mapping code for the Book entity:
using Acme.BookStore.Books;
...
namespace Acme.BookStore.EntityFrameworkCore;
public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{
...
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
/* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
...
/* Configure your own tables/entities inside here */
builder.Entity<Book>(b =>
{
b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
BookStoreConsts.DbSchema);
b.ConfigureByConvention(); //auto configure for the base class props
b.Property(x => x.Name).IsRequired().HasMaxLength(128);
});
}
}
BookStoreConsts has constant values for the schema and table prefixes for your tables. You don't have to use it, but it's 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.The startup solution is configured to use Entity Framework Core Code First Migrations. Since we've changed the database mapping configuration, we should create a new migration and apply changes to the database.
Open a command-line terminal in the directory of the Acme.BookStore.EntityFrameworkCore project and type the following command:
dotnet ef migrations add Created_Book_Entity
This will add a new migration class to the project:
If you are using Visual Studio, you may want to use the
Add-Migration Created_Book_EntityandUpdate-Databasecommands in the Package Manager Console (PMC). In this case, ensure thatAcme.BookStore.EntityFrameworkCoreis the startup project in Visual Studio andAcme.BookStore.EntityFrameworkCoreis the Default Project in PMC.
{{end}}
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. You can skip this section if you don't want to create the data seeding, but it is suggested to follow along and learn this useful ABP feature.
Create a class that implements the IDataSeedContributor interface in the *.Domain project by copying 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)
{
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
);
}
}
}
IRepository<Book, Guid> (the default repository) to insert two books to the database in case there weren't any books in it.Run the Acme.BookStore.DbMigrator application to update the database:
.DbMigrator is a console application that can be run to migrate the database schema and seed the data on development and production environments.
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.
CrudAppService base class requires to define the fundamental DTOs for the entity. Create a Books folder (namespace) in the Acme.BookStore.Application.Contracts project and add a BookDto class inside it:
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; }
}
BookDto is used to transfer the 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 the Book entities to the BookDto objects while returning books to the presentation layer. Mapperly library can automate this conversion when you define the proper mapping. The startup template comes with Mapperly pre-configured. So, you can just define the mapping in the BookStoreApplicationMappers class in the Acme.BookStore.Application project:
[Mapper]
public partial class BookToBookDtoMapper : MapperBase<Book, BookDto>
{
public override partial BookDto Map(Book source);
public override partial void Map(Book source, BookDto destination);
}
See the object to object mapping document for details.
Create a CreateUpdateBookDto class in the Books folder (namespace) of 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; } = string.Empty;
[Required]
public BookType Type { get; set; } = BookType.Undefined;
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; } = DateTime.Now;
[Required]
public float Price { get; set; }
}
DTO class is used to get a book information from the user interface while creating or updating the book.[Required]) to define validations for the properties. DTOs are automatically validated by the ABP.As done to the BookDto above, we should define the mapping from the CreateUpdateBookDto object to the Book entity. The final class will be as shown below:
[Mapper]
public partial class BookToBookDtoMapper : MapperBase<Book, BookDto>
{
public override partial BookDto Map(Book source);
public override partial void Map(Book source, BookDto destination);
}
[Mapper]
public partial class CreateUpdateBookDtoToBookMapper : MapperBase<CreateUpdateBookDto, Book>
{
public override partial Book Map(CreateUpdateBookDto source);
public override partial void Map(CreateUpdateBookDto source, Book destination);
}
Next step is to define an interface for the application service. Create an IBookAppService interface in the Books folder (namespace) of 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
{
}
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).ICrudAppService where you can use separated DTOs for each method (like using different DTOs for create and update).It is time to implement the IBookAppService interface. Create a new class, named BookAppService in the Books namespace (folder) of 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 the Book objects to the BookDto objects and CreateUpdateBookDto objects to the Book objects. The Startup template uses the Mapperly library as the object mapping provider. We have defined the mappings before, so it will work as expected.In a typical ASP.NET Core application, you create API Controllers to expose the application services as HTTP API endpoints. This allows browsers or 3rd-party clients to call them over HTTP.
ABP can automagically configure your application services as MVC API Controllers by convention.
The startup template is configured to run the Swagger UI using the Swashbuckle.AspNetCore library. Run the application ({{if UI=="MVC"}}Acme.BookStore.Web{{else if UI=="BlazorServer" || UI=="BlazorWebApp"}}Acme.BookStore.Blazor{{else}}Acme.BookStore.HttpApi.Host{{end}}) 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:
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!