Click here to Skip to main content
15,867,488 members
Articles / Web Development / HTML

Using ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate to Create NLayered Web Application (Part II)

Rate me:
Please Sign up or sign in to vote.
4.91/5 (18 votes)
13 Feb 2018CPOL5 min read 54K   420   35   8
A step by step guide to create a layered web application based on ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate frameworks with automated tests.

Contents

Introduction

This is the second part of the "Using ASP.NET Core, Entity Framework Core and ASP.NET Boilerplate to Create NLayered Web Application" article series. See other parts:

Developing the Application

Creating the Person Entity

I'll add Person concept to the application to assign tasks to people. So, I define a simple Person entity:

C#
[Table("AppPersons")]
public class Person : AuditedEntity<Guid>
{
    public const int MaxNameLength = 32;

    [Required]
    [MaxLength(MaxNameLength)]
    public string Name { get; set; }

    public Person()
    {
            
    }

    public Person(string name)
    {
        Name = name;
    }
}

This time, I set Id (primary key)  type as Guid, for demonstration. I also derived from AuditedEntity (which has CreationTime, CreaterUserId, LastModificationTime and LastModifierUserId properties) instead of base Entity class.

Relating Person to the Task Entity

I'm also adding AssignedPerson property to the Task entity (only sharing the changed parts here):

C#
[Table("AppTasks")]
public class Task : Entity, IHasCreationTime
{
    //...

    [ForeignKey(nameof(AssignedPersonId))]
    public Person AssignedPerson { get; set; }
    public Guid? AssignedPersonId { get; set; }

    public Task(string title, string description = null, Guid? assignedPersonId = null)
        : this()
    {
        Title = title;
        Description = description;
        AssignedPersonId = assignedPersonId;
    }
}

AssignedPerson is optional. So, as task can be assigned to a person or can be unassigned.

Adding Person to DbContext

Finally, I'm adding new Person entity to the DbContext class:

C#
public class SimpleTaskAppDbContext : AbpDbContext
{
    public DbSet<Person> People { get; set; }
    
    //...
}

Adding a New Migration for Person Entity

Now, I'm running the following command in the Package Manager Console:

Add new migration

And it creates a new migration class in the project:

C#
public partial class Added_Person : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "AppPersons",
            columns: table => new
            {
                Id = table.Column<Guid>(nullable: false),
                CreationTime = table.Column<DateTime>(nullable: false),
                CreatorUserId = table.Column<long>(nullable: true),
                LastModificationTime = table.Column<DateTime>(nullable: true),
                LastModifierUserId = table.Column<long>(nullable: true),
                Name = table.Column<string>(maxLength: 32, nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_AppPersons", x => x.Id);
            });

        migrationBuilder.AddColumn<Guid>(
            name: "AssignedPersonId",
            table: "AppTasks",
            nullable: true);

        migrationBuilder.CreateIndex(
            name: "IX_AppTasks_AssignedPersonId",
            table: "AppTasks",
            column: "AssignedPersonId");

        migrationBuilder.AddForeignKey(
            name: "FK_AppTasks_AppPersons_AssignedPersonId",
            table: "AppTasks",
            column: "AssignedPersonId",
            principalTable: "AppPersons",
            principalColumn: "Id",
            onDelete: ReferentialAction.SetNull);
    }

    //...
}

I just changed ReferentialAction.Restrict to ReferentialAction.SetNull. It does that: if I delete a person, assigned tasks to that person become unassigned. This is not important in this demo. But I wanted to show that you can change the migration code if you need. Actually, you always review the generated code before applying it to the database. After that, we can apply migration to our database:

Update-Database

When we open the database, we can see the new table and columns and add some test data:

Person table

I added a person and assigned to the first task:

Tasks table

Return Assigned Person in the Task List

I'll change the TaskAppService to return assigned person information. First, I'm adding two properties to TaskListDto:

C#
[AutoMapFrom(typeof(Task))]
public class TaskListDto : EntityDto, IHasCreationTime
{
    //...

    public Guid? AssignedPersonId { get; set; }

    public string AssignedPersonName { get; set; }
}

And including the Task.AssignedPerson property to the query. Just added the Include line:

C#
public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService
{
    //...

    public async Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input)
    {
        var tasks = await _taskRepository
            .GetAll()
            .Include(t => t.AssignedPerson)
            .WhereIf(input.State.HasValue, t => t.State == input.State.Value)
            .OrderByDescending(t => t.CreationTime)
            .ToListAsync();

        return new ListResultDto<TaskListDto>(
            ObjectMapper.Map<List<TaskListDto>>(tasks)
        );
    }
}

Thus, GetAll method will return Assigned person information with the tasks. Since we used AutoMapper, new properties will also be copied to DTO automatically.

Change Unit Test to Test Assigned Person

At this point, we can change unit tests to see if assigned people are retrieved while getting the task list. First, I changed initial test data in the TestDataBuilder class to assign a person to a task:

C#
public class TestDataBuilder
{
    //...

    public void Build()
    {
        var neo = new Person("Neo");
        _context.People.Add(neo);
        _context.SaveChanges();

        _context.Tasks.AddRange(
            new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality.", neo.Id),
            new Task("Clean your room") { State = TaskState.Completed }
            );
    }
}

Then I'm changing TaskAppService_Tests.Should_Get_All_Tasks() method to check if one of the retrieved tasks has a person assigned (see the last line added):

C#
[Fact]
public async System.Threading.Tasks.Task Should_Get_All_Tasks()
{
    //Act
    var output = await _taskAppService.GetAll(new GetAllTasksInput());

    //Assert
    output.Items.Count.ShouldBe(2);
    output.Items.Count(t => t.AssignedPersonName != null).ShouldBe(1);
}

Note: Count extension method requires using System.Linq; statement.

Show Assigned Person Name in the Task List Page

Finally, we can change Tasks\Index.cshtml to show AssignedPersonName:

XML
@foreach (var task in Model.Tasks)
{
    <li class="list-group-item">
        <span class="pull-right label label-lg @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span>
        <h4 class="list-group-item-heading">@task.Title</h4>
        <div class="list-group-item-text">
            @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") | @(task.AssignedPersonName ?? L("Unassigned"))
        </div>
    </li>
}

When we run the application, we can see it in the task list:

Task list with person name

New Application Service Method for Task Creation

We can list tasks, but we don't have a task creation page yet. First, adding a Create method to the ITaskAppService interface:

C#
public interface ITaskAppService : IApplicationService
{
    //...

    System.Threading.Tasks.Task Create(CreateTaskInput input);
}

And implementing it in TaskAppService class:

C#
public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService
{
    private readonly IRepository<Task> _taskRepository;

    public TaskAppService(IRepository<Task> taskRepository)
    {
        _taskRepository = taskRepository;
    }

    //...

    public async System.Threading.Tasks.Task Create(CreateTaskInput input)
    {
        var task = ObjectMapper.Map<Task>(input);
        await _taskRepository.InsertAsync(task);
    }
}

Create method automatically maps given input to a Task entity and inserting to the database using the repository. CreateTaskInput DTO is like that:

C#
using System;
using System.ComponentModel.DataAnnotations;
using Abp.AutoMapper;

namespace Acme.SimpleTaskApp.Tasks.Dtos
{
    [AutoMapTo(typeof(Task))]
    public class CreateTaskInput
    {
        [Required]
        [MaxLength(Task.MaxTitleLength)]
        public string Title { get; set; }

        [MaxLength(Task.MaxDescriptionLength)]
        public string Description { get; set; }

        public Guid? AssignedPersonId { get; set; }
    }
}

Configured to map it to Task entity (using AutoMapTo attribute) and added data annotations to apply validation. We used constants from Task entity to use same max lengths.

Testing Task Creation Service

I'm adding some integration tests into TaskAppService_Tests class to test the Create method:

C#
using Acme.SimpleTaskApp.Tasks;
using Acme.SimpleTaskApp.Tasks.Dtos;
using Shouldly;
using Xunit;
using System.Linq;
using Abp.Runtime.Validation;

namespace Acme.SimpleTaskApp.Tests.Tasks
{
    public class TaskAppService_Tests : SimpleTaskAppTestBase
    {
        private readonly ITaskAppService _taskAppService;

        public TaskAppService_Tests()
        {
            _taskAppService = Resolve<ITaskAppService>();
        }

        //...

        [Fact]
        public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title()
        {
            await _taskAppService.Create(new CreateTaskInput
            {
                Title = "Newly created task #1"
            });

            UsingDbContext(context =>
            {
                var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1");
                task1.ShouldNotBeNull();
            });
        }

        [Fact]
        public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title_And_Assigned_Person()
        {
            var neo = UsingDbContext(context => context.People.Single(p => p.Name == "Neo"));

            await _taskAppService.Create(new CreateTaskInput
            {
                Title = "Newly created task #1",
                AssignedPersonId = neo.Id
            });

            UsingDbContext(context =>
            {
                var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1");
                task1.ShouldNotBeNull();
                task1.AssignedPersonId.ShouldBe(neo.Id);
            });
        }

        [Fact]
        public async System.Threading.Tasks.Task Should_Not_Create_New_Task_Without_Title()
        {
            await Assert.ThrowsAsync<AbpValidationException>(async () =>
            {
                await _taskAppService.Create(new CreateTaskInput
                {
                    Title = null
                });
            });
        }
    }
}

First test creates a task with a title, second one creates a task with a title and assigned person, the last one tries to create an invalid task to show the exception case.

Task Creation Page

We know that TaskAppService.Create is properly working. Now, we can create a page to add a new task. Final page will be like that:

Create task page

First, I added a Create action to the TaskController in order to prepare the page above:

C#
using System.Threading.Tasks;
using Abp.Application.Services.Dto;
using Acme.SimpleTaskApp.Tasks;
using Acme.SimpleTaskApp.Tasks.Dtos;
using Acme.SimpleTaskApp.Web.Models.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Linq;
using Acme.SimpleTaskApp.Common;
using Acme.SimpleTaskApp.Web.Models.People;

namespace Acme.SimpleTaskApp.Web.Controllers
{
    public class TasksController : SimpleTaskAppControllerBase
    {
        private readonly ITaskAppService _taskAppService;
        private readonly ILookupAppService _lookupAppService;

        public TasksController(
            ITaskAppService taskAppService,
            ILookupAppService lookupAppService)
        {
            _taskAppService = taskAppService;
            _lookupAppService = lookupAppService;
        }

        //...
        
        public async Task<ActionResult> Create()
        {
            var peopleSelectListItems = (await _lookupAppService.GetPeopleComboboxItems()).Items
                .Select(p => p.ToSelectListItem())
                .ToList();

            peopleSelectListItems.Insert(0, new SelectListItem { Value = string.Empty, Text = L("Unassigned"), Selected = true });

            return View(new CreateTaskViewModel(peopleSelectListItems));
        }
    }
}

I injected ILookupAppService that is used to get people combobox items. While I could directly inject and use IRepository<Person, Guid> here, I prefered this to make a better layering and re-usability. ILookupAppService.GetPeopleComboboxItems is defined in application layer as shown below:

C#
public interface ILookupAppService : IApplicationService
{
    Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems();
}

public class LookupAppService : SimpleTaskAppAppServiceBase, ILookupAppService
{
    private readonly IRepository<Person, Guid> _personRepository;

    public LookupAppService(IRepository<Person, Guid> personRepository)
    {
        _personRepository = personRepository;
    }

    public async Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems()
    {
        var people = await _personRepository.GetAllListAsync();
        return new ListResultDto<ComboboxItemDto>(
            people.Select(p => new ComboboxItemDto(p.Id.ToString("D"), p.Name)).ToList()
        );
    }
}

ComboboxItemDto is a simple class (defined in ABP) to transfer a combobox item data. TaskController.Create method simply uses this method and converts the returned list to a list of SelectListItem (defined in AspNet Core) and passes to the view using CreateTaskViewModel class:

C#
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace Acme.SimpleTaskApp.Web.Models.People
{
    public class CreateTaskViewModel
    {
        public List<SelectListItem> People { get; set; }

        public CreateTaskViewModel(List<SelectListItem> people)
        {
            People = people;
        }
    }
}

Create view is shown below:

XML
@using Acme.SimpleTaskApp.Web.Models.People
@model CreateTaskViewModel

@section scripts
{
    <environment names="Development">
        <script src="~/js/views/tasks/create.js"></script>
    </environment>

    <environment names="Staging,Production">
        <script src="~/js/views/tasks/create.min.js"></script>
    </environment>
}

<h2>
    @L("NewTask")
</h2>

<form id="TaskCreationForm">
    
    <div class="form-group">
        <label for="Title">@L("Title")</label>
        <input type="text" name="Title" class="form-control" placeholder="@L("Title")" required maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxTitleLength">
    </div>

    <div class="form-group">
        <label for="Description">@L("Description")</label>
        <input type="text" name="Description" class="form-control" placeholder="@L("Description")" maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxDescriptionLength">
    </div>

    <div class="form-group">
        @Html.Label(L("AssignedPerson"))
        @Html.DropDownList(
            "AssignedPersonId",
            Model.People,
            new
            {
                @class = "form-control",
                id = "AssignedPersonCombobox"
            })
    </div>

    <button type="submit" class="btn btn-default">@L("Save")</button>

</form>

I included create.js defined like that:

JavaScript
(function($) {
    $(function() {

        var _$form = $('#TaskCreationForm');

        _$form.find('input:first').focus();

        _$form.validate();

        _$form.find('button[type=submit]')
            .click(function(e) {
                e.preventDefault();

                if (!_$form.valid()) {
                    return;
                }

                var input = _$form.serializeFormToObject();
                abp.services.app.task.create(input)
                    .done(function() {
                        location.href = '/Tasks';
                    });
            });
    });
})(jQuery);

Let's see what's done in this javascript code:

  • Prepares validatation for the form (using jquery validation plugin) and validates it on Save button's click.
  • Uses serializeFormToObject jquery plugin (defined in jquery-extensions.js in the solution) to convert forum data to a JSON object (I included jquery-extensions.js to the _Layout.cshtml as the last script file).
  • Uses abp.services.task.create method to call TaskAppService.Create method. This is one of the important features of ABP. We can use application services from javascript code just like calling a javascript method in our code. See details.

Finally, I added an "Add Task" button to the task list page in order to navigate to the task creation page:

XML
<a class="btn btn-primary btn-sm" asp-action="Create">@L("AddNew")</a>

Remove Home and About Page

We can remove Home and About page from the application if we don't need. To do that, first change HomeController like that:

C#
using Microsoft.AspNetCore.Mvc;

namespace Acme.SimpleTaskApp.Web.Controllers
{
    public class HomeController : SimpleTaskAppControllerBase
    {
        public ActionResult Index()
        {
            return RedirectToAction("Index", "Tasks");
        }
    }
}

Then delete Views/Home folder and remove menu items from SimpleTaskAppNavigationProvider class. You can also remove unnecessary keys from localization JSON files.

Source Code

You can get the latest source code here https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/SimpleTaskSystem-Core

Article History

  • 2018-02-14: Upgraded source code to ABP v3.4 and updated the download link.
  • 2017-07-30: Replaced ListResultOutput by ListResultDto in the article.
  • 2017-06-02: Changed article and solution to support .net core.
  • 2016-08-09: Revised article based on feedbacks.
  • 2016-08-08: Initial publication.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Founder Volosoft
Turkey Turkey
I have started programming at 14 years old using Pascal as hobby. Then I interested in web development (HTML, JavaScript, ASP...) before university.

I graduated from Sakarya University Computer Engineering. At university, I learned C++, Visual Basic.NET, C#, ASP.NET and Java. I partly implemented ARP, IP and TCP protocols in Java as my final term project.

Now, I am working on Windows and web based software development mostly using Microsoft technologies in my own company.

My open source projects:

* ABP Framework: https://abp.io
* jTable: http://jtable.org
* Others: https://github.com/hikalkan

My personal web site:

https://halilibrahimkalkan.com

Comments and Discussions

 
Question[__strong__] ? Pin
exiled783-Jun-17 21:32
exiled783-Jun-17 21:32 
AnswerRe: [__strong__] ? Pin
Halil ibrahim Kalkan4-Jun-17 8:43
Halil ibrahim Kalkan4-Jun-17 8:43 
BugListResultOutput -> ListResultDto Pin
BAYAS502-Jun-17 14:26
BAYAS502-Jun-17 14:26 
GeneralRe: ListResultOutput -> ListResultDto Pin
Halil ibrahim Kalkan30-Jul-17 3:28
Halil ibrahim Kalkan30-Jul-17 3:28 
PraiseThank you! Pin
Satal Keto25-Nov-16 5:32
Satal Keto25-Nov-16 5:32 
QuestionQuestion about System.Threading.Tasks Pin
JoshYates198019-Oct-16 9:31
professionalJoshYates198019-Oct-16 9:31 
QuestionReally Nice Pin
Sampath Lokuge8-Aug-16 4:37
Sampath Lokuge8-Aug-16 4:37 
AnswerRe: Really Nice Pin
Halil ibrahim Kalkan8-Aug-16 23:51
Halil ibrahim Kalkan8-Aug-16 23:51 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.