Web Application Development Tutorial - Part 1: Creating the Server Side

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:

  • Entity Framework Core as the ORM provider.
  • MVC / Razor Pages as the UI Framework.

This tutorial is organized as the following parts;

Download the Source Code

This tutorial has multiple versions based on your UI and Database preferences. We’ve prepared two combinations of the source code to be downloaded:

Video Tutorial

This part is also recorded as a video tutorial and published on YouTube.

Creating the Solution

Before starting to the development, create a new solution named Acme.BookStore and run it by following the getting started tutorial.

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:

  1. using System;
  2. using Volo.Abp.Domain.Entities.Auditing;
  3. namespace Acme.BookStore.Books
  4. {
  5. public class Book : AuditedAggregateRoot<Guid>
  6. {
  7. public string Name { get; set; }
  8. public BookType Type { get; set; }
  9. public DateTime PublishDate { get; set; }
  10. public float Price { get; set; }
  11. }
  12. }
  • 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 a Books folder (namespace) in the Acme.BookStore.Domain.Shared project and add a BookType inside it:

  1. namespace Acme.BookStore.Books
  2. {
  3. public enum BookType
  4. {
  5. Undefined,
  6. Adventure,
  7. Biography,
  8. Dystopia,
  9. Fantastic,
  10. Horror,
  11. Science,
  12. ScienceFiction,
  13. Poetry
  14. }
  15. }

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

bookstore-book-and-booktype

Add Book Entity to the DbContext

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:

  1. public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
  2. {
  3. public DbSet<Book> Books { get; set; }
  4. //...
  5. }

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:

  1. using Acme.BookStore.Books;
  2. using Microsoft.EntityFrameworkCore;
  3. using Volo.Abp;
  4. using Volo.Abp.EntityFrameworkCore.Modeling;
  5. namespace Acme.BookStore.EntityFrameworkCore
  6. {
  7. public static class BookStoreDbContextModelCreatingExtensions
  8. {
  9. public static void ConfigureBookStore(this ModelBuilder builder)
  10. {
  11. Check.NotNull(builder, nameof(builder));
  12. /* Configure your own tables/entities inside here */
  13. builder.Entity<Book>(b =>
  14. {
  15. b.ToTable(BookStoreConsts.DbTablePrefix + "Books",
  16. BookStoreConsts.DbSchema);
  17. b.ConfigureByConvention(); //auto configure for the base class props
  18. b.Property(x => x.Name).IsRequired().HasMaxLength(128);
  19. });
  20. }
  21. }
  22. }
  • 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:

  1. 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.

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:

  1. using System;
  2. using System.Threading.Tasks;
  3. using Acme.BookStore.Books;
  4. using Volo.Abp.Data;
  5. using Volo.Abp.DependencyInjection;
  6. using Volo.Abp.Domain.Repositories;
  7. namespace Acme.BookStore
  8. {
  9. public class BookStoreDataSeederContributor
  10. : IDataSeedContributor, ITransientDependency
  11. {
  12. private readonly IRepository<Book, Guid> _bookRepository;
  13. public BookStoreDataSeederContributor(IRepository<Book, Guid> bookRepository)
  14. {
  15. _bookRepository = bookRepository;
  16. }
  17. public async Task SeedAsync(DataSeedContext context)
  18. {
  19. if (await _bookRepository.GetCountAsync() <= 0)
  20. {
  21. await _bookRepository.InsertAsync(
  22. new Book
  23. {
  24. Name = "1984",
  25. Type = BookType.Dystopia,
  26. PublishDate = new DateTime(1949, 6, 8),
  27. Price = 19.84f
  28. },
  29. autoSave: true
  30. );
  31. await _bookRepository.InsertAsync(
  32. new Book
  33. {
  34. Name = "The Hitchhiker's Guide to the Galaxy",
  35. Type = BookType.ScienceFiction,
  36. PublishDate = new DateTime(1995, 9, 27),
  37. Price = 42.0f
  38. },
  39. autoSave: true
  40. );
  41. }
  42. }
  43. }
  44. }
  • 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

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

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 Books folder (namespace) in the Acme.BookStore.Application.Contracts project and add a BookDto class inside it:

  1. using System;
  2. using Volo.Abp.Application.Dtos;
  3. namespace Acme.BookStore.Books
  4. {
  5. public class BookDto : AuditedEntityDto<Guid>
  6. {
  7. public string Name { get; set; }
  8. public BookType Type { get; set; }
  9. public DateTime PublishDate { get; set; }
  10. public float Price { get; set; }
  11. }
  12. }
  • 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:

  1. using Acme.BookStore.Books;
  2. using AutoMapper;
  3. namespace Acme.BookStore
  4. {
  5. public class BookStoreApplicationAutoMapperProfile : Profile
  6. {
  7. public BookStoreApplicationAutoMapperProfile()
  8. {
  9. CreateMap<Book, BookDto>();
  10. }
  11. }
  12. }

See the object to object mapping document for details.

CreateUpdateBookDto

Create a CreateUpdateBookDto class in the Books folder (namespace) of the Acme.BookStore.Application.Contracts project:

  1. using System;
  2. using System.ComponentModel.DataAnnotations;
  3. namespace Acme.BookStore.Books
  4. {
  5. public class CreateUpdateBookDto
  6. {
  7. [Required]
  8. [StringLength(128)]
  9. public string Name { get; set; }
  10. [Required]
  11. public BookType Type { get; set; } = BookType.Undefined;
  12. [Required]
  13. [DataType(DataType.Date)]
  14. public DateTime PublishDate { get; set; } = DateTime.Now;
  15. [Required]
  16. public float Price { get; set; }
  17. }
  18. }
  • 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:

  1. using Acme.BookStore.Books;
  2. using AutoMapper;
  3. namespace Acme.BookStore
  4. {
  5. public class BookStoreApplicationAutoMapperProfile : Profile
  6. {
  7. public BookStoreApplicationAutoMapperProfile()
  8. {
  9. CreateMap<Book, BookDto>();
  10. CreateMap<CreateUpdateBookDto, Book>();
  11. }
  12. }
  13. }

IBookAppService

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:

  1. using System;
  2. using Volo.Abp.Application.Dtos;
  3. using Volo.Abp.Application.Services;
  4. namespace Acme.BookStore.Books
  5. {
  6. public interface IBookAppService :
  7. ICrudAppService< //Defines CRUD methods
  8. BookDto, //Used to show books
  9. Guid, //Primary key of the book entity
  10. PagedAndSortedResultRequestDto, //Used for paging/sorting
  11. CreateUpdateBookDto> //Used to create/update a book
  12. {
  13. }
  14. }
  • 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

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:

  1. using System;
  2. using Volo.Abp.Application.Dtos;
  3. using Volo.Abp.Application.Services;
  4. using Volo.Abp.Domain.Repositories;
  5. namespace Acme.BookStore.Books
  6. {
  7. public class BookAppService :
  8. CrudAppService<
  9. Book, //The Book entity
  10. BookDto, //Used to show books
  11. Guid, //Primary key of the book entity
  12. PagedAndSortedResultRequestDto, //Used for paging/sorting
  13. CreateUpdateBookDto>, //Used to create/update a book
  14. IBookAppService //implement the IBookAppService
  15. {
  16. public BookAppService(IRepository<Book, Guid> repository)
  17. : base(repository)
  18. {
  19. }
  20. }
  21. }
  • 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:

  1. {
  2. "totalCount": 2,
  3. "items": [
  4. {
  5. "name": "The Hitchhiker's Guide to the Galaxy",
  6. "type": 7,
  7. "publishDate": "1995-09-27T00:00:00",
  8. "price": 42,
  9. "lastModificationTime": null,
  10. "lastModifierId": null,
  11. "creationTime": "2020-07-03T21:04:18.4607218",
  12. "creatorId": null,
  13. "id": "86100bb6-cbc1-25be-6643-39f62806969c"
  14. },
  15. {
  16. "name": "1984",
  17. "type": 3,
  18. "publishDate": "1949-06-08T00:00:00",
  19. "price": 19.84,
  20. "lastModificationTime": null,
  21. "lastModifierId": null,
  22. "creationTime": "2020-07-03T21:04:18.3174016",
  23. "creatorId": null,
  24. "id": "41055277-cce8-37d7-bb37-39f62806960b"
  25. }
  26. ]
  27. }

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!

The Next Part

See the next part of this tutorial.