使用 ASP.NET Core, Entity Framework Core 和 ABP 创建N层Web应用 第二篇
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 ASP.NET Core, Entity Framework Core 和 ABP 创建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 语句。
在任务列表页展示责任人的名字
最后,我们修改 TaskIndex.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 ></script>
</environment>
<environment names="Staging,Production">
<script ></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 和 ABP 创建N层Web应用 第二篇的主要内容,如果未能解决你的问题,请参考以下文章
C# ASP.NET Core Entity Framework Core 异步 ToQueryable 比较
ASP.NET Core 和 Entity Framework Core:Linq 中的左(外)连接
使用 ASP.NET Core, Entity Framework Core 和 ABP 创建N层Web应用 第二篇
更新 ASP.NET Core Entity Framework 中的实体类
这是如何使用 Entity Framework Core 和 ASP.NET Core MVC 2.2+ 和 3.0 创建数据传输对象 (DTO)
使用 ASP.NET Core 和 Entity Framework Core 进行集成测试 - 如何在每次测试时恢复数据库中的测试数据?