Permalink
Fetching contributors…
Cannot retrieve contributors at this time
427 lines (305 sloc) 17.6 KB

ASP.NET Core MVC Tutorial - Part I

About this Tutorial

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 comes pre-configured with the startup template).

This is the first part of the tutorial series. See all parts:

You can download the source code of the application from here.

Creating the Project

Go to the startup template page and download a new project named Acme.BookStore, create the database and run the application by following the template document.

Solution Structure

This is the how the layered solution structure looks after it's created from the startup template:

bookstore-visual-studio-solution

Create the Book Entity

Define entities in the domain layer (Acme.BookStore.Domain project) of the solution. The main entity of the application is the Book:

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Volo.Abp.Domain.Entities.Auditing;

namespace Acme.BookStore
{
    [Table("Books")]
    public class Book : AuditedAggregateRoot<Guid>
    {
        [Required]
        [StringLength(128)]
        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 for more 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.
  • Used data annotation attributes in this code for EF Core mappings. Alternatively you could use EF Core's fluent mapping API instead.

BookType Enum

The BookType enum used above is defined as below:

namespace Acme.BookStore
{
    public enum BookType : byte
    {
        Undefined,
        Advanture,
        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:

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

Add New Migration & Update the Database

The Startup template uses EF Core Code First 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 as the default project and execute the following command:

bookstore-pmc-add-book-migration

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 Books table in the database. Open your database and enter a few sample rows, so you can show them on the page:

bookstore-books-table

Create the Application Service

The next step is to create an application service to manage (create, list, update, delete...) the books.

BookDto

Create a DTO class named BookDto into the Acme.BookStore.Application project:

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

namespace Acme.BookStore
{
    [AutoMapFrom(typeof(Book))]
    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 class defined above.
  • [AutoMapFrom(typeof(Book))] is used to create AutoMapper mapping from the Book class to the BookDto class. In this way, you get automatic convertion of Book objects to BookDto objects (instead of manually copy all properties).

CreateUpdateBookDto

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

using System;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.AutoMapper;

namespace Acme.BookStore
{
    [AutoMapTo(typeof(Book))]
    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 ABP.

IBookAppService

Define an interface named IBookAppService for the book application service:

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

namespace Acme.BookStore
{
    public interface IBookAppService : 
        IAsyncCrudAppService< //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 best practice.
  • IAsyncCrudAppService 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.
  • There are some variations of the IAsyncCrudAppService where you can use a single DTO or separated DTOs for each method.

BookAppService

Implement the IBookAppService as named BookAppService:

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

namespace Acme.BookStore
{
    public class BookAppService : 
        AsyncCrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto,
                            CreateUpdateBookDto, CreateUpdateBookDto>,
        IBookAppService
    {
        public BookAppService(IRepository<Book, Guid> repository) 
            : base(repository)
        {

        }
    }
}
  • BookAppService is derived from AsyncCrudAppService<...> which implements all the CRUD methods defined above.
  • BookAppService injects IRepository<Book, Guid> which is the default repository created for the Book entity. ABP automatically creates repositories for each aggregate root (or entity). See the repository document.
  • BookAppService uses IObjectMapper to convert Book objects to BookDto objects and CreateUpdateBookDto objects to Book objects. The Startup template uses the AutoMapper library as object mapping provider. You defined mappings using the AutoMapFrom and the AutoMapTo attributes above. See the AutoMapper integration document for details.

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 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 and enter http://localhost:53929/swagger/ 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

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 proxy using your favorite browser's Developer Console now. Run the application again, open your browser's developer tools (shortcut: F12), switch to the Console tab, type the following code and press enter:

acme.bookStore.book.getList({}).done(function (result) { console.log(result); });
  • acme.bookStore 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 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 which is used to send paging and sorting options to the server.
  • 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

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

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

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

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

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

@page
@using Acme.BookStore.Pages.Books
@inherits Acme.BookStore.Pages.BookStorePageBase
@model IndexModel

<h2>Books</h2>
  • Change the default inhertitance of the Razor View Page Model so it inherits from the BookStorePageBase class (instead of PageModel). The BookStorePageBase class which comes with the startup template and provides some shared properties/methods used by all pages.

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"])
        .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 project:

bookstore-localization-files

Open the en.json file and add localization texts for Menu:BookStore and Menu:Books keys:

{
  "culture": "en",
  "texts": {
    //...
    "Menu:BookStore": "Book Store",
    "Menu:Books": "Books"
  }
}
  • 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.
  • Localization key names are arbitrary, you can set any name. We prefer to add Menu namespace 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 menu items are added to the top bar:

bookstore-menu-items

When you click to the Books menu item, you are redirected to the new Books page.

Book List

We will use the Datatables.net JQuery plugin to show list of tables on the page. Datatables can completely work via AJAX, so 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 Changes

Change the Pages/Books/Index.cshtml as following:

@page
@using Acme.BookStore.Pages.Books
@inherits Acme.BookStore.Pages.BookStorePageBase
@model 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 for example. See the bundling & minification document for details.
  • abp-card and abp-table are tag helpers for Twitter Bootstrap's card component. There are many 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 of the intellisense and compile time type checking. 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" },
            { 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 requirment 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 for more configuration options.

The final UI is shown below:

bookstore-book-list

Next Part

See the next part of this tutorial.