ABP 教程文档 1-1 手把手引进门之 ASP.NET Core & Entity Framework Core(官方教程翻译版 版本3.2.5)第二篇
官方文档分四部分
一、 教程文档
二、ABP 框架
三、zero 模块
四、其他(中文翻译资源)
本篇是第一部分的第一篇。
第一部分分三篇
1-1 手把手引进门 第一篇
1-2 进阶
1-3 杂项 (相关理论知识)
第一篇含两个步骤。
1-1-1 ASP.NET Core & Entity Framework Core 后端(内核)
1-1-2 ASP.NET MVC, Web API, EntityFramework & AngularJs 前端
现在进入正文
使用 ASP.NET Core, Entity Framework Core 和 ASP.NET Boilerplate 创建N层Web应用 第二篇
土牛语录:
以下是手把手引进门教程,基于 ASP.NET Core, Entity Framework Core ,ABP 框架 创建Web 应用, PS: 自带自动的测试模块哦。
本文目录如下:
介绍
应用开发
创建 Person 实体
关联 Person 到 Task 实体
添加 Person 到 数据库上下文 DbContext
添加 Person 实体的新迁移文件
返回任务列表中的责任人 Person
单元测试责任人 Person
在任务列表页展示责任人的名字
任务创建的新应用服务方法
测试任务创建服务
任务创建页面
删除主页和关于页
其他相关内容
文章更改历史
版权所有
介绍
这是“使用 ASP.NET Core ,Entity Framework Core 和 ASP.NET Boilerplate 创建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 实体。
代码如下
1 [Table("AppPersons")] 2 public class Person : AuditedEntity<Guid> 3 { 4 public const int MaxNameLength = 32; 5 6 [Required] 7 [MaxLength(MaxNameLength)] 8 public string Name { get; set; } 9 10 public Person() 11 { 12 13 } 14 15 public Person(string name) 16 { 17 Name = name; 18 } 19 }
这一次,我们作为示范,将 Id (主键)设置为 Guid 类型。同时,这次不从 base Entity 继承,而是从 AuditedEntity 继承 (该类定义了多个常用属性 创建时间 CreationTime, 创建者用户Id CreaterUserId, 最后修改时间 LastModificationTime 和最后修改人Id LastModifierUserId )
关联 Person 到 Task 实体
我们同时将 责任人 AssignedPerson 属性添加到 任务 Task 实体中(如下代码只粘贴修改的部分)
代码如下
1 [Table("AppTasks")] 2 public class Task : Entity, IHasCreationTime 3 { 4 //... 5 6 [ForeignKey(nameof(AssignedPersonId))] 7 public Person AssignedPerson { get; set; } 8 public Guid? AssignedPersonId { get; set; } 9 10 public Task(string title, string description = null, Guid? assignedPersonId = null) 11 : this() 12 { 13 Title = title; 14 Description = description; 15 AssignedPersonId = assignedPersonId; 16 } 17 }
责任人 AssignedPerson 是可选的。所以,任务可以指派给责任人或者不指派
添加 Person 到 数据库上下文 DbContext
最后,我们添加新的责任人 Person 实体到 DbContext 类中:
代码如下
1 public class SimpleTaskAppDbContext : AbpDbContext 2 { 3 public DbSet<Person> People { get; set; } 4 5 //... 6 }
添加 Person 实体的新迁移文件
现在,我们在 源包管理控制台 Package Manager Console 中执行迁移命令,如图所示
该命令将会在项目里创建新的数据迁移类。
代码如下
1 public partial class Added_Person : Migration 2 { 3 protected override void Up(MigrationBuilder migrationBuilder) 4 { 5 migrationBuilder.CreateTable( 6 name: "AppPersons", 7 columns: table => new 8 { 9 Id = table.Column<Guid>(nullable: false), 10 CreationTime = table.Column<DateTime>(nullable: false), 11 CreatorUserId = table.Column<long>(nullable: true), 12 LastModificationTime = table.Column<DateTime>(nullable: true), 13 LastModifierUserId = table.Column<long>(nullable: true), 14 Name = table.Column<string>(maxLength: 32, nullable: false) 15 }, 16 constraints: table => 17 { 18 table.PrimaryKey("PK_AppPersons", x => x.Id); 19 }); 20 21 migrationBuilder.AddColumn<Guid>( 22 name: "AssignedPersonId", 23 table: "AppTasks", 24 nullable: true); 25 26 migrationBuilder.CreateIndex( 27 name: "IX_AppTasks_AssignedPersonId", 28 table: "AppTasks", 29 column: "AssignedPersonId"); 30 31 migrationBuilder.AddForeignKey( 32 name: "FK_AppTasks_AppPersons_AssignedPersonId", 33 table: "AppTasks", 34 column: "AssignedPersonId", 35 principalTable: "AppPersons", 36 principalColumn: "Id", 37 onDelete: ReferentialAction.SetNull); 38 } 39 40 //... 41 }
该类为自动生成的,我们只是将 ReferentialAction.Restrict 修改为 ReferentialAction.SetNull 。它的作用是:当我们删除一个责任人的时候,分配给这个人的任务会变成为分配。在这个 demo 里,这并不重要。我们只是想告诉你,如果有必要的话,迁移类的代码是可以修改的。实际上,我们总是应该在执行到数据库之前,重新阅读生成的代码。
之后,我们可以对我们的数据库执行迁移了。如下图:(更多迁移相关信息请参照 entity framework documentation )
当我们打开数据库的时候,我们可以看到表和字段都已经创建完毕了,我们可以添加一些测试数据。如下图:
我们添加一个责任人并分配第一个任务给他。如下图:
返回任务列表中的责任人 Person
我们将修改 TaskAppService ,使之可以返回责任人信息。首先,我们在 TaskListDto 中添加2个属性。
代码如下 (只显示有变动的代码,如需看完整代码请参考第一篇,下同)
1 [AutoMapFrom(typeof(Task))] 2 public class TaskListDto : EntityDto, IHasCreationTime 3 { 4 //... 5 6 public Guid? AssignedPersonId { get; set; } 7 8 public string AssignedPersonName { get; set; } 9 }
同时将 Task.AssignedPerson 属性添加到查询里,仅添加 Include 行即可
代码如下
1 public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService 2 { 3 //... 4 5 public async Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input) 6 { 7 var tasks = await _taskRepository 8 .GetAll() 9 .Include(t => t.AssignedPerson) 10 .WhereIf(input.State.HasValue, t => t.State == input.State.Value) 11 .OrderByDescending(t => t.CreationTime) 12 .ToListAsync(); 13 14 return new ListResultDto<TaskListDto>( 15 ObjectMapper.Map<List<TaskListDto>>(tasks) 16 ); 17 } 18 }
这样, GetAll 方法会返回任务及相关的责任人信息。由于我们使用了 AutoMapper , 新的属性也会自动添加到 DTO 里。
单元测试责任人 Person
在这里,我们修改单元测试,(对测试不感兴趣者可直接跳过)看看获取任务列表时是否能获取到责任人。首先,我们修改 TestDataBuilder 类里的初始化测试数据,分配任务给责任人。
代码如下
1 public class TestDataBuilder 2 { 3 //... 4 5 public void Build() 6 { 7 var neo = new Person("Neo"); 8 _context.People.Add(neo); 9 _context.SaveChanges(); 10 11 _context.Tasks.AddRange( 12 new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality.", neo.Id), 13 new Task("Clean your room") { State = TaskState.Completed } 14 ); 15 } 16 }
然后我们修改 TaskAppService_Tests.Should_Get_All_Tasks() 方法,检查是否有一个任务已经指派了责任人(请看代码最后一行)
代码如下
1 [Fact] 2 public async System.Threading.Tasks.Task Should_Get_All_Tasks() 3 { 4 //Act 5 var output = await _taskAppService.GetAll(new GetAllTasksInput()); 6 7 //Assert 8 output.Items.Count.ShouldBe(2); 9 output.Items.Count(t => t.AssignedPersonName != null).ShouldBe(1); 10 }
友情提示:扩张方法 Count 需要使用 using System.Linq 语句。
在任务列表页展示责任人的名字
最后,我们修改 Task\Index.cshtml 来展示 责任人的名字 AssignedPersonName 。
代码如下
1 @foreach (var task in Model.Tasks) 2 { 3 <li class="list-group-item"> 4 <span class="pull-right label label-lg @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span> 5 <h4 class="list-group-item-heading">@task.Title</h4> 6 <div class="list-group-item-text"> 7 @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") | @(task.AssignedPersonName ?? L("Unassigned")) 8 </div> 9 </li> 10 }
启动程序,我们可以看到任务列表入下图:
任务创建的新应用服务方法
现在我们可以展示所有的任务,但是我们却还没有一个任务创建页面。首先,在 ITaskAppService 接口里添加一个 Create 方法。
代码如下
1 public interface ITaskAppService : IApplicationService 2 { 3 //... 4 5 System.Threading.Tasks.Task Create(CreateTaskInput input); 6 }
然后在 TaskAppService 类里实现它
代码如下
1 public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService 2 { 3 private readonly IRepository<Task> _taskRepository; 4 5 public TaskAppService(IRepository<Task> taskRepository) 6 { 7 _taskRepository = taskRepository; 8 } 9 10 //... 11 12 public async System.Threading.Tasks.Task Create(CreateTaskInput input) 13 { 14 var task = ObjectMapper.Map<Task>(input); 15 await _taskRepository.InsertAsync(task); 16 } 17 }
Create 方法会自动映射输入参数 input 到task 实体,之后我们使用仓储 repository 来将任务实体插入数据库中。让我们来看看输入参数 input 的 CreateTaskInput DTO 。
代码如下
1 using System; 2 using System.ComponentModel.DataAnnotations; 3 using Abp.AutoMapper; 4 5 namespace Acme.SimpleTaskApp.Tasks.Dtos 6 { 7 [AutoMapTo(typeof(Task))] 8 public class CreateTaskInput 9 { 10 [Required] 11 [MaxLength(Task.MaxTitleLength)] 12 public string Title { get; set; } 13 14 [MaxLength(Task.MaxDescriptionLength)] 15 public string Description { get; set; } 16 17 public Guid? AssignedPersonId { get; set; } 18 } 19 }
我们将DTO配置为映射到任务 Task 实体(使用 AutoMap 特性),同时添加数据验证 validation 。我们使用任务 Task 实体的常量来同步设置最大字串长度。
测试任务创建服务
我们添加 TaskAppService_Tests 类的集成测试来测试 Create 方法:(如果对测试不感兴趣者可以跳过这个部分)
代码如下
1 using Acme.SimpleTaskApp.Tasks; 2 using Acme.SimpleTaskApp.Tasks.Dtos; 3 using Shouldly; 4 using Xunit; 5 using System.Linq; 6 using Abp.Runtime.Validation; 7 8 namespace Acme.SimpleTaskApp.Tests.Tasks 9 { 10 public class TaskAppService_Tests : SimpleTaskAppTestBase 11 { 12 private readonly ITaskAppService _taskAppService; 13 14 public TaskAppService_Tests() 15 { 16 _taskAppService = Resolve<ITaskAppService>(); 17 } 18 19 //... 20 21 [Fact] 22 public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title() 23 { 24 await _taskAppService.Create(new CreateTaskInput 25 { 26 Title = "Newly created task #1" 27 }); 28 29 UsingDbContext(context => 30 { 31 var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1"); 32 task1.ShouldNotBeNull(); 33 }); 34 } 35 36 [Fact] 37 public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title_And_Assigned_Person() 38 { 39 var neo = UsingDbContext(context => context.People.Single(p => p.Name == "Neo")); 40 41 await _taskAppService.Create(new CreateTaskInput 42 { 43 Title = "Newly created task #1", 44 AssignedPersonId = neo.Id 45 }); 46 47 UsingDbContext(context => 48 { 49 var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1"); 50 task1.ShouldNotBeNull(); 51 task1.AssignedPersonId.ShouldBe(neo.Id); 52 }); 53 } 54 55 [Fact] 56 public async System.Threading.Tasks.Task Should_Not_Create_New_Task_Without_Title() 57 { 58 await Assert.ThrowsAsync<AbpValidationException>(async () => 59 { 60 await _taskAppService.Create(new CreateTaskInput 61 { 62 Title = null 63 }); 64 }); 65 } 66 } 67 }
第一个测试创建了一个带 title 的任务, 第二个测试创建了一个带 title 和 责任人 的测试,最后一个测试创建了一个无效的任务来展示 exception 例子。
任务创建页面
我们现在知道 TaskAppService.Create 方法可以正常工作了。现在,我们可以创建一个页面来添加新任务了。完成后的效果如下图所示:
首先,我们在任务控制器 TaskController 添加一个 Create action 。
代码如下
1 using System.Threading.Tasks; 2 using Abp.Application.Services.Dto; 3 using Acme.SimpleTaskApp.Tasks; 4 using Acme.SimpleTaskApp.Tasks.Dtos; 5 using Acme.SimpleTaskApp.Web.Models.Tasks; 6 using Microsoft.AspNetCore.Mvc; 7 using Microsoft.AspNetCore.Mvc.Rendering; 8 using System.Linq; 9 using Acme.SimpleTaskApp.Common; 10 using Acme.SimpleTaskApp.Web.Models.People; 11 12 namespace Acme.SimpleTaskApp.Web.Controllers 13 { 14 public class TasksController : SimpleTaskAppControllerBase 15 { 16 private readonly ITaskAppService _taskAppService; 17 private readonly ILookupAppService _lookupAppService; 18 19 public TasksController( 20 ITaskAppService taskAppService, 21 ILookupAppService lookupAppService) 22 { 23 _taskAppService = taskAppService; 24 _lookupAppService = lookupAppService; 25 } 26 27 //... 28 29 public async Task<ActionResult> Create() 30 { 31 var peopleSelectListItems = (await _lookupAppService.GetPeopleComboboxItems()).Items 32 .Select(p => p.ToSelectListItem()) 33 .ToList(); 34 35 peopleSelectListItems.Insert(0, new SelectListItem { Value = string.Empty, Text = L("Unassigned"), Selected = true }); 36 37 return View(new CreateTaskViewModel(peopleSelectListItems)); 38 } 39 } 40 }
我们将 ILookupAppService 反射进来,这样可以获取责任人下拉框的项目。本来我们是可以直接反射使用 IRepository<Person,Guid> 的,但是为了更好的分层和复用,我们还是使用 ILookUpAppService 。ILookupAppService.GetPeopleComboboxItems 在应用层的定义如下:
代码如下
1 public interface ILookupAppService : IApplicationService 2 { 3 Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems(); 4 } 5 6 public class LookupAppService : SimpleTaskAppAppServiceBase, ILookupAppService 7 { 8 private readonly IRepository<Person, Guid> _personRepository; 9 10 public LookupAppService(IRepository<Person, Guid> personRepository) 11 { 12 _personRepository = personRepository; 13 } 14 15 public async Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems() 16 { 17 var people = await _personRepository.GetAllListAsync(); 18 return new ListResultDto<ComboboxItemDto>( 19 people.Select(p => new ComboboxItemDto(p.Id.ToString("D"), p.Name)).ToList() 20 ); 21 } 22 }
ComboboxItemDto 是一个简单的类(在 ABP 中定义),用于传递下拉框 Combobox 的项目的数据。 TaskController.Create 方法仅使用了这个方法并将返回的列表转换为 SelectListItem (在 AspNet Core 中定义),然后用 CreateTaskViewModel 返回给视图。
代码如下
1 using System.Collections.Generic; 2 using Microsoft.AspNetCore.Mvc.Rendering; 3 4 namespace Acme.SimpleTaskApp.Web.Models.People 5 { 6 public class CreateTaskViewModel 7 { 8 public List<SelectListItem> People { get; set; } 9 10 public CreateTaskViewModel(List<SelectListItem> people) 11 { 12 People = people; 13 } 14 } 15 }
Create 视图如下:
代码如下
1 @using Acme.SimpleTaskApp.Web.Models.People 2 @model CreateTaskViewModel 3 4 @section scripts 5 { 6 <environment names="Development"> 7 <script src="~/js/views/tasks/create.js"></script> 8 </environment> 9 10 <environment names="Staging,Production"> 11 <script src="~/js/views/tasks/create.min.js"></script> 12 </environment> 13 } 14 15 <h2> 16 @L("NewTask") 17 </h2> 18 19 <form id="TaskCreationForm"> 20 21 <div class="form-group"> 22 <label for="Title">@L("Title")</label> 23 <input type="text" name="Title" class="form-control" placeholder="@L("Title")" required maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxTitleLength"> 24 </div> 25 26 <div class="form-group"> 27 <label for="Description">@L("Description")</label> 28 <input type="text" name="Description" class="form-control" placeholder="@L("Description")" maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxDescriptionLength"> 29 </div> 30 31 <div class="form-group"> 32 @Html.Label(L("AssignedPerson")) 33 @Html.DropDownList( 34 "AssignedPersonId", 35 Model.People, 36 new 37 { 38 @class = "form-control", 39 id = "AssignedPersonCombobox" 40 }) 41 </div> 42 43 <button type="submit" class="btn btn-default">@L("Save")</button> 44 45 </form>
我们编写 create.js 如下:
代码如下
1 (function($) { 2 $(function() { 3 4 var _$form = $('#TaskCreationForm'); 5 6 _$form.find('input:first').focus(); 7 8 _$form.validate(); 9 10 _$form.find('button[type=submit]') 11 .click(function(e) { 12 e.preventDefault(); 13 14 if (!_$form.valid()) { 15 return; 16 } 17 18 var input = _$form.serializeFormToObject(); 19 abp.services.app.task.create(input) 20 .done(function() { 21 location.href = '/Tasks'; 22 }); 23 }); 24 }); 25 })(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 :
代码如下
1 using Microsoft.AspNetCore.Mvc; 2 3 namespace Acme.SimpleTaskApp.Web.Controllers 4 { 5 public class HomeController : SimpleTaskAppControllerBase 6 { 7 public ActionResult Index() 8 { 9 return RedirectToAction("Index", "Tasks"); 10 } 11 } 12 }
然后删除 视图里的主页 Views/Home 文件夹并从 SimpleTaskAppNavigationProvider 类里删除菜单项。我们也可以从本地化 JSON 文件中删除点不需要的关键词。
其他相关内容
我们将不断改进本篇内容
- 从任务列表里打开/关闭任务,然后刷新任务项目。
- 为责任人下拉框使用组件
- 等等
文章更改历史
- 2017-07-30:将文章中的 ListResultOutput 替换为 ListResultDto
- 2017-06-02:将项目和文章修改为支持 .net core
- 2016-08-09:根据反馈修改文章
- 2016-08-08:初次发布文章
版权所有
该文章和其中的任何源代码和文件的版权均归 The Code Project Open License (CPOL) 所有
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步