zoukankan      html  css  js  c++  java
  • ABP 教程文档 1-1 手把手引进门之 ASP.NET Core & Entity Framework Core(官方教程翻译版 版本3.2.5)第二篇

     

    本文是ABP官方文档翻译版,翻译基于 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应用 第二篇

    土牛语录:30 Jul 2017

    以下是手把手引进门教程,基于 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 应用”系列文章的第二篇。以下可以看其他篇目:

    应用开发

    创建 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 }
    View Code

    这一次,我们作为示范,将 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 }
    View Code

    责任人 AssignedPerson 是可选的。所以,任务可以指派给责任人或者不指派

    添加 Person 到 数据库上下文 DbContext

    最后,我们添加新的责任人 Person 实体到 DbContext 类中:

    代码如下

    1 public class SimpleTaskAppDbContext : AbpDbContext
    2 {
    3     public DbSet<Person> People { get; set; }
    4     
    5     //...
    6 }
    View Code

    添加 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 }
    View Code

    该类为自动生成的,我们只是将 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 }
    View Code

    同时将 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 }
    View Code

    这样, 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 }
    View Code

    然后我们修改 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 }
    View Code

    友情提示:扩张方法 Count 需要使用 using System.Linq 语句。

    在任务列表页展示责任人的名字

    最后,我们修改 TaskIndex.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 }
    View Code

    启动程序,我们可以看到任务列表入下图:

    任务创建的新应用服务方法

    现在我们可以展示所有的任务,但是我们却还没有一个任务创建页面。首先,在 ITaskAppService 接口里添加一个 Create 方法。

    代码如下

    1 public interface ITaskAppService : IApplicationService
    2 {
    3     //...
    4 
    5     System.Threading.Tasks.Task Create(CreateTaskInput input);
    6 }
    View Code

    然后在 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 }
    View Code

    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 }
    View Code

    我们将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 }
    View Code

    第一个测试创建了一个带 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 }
    View Code

    我们将 ILookupAppService 反射进来,这样可以获取责任人下拉框的项目。本来我们是可以直接反射使用 IRepository<Person,Guid> 的,但是为了更好的分层和复用,我们还是使用 ILookUpAppService 。ILookupAppService.GetPeopleComboboxItems 在应用层的定义如下:

    代码如下

     1 public interface ILookupAppService : IApplicationService
     2 {
     3     Task&lt;ListResultDto&lt;ComboboxItemDto>> GetPeopleComboboxItems();
     4 }
     5 
     6 public class LookupAppService : SimpleTaskAppAppServiceBase, ILookupAppService
     7 {
     8     private readonly IRepository&lt;Person, Guid> _personRepository;
     9 
    10     public LookupAppService(IRepository&lt;Person, Guid> personRepository)
    11     {
    12         _personRepository = personRepository;
    13     }
    14 
    15     public async Task&lt;ListResultDto&lt;ComboboxItemDto>> GetPeopleComboboxItems()
    16     {
    17         var people = await _personRepository.GetAllListAsync();
    18         return new ListResultDto&lt;ComboboxItemDto>(
    19             people.Select(p => new ComboboxItemDto(p.Id.ToString("D"), p.Name)).ToList()
    20         );
    21     }
    22 }
    View Code

    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&lt;SelectListItem> People { get; set; }
     9 
    10         public CreateTaskViewModel(List&lt;SelectListItem> people)
    11         {
    12             People = people;
    13         }
    14     }
    15 }
    View Code

    Create 视图如下:

    代码如下

     1 @using Acme.SimpleTaskApp.Web.Models.People
     2 @model CreateTaskViewModel
     3 
     4 @section scripts
     5 {
     6     &lt;environment names="Development">
     7         &lt;script src="~/js/views/tasks/create.js">&lt;/script>
     8     &lt;/environment>
     9 
    10     &lt;environment names="Staging,Production">
    11         &lt;script src="~/js/views/tasks/create.min.js">&lt;/script>
    12     &lt;/environment>
    13 }
    14 
    15 &lt;h2>
    16     @L("NewTask")
    17 &lt;/h2>
    18 
    19 &lt;form id="TaskCreationForm">
    20     
    21     &lt;div class="form-group">
    22         &lt;label for="Title">@L("Title")&lt;/label>
    23         &lt;input type="text" name="Title" class="form-control" placeholder="@L("Title")" required maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxTitleLength">
    24     &lt;/div>
    25 
    26     &lt;div class="form-group">
    27         &lt;label for="Description">@L("Description")&lt;/label>
    28         &lt;input type="text" name="Description" class="form-control" placeholder="@L("Description")" maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxDescriptionLength">
    29     &lt;/div>
    30 
    31     &lt;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     &lt;/div>
    42 
    43     &lt;button type="submit" class="btn btn-default">@L("Save")&lt;/button>
    44 
    45 &lt;/form>
    View Code

    我们编写 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);
    View Code

    让我们一起来看看这个 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 &lt;a class="btn btn-primary btn-sm" asp-action="Create">@L("AddNew")&lt;/a>
    View Code

    删除主页和关于页

    如果我们不需要主页和关于页,我们可以从应用里删除掉它们。首先,删除主页控制器 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 }
    View Code

    然后删除 视图里的主页 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) 所有

  • 相关阅读:
    java performance
    C# and Java: Comparing Programming Languages
    MYSQL blogs and articles
    网络基本功系列:细说网络那些事儿
    Spark 优化器 ML的论文
    逻辑回归
    MapReduce
    Spark
    Set-Theory-and-Logic
    k-means
  • 原文地址:https://www.cnblogs.com/yabu007/p/8117792.html
Copyright © 2011-2022 走看看