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應用 第二篇

土牛語錄:Halil ibrahim Kalkan30 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 語句。

 

在任務列表頁展示責任人的名字

最后,我們修改 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 }
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) 所有


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM