使用 ASP.NET Core, Entity Framework Core 和 ABP 创建N层Web应用 第二篇
介紹
這是“使用 ASP.NET Core ,Entity Framework Core 和 ASP.NET Boilerplate 創建N層 Web 應用”系列文章的第二篇。以下可以看其他篇目:
使用 ASP.NET Core ,Entity Framework Core 和 ASP.NET Boilerplate 創建N層 Web 應用 ?第一篇 (翻譯版本鏈接)
應用開發
創建 Person 實體
我們將任務分配給具體的人,所以添加一個責任人的概念。我們定義一個簡單的 Person 實體。
代碼如下
[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;
? ? }
}
這一次,我們作為示范,將 Id (主鍵)設置為 Guid 類型。同時,這次不從 base Entity 繼承,而是從 AuditedEntity 繼承 (該類定義了多個常用屬性 創建時間?CreationTime, 創建者用戶Id CreaterUserId, 最后修改時間 LastModificationTime 和最后修改人Id LastModifierUserId?)
?
關聯 Person 到 Task 實體
我們同時將 責任人 AssignedPerson 屬性添加到 任務 Task 實體中(如下代碼只粘貼修改的部分)
代碼如下
[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 是可選的。所以,任務可以指派給責任人或者不指派
?
添加 Person 到 數據庫上下文 DbContext
最后,我們添加新的責任人 Person 實體到 DbContext 類中:
代碼如下
public class SimpleTaskAppDbContext : AbpDbContext
{
? ? public DbSet<Person> People { get; set; }
? ??
? ? //...
}
添加 Person 實體的新遷移文件
現在,我們在 源包管理控制臺 Package Manager Console 中執行遷移命令,如圖所示
該命令將會在項目里創建新的數據遷移類。
代碼如下
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);
? ? }
? ? //...
}
該類為自動生成的,我們只是將 ReferentialAction.Restrict 修改為 ReferentialAction.SetNull 。它的作用是:當我們刪除一個責任人的時候,分配給這個人的任務會變成為分配。在這個 demo 里,這并不重要。我們只是想告訴你,如果有必要的話,遷移類的代碼是可以修改的。實際上,我們總是應該在執行到數據庫之前,重新閱讀生成的代碼。
之后,我們可以對我們的數據庫執行遷移了。如下圖:(更多遷移相關信息請參照 ?entity framework documentation?)
當我們打開數據庫的時候,我們可以看到表和字段都已經創建完畢了,我們可以添加一些測試數據。如下圖:
我們添加一個責任人并分配第一個任務給他。如下圖:
?
返回任務列表中的責任人 Person
我們將修改 TaskAppService ,使之可以返回責任人信息。首先,我們在 TaskListDto 中添加2個屬性。
代碼如下 (只顯示有變動的代碼,如需看完整代碼請參考第一篇,下同)
[AutoMapFrom(typeof(Task))]
public class TaskListDto : EntityDto, IHasCreationTime
{
? ? //...
? ? public Guid? AssignedPersonId { get; set; }
? ? public string AssignedPersonName { get; set; }
}
同時將 Task.AssignedPerson 屬性添加到查詢里,僅添加 Include 行即可
代碼如下
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)
? ? ? ? );
? ? }
}
這樣, GetAll 方法會返回任務及相關的責任人信息。由于我們使用了 AutoMapper , 新的屬性也會自動添加到 DTO 里。
?
單元測試責任人 Person
在這里,我們修改單元測試,(對測試不感興趣者可直接跳過)看看獲取任務列表時是否能獲取到責任人。首先,我們修改 TestDataBuilder 類里的初始化測試數據,分配任務給責任人。
代碼如下
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 }
? ? ? ? ? ? );
? ? }
}
然后我們修改 TaskAppService_Tests.Should_Get_All_Tasks() 方法,檢查是否有一個任務已經指派了責任人(請看代碼最后一行)
代碼如下
[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);
}
友情提示:擴張方法 Count 需要使用 using System.Linq 語句。
?
在任務列表頁展示責任人的名字
最后,我們修改 Task\Index.cshtml 來展示 責任人的名字 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>
}
啟動程序,我們可以看到任務列表入下圖:
?
任務創建的新應用服務方法
現在我們可以展示所有的任務,但是我們卻還沒有一個任務創建頁面。首先,在 ITaskAppService 接口里添加一個 Create 方法。
代碼如下
public interface ITaskAppService : IApplicationService
{
? ? //...
? ? System.Threading.Tasks.Task Create(CreateTaskInput input);
}
然后在 TaskAppService 類里實現它
代碼如下
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 方法會自動映射輸入參數 input 到task 實體,之后我們使用倉儲 repository 來將任務實體插入數據庫中。讓我們來看看輸入參數 input 的 CreateTaskInput DTO 。
代碼如下
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; }
? ? }
}
我們將DTO配置為映射到任務 Task 實體(使用 AutoMap 特性),同時添加數據驗證 validation?。我們使用任務 Task 實體的常量來同步設置最大字串長度。?
?
測試任務創建服務
我們添加 TaskAppService_Tests 類的集成測試來測試 Create 方法:(如果對測試不感興趣者可以跳過這個部分)
代碼如下
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
? ? ? ? ? ? ? ? });
? ? ? ? ? ? });
? ? ? ? }
? ? }
}
第一個測試創建了一個帶 title 的任務, 第二個測試創建了一個帶 title 和 責任人 的測試,最后一個測試創建了一個無效的任務來展示 exception 例子。
?
任務創建頁面
我們現在知道 TaskAppService.Create 方法可以正常工作了。現在,我們可以創建一個頁面來添加新任務了。完成后的效果如下圖所示:
首先,我們在任務控制器 TaskController 添加一個 Create action 。
代碼如下
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));
? ? ? ? }
? ? }
}
我們將 ILookupAppService 反射進來,這樣可以獲取責任人下拉框的項目。本來我們是可以直接反射使用 IRepository<Person,Guid> 的,但是為了更好的分層和復用,我們還是使用 ILookUpAppService 。ILookupAppService.GetPeopleComboboxItems 在應用層的定義如下:
代碼如下
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 是一個簡單的類(在 ABP 中定義),用于傳遞下拉框 Combobox 的項目的數據。 TaskController.Create 方法僅使用了這個方法并將返回的列表轉換為 SelectListItem (在 AspNet Core 中定義),然后用 CreateTaskViewModel 返回給視圖。
代碼如下
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 視圖如下:
代碼如下
@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>
我們編寫 create.js 如下:
代碼如下
(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);
讓我們一起來看看這個 javascript 代碼都做了什么:
在表單里預先做好驗證(使用 jquery validation?插件)準備,并在保存 Save 按鈕被點擊后進行驗證。
使用序列化表格為對象 serializeFormToObject 插件 (在解決方案中的 jquery-extensions.js 中定義), 將表格數據 forum data 轉換為 JSON 對象(我們將 jquery-extensions.js 添加到最后的腳本文件 _Layout.cshtml )
使用 abp.services.task.create 方法調用 TaskAppService.Create 方法。這是 ABP 中的一個很重要的特性。我們可以在 javascript 代碼中使用應用服務,簡單的就想在代碼里直接調用 javascript 方法 (詳情請見 ?details)
最后,我們在任務列表頁面里添加一個 “添加任務 Add Task”按鈕,點擊后就可以導航到任務創建頁面:
代碼如下
1 <a class="btn btn-primary btn-sm" asp-action="Create">@L("AddNew")</a>刪除主頁和關于頁
如果我們不需要主頁和關于頁,我們可以從應用里刪除掉它們。首先,刪除主頁控制器 HomeController :
代碼如下
using Microsoft.AspNetCore.Mvc;
namespace Acme.SimpleTaskApp.Web.Controllers
{
? ? public class HomeController : SimpleTaskAppControllerBase
? ? {
? ? ? ? public ActionResult Index()
? ? ? ? {
? ? ? ? ? ? return RedirectToAction("Index", "Tasks");
? ? ? ? }
? ? }
}
然后刪除 視圖里的主頁 Views/Home 文件夾并從 SimpleTaskAppNavigationProvider 類里刪除菜單項。我們也可以從本地化 JSON 文件中刪除點不需要的關鍵詞。
?
其他相關內容
我們將不斷改進本篇內容
從任務列表里打開/關閉任務,然后刷新任務項目。
為責任人下拉框使用組件
等等
?
文章更改歷史
2017-07-30:將文章中的 ListResultOutput 替換為 ListResultDto
2017-06-02:將項目和文章修改為支持 .net core
2016-08-09:根據反饋修改文章
2016-08-08:初次發布文章
相關文章:
手把手引進門之 ASP.NET Core & Entity Framework Core(官方教程翻譯版 版本3.2.5)
ABP .Net Core Entity Framework遷移使用MySql數據庫
[52ABP實戰系列] .NET CORE實戰入門第三章更新了
ABP從入門到精通(1):aspnet-zero-core項目啟動及各項目源碼說明
ABP從入門到精通(2):aspnet-zero-core 使用MySql數據庫
ABP從入門到精通(3):aspnet-zero-core 使用Redis緩存
ABP從入門到精通(4):使用基于JWT標準的Token訪問WebApi
ABP從入門到精通(5):.擴展國際化語言資源
原文地址:http://www.cnblogs.com/yabu007/p/8117792.html
.NET社區新聞,深度好文,歡迎訪問公眾號文章匯總 http://www.csharpkit.com
總結
以上是生活随笔為你收集整理的使用 ASP.NET Core, Entity Framework Core 和 ABP 创建N层Web应用 第二篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 利用VSTS跟Kubernetes整合进
- 下一篇: 揭秘微软6万工程师DevOps成功转型的