mirror of https://github.com/abpframework/abp
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.
427 lines
18 KiB
427 lines
18 KiB
7 years ago
|
## ASP.NET Core MVC Tutorial - Part I
|
||
|
|
||
7 years ago
|
### About this Tutorial
|
||
7 years ago
|
|
||
7 years ago
|
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](https://abp.io/Templates)).
|
||
7 years ago
|
|
||
7 years ago
|
This is the first part of the tutorial series. See all parts:
|
||
7 years ago
|
|
||
7 years ago
|
- **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)
|
||
7 years ago
|
|
||
7 years ago
|
You can download the **source code** of the application [from here](https://github.com/volosoft/abp/tree/master/samples/BookStore).
|
||
|
|
||
|
### Creating the Project
|
||
|
|
||
6 years ago
|
Go to the [startup template page](https://abp.io/Templates) and download a new project named `Acme.BookStore`, create the database and run the application by following the [template document](../../Getting-Started-AspNetCore-MVC-Template.md).
|
||
7 years ago
|
|
||
7 years ago
|
### Solution Structure
|
||
|
|
||
6 years ago
|
This is the how the layered solution structure looks after it's created from the startup template:
|
||
7 years ago
|
|
||
7 years ago
|
![bookstore-visual-studio-solution](images/bookstore-visual-studio-solution.png)
|
||
7 years ago
|
|
||
|
### Create the Book Entity
|
||
|
|
||
|
Define [entities](../../Entities.md) in the **domain layer** (`Acme.BookStore.Domain` project) of the solution. The main entity of the application is the `Book`:
|
||
|
|
||
|
````C#
|
||
|
using System;
|
||
|
using System.ComponentModel.DataAnnotations;
|
||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||
7 years ago
|
using Volo.Abp.Domain.Entities.Auditing;
|
||
7 years ago
|
|
||
|
namespace Acme.BookStore
|
||
|
{
|
||
|
[Table("Books")]
|
||
7 years ago
|
public class Book : AuditedAggregateRoot<Guid>
|
||
7 years ago
|
{
|
||
|
[Required]
|
||
|
[StringLength(128)]
|
||
|
public string Name { get; set; }
|
||
|
|
||
|
public BookType Type { get; set; }
|
||
|
|
||
|
public DateTime PublishDate { get; set; }
|
||
|
|
||
|
public float Price { get; set; }
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
|
||
6 years ago
|
* 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 more details and best practices.
|
||
7 years ago
|
* `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.
|
||
6 years ago
|
* Used **data annotation attributes** in this code for EF Core mappings. Alternatively you could use EF Core's [fluent mapping API](https://docs.microsoft.com/en-us/ef/core/modeling) instead.
|
||
7 years ago
|
|
||
|
#### BookType Enum
|
||
|
|
||
|
The `BookType` enum used above is defined as below:
|
||
|
|
||
|
````C#
|
||
|
namespace Acme.BookStore
|
||
|
{
|
||
|
public enum BookType : byte
|
||
|
{
|
||
|
Undefined,
|
||
|
Advanture,
|
||
|
Biography,
|
||
|
Dystopia,
|
||
|
Fantastic,
|
||
|
Horror,
|
||
|
Science,
|
||
|
ScienceFiction,
|
||
|
Poetry
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
|
||
|
#### Add Book Entity to Your DbContext
|
||
|
|
||
6 years ago
|
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:
|
||
7 years ago
|
|
||
|
````C#
|
||
|
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
|
||
|
{
|
||
|
public DbSet<Book> Book { get; set; }
|
||
|
...
|
||
|
}
|
||
|
````
|
||
|
|
||
7 years ago
|
#### Add New Migration & Update the Database
|
||
7 years ago
|
|
||
6 years ago
|
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` as the **default project** and execute the following command:
|
||
7 years ago
|
|
||
7 years ago
|
![bookstore-pmc-add-book-migration](images/bookstore-pmc-add-book-migration.png)
|
||
7 years ago
|
|
||
|
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
|
||
|
|
||
7 years ago
|
`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:
|
||
7 years ago
|
|
||
7 years ago
|
![bookstore-books-table](images/bookstore-books-table.png)
|
||
7 years ago
|
|
||
|
### Create the Application Service
|
||
|
|
||
7 years ago
|
The next step is to create an [application service](../../Application-Services.md) to manage (create, list, update, delete...) the books.
|
||
7 years ago
|
|
||
7 years ago
|
#### BookDto
|
||
|
|
||
|
Create a DTO class named `BookDto` into the `Acme.BookStore.Application` project:
|
||
|
|
||
|
````C#
|
||
|
using System;
|
||
|
using Volo.Abp.Application.Dtos;
|
||
7 years ago
|
using Volo.Abp.AutoMapper;
|
||
7 years ago
|
|
||
|
namespace Acme.BookStore
|
||
|
{
|
||
7 years ago
|
[AutoMapFrom(typeof(Book))]
|
||
|
public class BookDto : AuditedEntityDto<Guid>
|
||
7 years ago
|
{
|
||
|
public string Name { get; set; }
|
||
|
|
||
|
public BookType Type { get; set; }
|
||
|
|
||
|
public DateTime PublishDate { get; set; }
|
||
|
|
||
|
public float Price { get; set; }
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
|
||
6 years ago
|
* **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<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).
|
||
7 years ago
|
|
||
7 years ago
|
#### CreateUpdateBookDto
|
||
7 years ago
|
|
||
7 years ago
|
Create a DTO class named `CreateUpdateBookDto` into the `Acme.BookStore.Application` project:
|
||
7 years ago
|
|
||
|
````c#
|
||
|
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; }
|
||
|
|
||
7 years ago
|
[Required]
|
||
7 years ago
|
public BookType Type { get; set; } = BookType.Undefined;
|
||
|
|
||
7 years ago
|
[Required]
|
||
7 years ago
|
public DateTime PublishDate { get; set; }
|
||
|
|
||
7 years ago
|
[Required]
|
||
7 years ago
|
public float Price { get; set; }
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
|
||
7 years ago
|
* 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.
|
||
7 years ago
|
|
||
|
#### IBookAppService
|
||
|
|
||
7 years ago
|
Define an interface named `IBookAppService` for the book application service:
|
||
7 years ago
|
|
||
|
````C#
|
||
|
using System;
|
||
7 years ago
|
using Volo.Abp.Application.Dtos;
|
||
7 years ago
|
using Volo.Abp.Application.Services;
|
||
|
|
||
|
namespace Acme.BookStore
|
||
|
{
|
||
7 years ago
|
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
|
||
7 years ago
|
{
|
||
|
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
|
||
6 years ago
|
* Defining interfaces for application services is <u>not required</u> by the framework. However, it's suggested as best practice.
|
||
7 years ago
|
* `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.
|
||
7 years ago
|
* There are some variations of the `IAsyncCrudAppService` where you can use a single DTO or separated DTOs for each method.
|
||
7 years ago
|
|
||
|
#### BookAppService
|
||
|
|
||
|
Implement the `IBookAppService` as named `BookAppService`:
|
||
|
|
||
|
````C#
|
||
|
using System;
|
||
7 years ago
|
using Volo.Abp.Application.Dtos;
|
||
7 years ago
|
using Volo.Abp.Application.Services;
|
||
|
using Volo.Abp.Domain.Repositories;
|
||
|
|
||
|
namespace Acme.BookStore
|
||
|
{
|
||
7 years ago
|
public class BookAppService :
|
||
|
AsyncCrudAppService<Book, BookDto, Guid, PagedAndSortedResultRequestDto,
|
||
|
CreateUpdateBookDto, CreateUpdateBookDto>,
|
||
|
IBookAppService
|
||
7 years ago
|
{
|
||
|
public BookAppService(IRepository<Book, Guid> repository)
|
||
|
: base(repository)
|
||
|
{
|
||
|
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
````
|
||
|
|
||
6 years ago
|
* `BookAppService` is derived from `AsyncCrudAppService<...>` which implements all the CRUD methods defined above.
|
||
7 years ago
|
* `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](../../Repositories.md).
|
||
6 years ago
|
* `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 object mapping provider. You defined mappings using the `AutoMapFrom` and the `AutoMapTo` attributes above. See the [AutoMapper integration document](../../AutoMapper-Integration.md) for details.
|
||
7 years ago
|
|
||
|
### Auto API Controllers
|
||
|
|
||
6 years ago
|
You normally create **Controllers** to expose application services as **HTTP API** endpoints. Thus allowing browser or 3rd-party clients to call them via AJAX.
|
||
7 years ago
|
|
||
7 years ago
|
ABP can **automagically** configures your application services as MVC API Controllers by convention.
|
||
7 years ago
|
|
||
|
#### Swagger UI
|
||
|
|
||
7 years ago
|
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 `http://localhost:53929/swagger/` as URL on your browser.
|
||
7 years ago
|
|
||
7 years ago
|
You will see some built-in service endpoints as well as the `Book` service and its REST-style endpoints:
|
||
7 years ago
|
|
||
7 years ago
|
![bookstore-swagger](images/bookstore-swagger.png)
|
||
7 years ago
|
|
||
|
### 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
|
||
|
|
||
6 years ago
|
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:
|
||
7 years ago
|
|
||
|
````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` 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.
|
||
|
|
||
6 years ago
|
Running this code produces the following output:
|
||
7 years ago
|
|
||
7 years ago
|
![bookstore-test-js-proxy-getlist](images/bookstore-test-js-proxy-getlist.png)
|
||
7 years ago
|
|
||
7 years ago
|
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)
|
||
7 years ago
|
|
||
|
Let's **create a new book** using the `create` function:
|
||
|
|
||
|
````js
|
||
7 years ago
|
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); });
|
||
7 years ago
|
````
|
||
|
|
||
|
You should see a message in the console something like that:
|
||
|
|
||
|
````
|
||
|
successfully created the book with id: f3f03580-c1aa-d6a9-072d-39e75c69f5c7
|
||
|
````
|
||
|
|
||
7 years ago
|
Check the `books` table in the database to see the new book row. You can try `get`, `update` and `delete` functions too.
|
||
7 years ago
|
|
||
7 years ago
|
### Create the Books Page
|
||
7 years ago
|
|
||
7 years ago
|
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.
|
||
7 years ago
|
|
||
7 years ago
|
Create a new `Books` folder under the `Pages` folder of the `Acme.BookStore.Web` project and add a new Razor Page named `Index.html`:
|
||
7 years ago
|
|
||
7 years ago
|
![bookstore-add-index-page](images/bookstore-add-index-page.png)
|
||
7 years ago
|
|
||
7 years ago
|
Open the `Index.cshtml` and change the content as shown below:
|
||
7 years ago
|
|
||
7 years ago
|
````html
|
||
|
@page
|
||
|
@using Acme.BookStore.Pages.Books
|
||
|
@inherits Acme.BookStore.Pages.BookStorePageBase
|
||
|
@model IndexModel
|
||
7 years ago
|
|
||
7 years ago
|
<h2>Books</h2>
|
||
|
````
|
||
|
|
||
6 years ago
|
* 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.
|
||
7 years ago
|
|
||
|
#### 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` project:
|
||
|
|
||
7 years ago
|
![bookstore-localization-files](images/bookstore-localization-files.png)
|
||
7 years ago
|
|
||
|
Open the `en.json` file and add localization texts for `Menu:BookStore` and `Menu:Books` keys:
|
||
|
|
||
|
````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` 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:
|
||
7 years ago
|
|
||
7 years ago
|
![bookstore-menu-items](images/bookstore-menu-items.png)
|
||
7 years ago
|
|
||
7 years ago
|
When you click to the Books menu item, you are redirected to the new Books page.
|
||
7 years ago
|
|
||
7 years ago
|
#### Book List
|
||
7 years ago
|
|
||
7 years ago
|
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, 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.
|
||
7 years ago
|
|
||
|
##### Index.cshtml Changes
|
||
|
|
||
|
Change the `Pages/Books/Index.cshtml` as following:
|
||
|
|
||
|
````html
|
||
|
@page
|
||
|
@using Acme.BookStore.Pages.Books
|
||
|
@inherits Acme.BookStore.Pages.BookStorePageBase
|
||
|
@model IndexModel
|
||
|
@section scripts
|
||
|
{
|
||
7 years ago
|
<abp-script src="/Pages/Books/index.js" />
|
||
7 years ago
|
}
|
||
|
<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>
|
||
7 years ago
|
<th>@L["CreationTime"]</th>
|
||
7 years ago
|
</tr>
|
||
|
</thead>
|
||
|
</abp-table>
|
||
|
</abp-card-body>
|
||
|
</abp-card>
|
||
|
````
|
||
|
|
||
|
* `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.
|
||
6 years ago
|
* `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.md).
|
||
7 years ago
|
* You can **localize** the column names in the localization file as you did for the menu items above.
|
||
|
|
||
|
##### Add a Script File
|
||
|
|
||
7 years ago
|
Create `index.js` JavaScript file under the `Pages/Books/` folder:
|
||
7 years ago
|
|
||
7 years ago
|
![bookstore-index-js-file](images/bookstore-index-js-file.png)
|
||
7 years ago
|
|
||
|
`index.js` content is shown below:
|
||
|
|
||
|
````js
|
||
7 years ago
|
$(function () {
|
||
|
var dataTable = $('#BooksTable').DataTable(abp.libs.datatables.normalizeConfiguration({
|
||
7 years ago
|
ajax: abp.libs.datatables.createAjax(acme.bookStore.book.getList),
|
||
|
columnDefs: [
|
||
7 years ago
|
{ data: "name" },
|
||
|
{ data: "type" },
|
||
|
{ data: "publishDate" },
|
||
|
{ data: "price" },
|
||
|
{ data: "creationTime" }
|
||
7 years ago
|
]
|
||
7 years ago
|
}));
|
||
7 years ago
|
});
|
||
|
````
|
||
|
|
||
|
* `abp.libs.datatables.createAjax` is a helper function to adapt ABP's dynamic JavaScript API proxies to Datatable's format.
|
||
6 years ago
|
* `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.
|
||
7 years ago
|
* `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:
|
||
|
|
||
7 years ago
|
![bookstore-book-list](images/bookstore-book-list.png)
|
||
7 years ago
|
|
||
|
### Next Part
|
||
|
|
||
6 years ago
|
See the [next part](Part-II.md) of this tutorial.
|