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

6
482

Contents

Introduction

This is 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:

[__strong__][Table("AppPersons")] public class Person : AuditedEntity { 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):

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

public class SimpleTaskAppDbContext : AbpDbContext { public DbSet 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:

public partial class Added_Person : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "AppPersons", columns: table => new { Id = table.Column(nullable: false), CreationTime = table.Column(nullable: false), CreatorUserId = table.Column(nullable: true), LastModificationTime = table.Column(nullable: true), LastModifierUserId = table.Column(nullable: true), Name = table.Column(maxLength: 32, nullable: false) }, constraints: table => { table.PrimaryKey("PK_AppPersons", x => x.Id); }); migrationBuilder.AddColumn( 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:

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

public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService { public async Task<ListResultOutput> 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 ListResultOutput( ObjectMapper.Map<List>(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:

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

[Fact] public async System.Threading.Tasks.Task Should_Get_All_Tasks() { var output = await _taskAppService.GetAll(new GetAllTasksInput()); 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:

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

public interface ITaskAppService : IApplicationService { System.Threading.Tasks.Task Create(CreateTaskInput input); }

And implementing it in TaskAppService class:

public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService { private readonly IRepository _taskRepository; public TaskAppService(IRepository taskRepository) { _taskRepository = taskRepository; }  public async System.Threading.Tasks.Task Create(CreateTaskInput input) { var task = ObjectMapper.Map(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:

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:

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(); } [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(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:

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 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 here, I prefered this to make a better layering and re-usability. ILookupAppService.GetPeopleComboboxItems is defined in application layer as shown below:

public interface ILookupAppService : IApplicationService { Task<ListResultOutput> GetPeopleComboboxItems(); } public class LookupAppService : SimpleTaskAppAppServiceBase, ILookupAppService { private readonly IRepository _personRepository; public LookupAppService(IRepository personRepository) { _personRepository = personRepository; }  public async Task<ListResultOutput> GetPeopleComboboxItems() { var people = await _personRepository.GetAllListAsync(); return new ListResultOutput( 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:

using System.Collections.Generic; using Microsoft.AspNetCore.Mvc.Rendering; namespace Acme.SimpleTaskApp.Web.Models.People { public class CreateTaskViewModel { public List People { get; set; } public CreateTaskViewModel(List people) { People = people; } } }

Create view is shown below:

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

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

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

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.

More

I’ll improve this article by adding

  • Open/Close tasks from the task list and refresh the task item.
  • Use component for person combobox.

Article History

  • 2017-06-02: Changed article and solution to support .net core.
  • 2016-08-09: Revised article based on feedbacks.
  • 2016-08-08: Initial publication.

6 COMMENTS

  1. It’s not my first time to pay a visit this web page, i am browsing this web site dailly
    and get fastidious data from here everyday.

  2. I am sure this paragraph has touched all the internet viewers, its really really fastidious article on building up new web site.

  3. Hallo!

    Ich frage mich momentan, welches Abnehm-Produkt für mich in Frage kommt.
    Was auch immer ich bisher ausprobiert habe, war durchweg ineffektiv.

    Auf was soll man da am Besten denken? Produkte, die zwar jede Menge zusichern, am Schluss aber nichts dabei herauskommt, reizen mich kein bisschen. Deshalb
    nicht falsch interpretieren, ich will keine Wunderwaffe, aber
    es muss doch irgendwelche Produkte geben, die mindestens
    ein bisschen helfen.

    Über Ideen oder Infos würde ich mich ziemlich freuen

    Liebe Grüße
    Katharina

  4. Every weekend i used to visit this website, because i wish for enjoyment, as this this web site conations truly good funny data too.

LEAVE A REPLY