本文是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應用
土牛語錄:
以下是手把手引進門教程,基於 ASP.NET Core, Entity Framework Core ,ABP 框架 創建Web 應用, PS: 自帶自動的測試模塊哦。
本文目錄如下:
介紹
前期准備
創建應用
正式開發
創建任務實體 Entity
將任務添加到數據庫上下文 DbContext
創建第一個數據遷移
創建數據庫
編寫任務服務
測試任務服務
任務列表展示
添加菜單
創建任務 Controller 和 視圖模型
任務列表頁面
本地化
任務過濾
任務列表頁面的自動化測試
其他相關內容
文章修改歷史
版權所有
介紹
這是系列文章的第一部分:使用 ASP.NET Core, Entity Framework Core 和 ASP.NET Boilerplate 創建N層Web應用
在本文中,我將指導大家創建一個樣例(跨平台的多層Web應用),該樣例會用到如下工具(請讀者提前准備):
- Net Core 跨平台應用的基礎開發框架
- ASP.NET Boilerplate (ABP) 開發的基礎框架模板
- ASP.NET Core web開發框架
- Entity Framework Core ORM 數據框架
- Twitter Bootstrap HTML&CSS 前端開發框架
- jQuery 客戶端 AJAX/DOM 類庫
- xUnit 和 Shouldly 服務端測試工具(單元測試/集成測試)
ABP 框架中會默認使用 Log4Net 和 AutoMapper 。
我們同時還會使用以下技術:
- Layered Architecture 分層架構
- Domain Driven Design (DDD) DDD領域模型
- Dependency injection (DI) DI 依賴注入
- Integration Testing 集成測試
演示的開發項目是一個簡單的任務管理應用,用於將任務分配出去。我不會一層一層的進行開發,而是隨着應用的拓展直接切換所需的層次。隨着應用的推拓展,我將會介紹所需的ABP和其他框架的特性。
前期准備
開發樣例時需要以下工具,請提前在你的機器上進行安裝:
- Visual Studio 2017
- SQL Server (你可以更改連接字符串為 localdb)
- Visual Studio Extensions:
創建應用
首先使用ABP模版(http://www.aspnetboilerplate.com/Templates)創建一個web應用項目,命名為"Acme.SimpleTaskApp" 。創建模板時可以設置自己的公司名稱(比如Acme)。
本樣例使用MPA(Multi Page Web Application)多頁面模式(注:即使用MVC和Razor技術)進行開發,本文不使用SPA(注:土牛的SPA是使用Angular)單頁面模式。同時為了使用最基礎的開發模板功能,本文不使用Module Zero模塊。
ABP 模版會創建一個多層的解決方案,如下圖:
模板會根據輸入的名字自動創建6個項目。
- core 領域層/業務層,包含實體Entity,領域服務 domain service 等等
- Application 應用層 , 包含DTO,應用服務 application service 等等
- Entity Framework 基礎設施層 ,EF core 數據庫集成處理 (從其他層抽象出來的EF core)
- Web 展示層 , 即Asp.net MVC層
- Tests 單元測試和集成測試,含應用層,領域層,基礎設施層,不含Web展示層
- Web.Tests ASP.NET Core集成測試,包含web展示層的全部集成測試
以上是沒有選擇zero的項目結果,如果你選擇了zero,項目結構就會變成下圖:
當你把應用運行起來后,你會看到下圖所示的用戶界面:
這個應用包含一個頂級菜單欄,包含空的首頁,關於頁,還有一個語言的下拉選項。
正式開發
創建任務實體 Entity
我們從創建一個簡單的任務實體 Task Entity 開始,由於它屬於領域層,把它加到 core 項目里。
代碼如下:

1 using System; 2 using System.ComponentModel.DataAnnotations; 3 using System.ComponentModel.DataAnnotations.Schema; 4 using Abp.Domain.Entities; 5 using Abp.Domain.Entities.Auditing; 6 using Abp.Timing; 7 8 namespace Acme.SimpleTaskApp.Tasks 9 { 10 [Table("AppTasks")] 11 public class Task : Entity, IHasCreationTime 12 { 13 public const int MaxTitleLength = 256; 14 public const int MaxDescriptionLength = 64 * 1024; //64KB 15 16 [Required] 17 [MaxLength(MaxTitleLength)] 18 public string Title { get; set; } 19 20 [MaxLength(MaxDescriptionLength)] 21 public string Description { get; set; } 22 23 public DateTime CreationTime { get; set; } 24 25 public TaskState State { get; set; } 26 27 public Task() 28 { 29 CreationTime = Clock.Now; 30 State = TaskState.Open; 31 } 32 33 public Task(string title, string description = null) 34 : this() 35 { 36 Title = title; 37 Description = description; 38 } 39 } 40 41 public enum TaskState : byte 42 { 43 Open = 0, 44 Completed = 1 45 } 46 }
- Task 實體從 ABP 的 Entity 基類繼承,Entity 基類默認ID屬性是 int 類型。如果主鍵類型為非 int 類型,也可以選擇范型版本的 Entity<TPrimaryKey>.
- IHasCreationTime 是一個簡單的接口,只定義了 CreationTime 屬性 (統一規范 CreationTime 的名字)
- Task 實體定義了一個必填的 Title 和 非必填的 Description
- TaskState 是一個簡單枚舉,定義了 Task 任務的狀態
- Clock.Now 返回默認的 DateTime.Now 。但它提供了一個抽象方法,使得我們可以在將來有需要的時候很輕松就可以轉換為 DateTime.UtcNow 。在 ABP 框架中總是使用Clock.Now 而不使用 DateTime.Now 。
- 將 Task 實體存儲到數據庫的 AppTasks 表中。
將任務添加到數據庫上下文 DbContext
.EntityFrameworkCore 包含一個預定義的 DbContext 。將 Task 實體的 DbSet 加到 DbContext 里。
代碼如下:

1 public class SimpleTaskAppDbContext : AbpDbContext 2 { 3 public DbSet<Task> Tasks { get; set; } 4 5 public SimpleTaskAppDbContext(DbContextOptions<SimpleTaskAppDbContext> options) 6 : base(options) 7 { 8 9 } 10 }
現在,EF core 知道我們有了一個 Task 的實體。
創建第一個數據遷移
我們將創建一個初始化數據庫遷移文件,它會自動創建數據庫和數據庫表 AppTasks 。打開源管理器 Package Manager Console from Visual Studio , 執行 Add-Migration 命令(默認的項目必須是 .EntityFrameworkCore 項目),如圖:
這個命令會在 . EntityFrameworkCore 項目下創建一個遷移( Migrations )文件夾,文件夾包含一個遷移類和數據庫模型的快照,如圖:
如下代碼所示,自動創建了 “初始化 ( Initial )”遷移類:

1 public partial class Initial : Migration 2 { 3 protected override void Up(MigrationBuilder migrationBuilder) 4 { 5 migrationBuilder.CreateTable( 6 name: "AppTasks", 7 columns: table => new 8 { 9 Id = table.Column<int>(nullable: false) 10 .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 11 CreationTime = table.Column<DateTime>(nullable: false), 12 Description = table.Column<string>(maxLength: 65536, nullable: true), 13 State = table.Column<byte>(nullable: false), 14 Title = table.Column<string>(maxLength: 256, nullable: false) 15 }, 16 constraints: table => 17 { 18 table.PrimaryKey("PK_AppTasks", x => x.Id); 19 }); 20 } 21 22 protected override void Down(MigrationBuilder migrationBuilder) 23 { 24 migrationBuilder.DropTable( 25 name: "AppTasks"); 26 } 27 }
當我們執行數據庫遷移命令時,這些代碼會創建 AppTasks 表 (更多遷移相關信息請參照 entity framework documentation )
創建數據庫
以上的遷移執行完畢后(注:Add-Migration 命令執行后),在包管理控制台中執行 Update-Database 命令,如下圖:
這個命令將在 local SQL Server 中創建一個名為 “SimpleTaskAppDb” 的數據庫並執行數據庫遷移(此時,我們只有一個“初始化 ( Initial )”的遷移):
現在,我們有了 Task 實體,並且在數據庫中有對應的數據庫表, 我們輸入一些簡單的任務到表里。
友情提示: 數據庫上下文字符串 connection string 在 .web 應用的 appsettings.json 中。 (要換數據庫的自己改一下字符串哦)。
編寫任務服務
Application Services 應用層服務用於將領域業務邏輯暴露給展示層。展示層在必要時通過使用 Data Transfer Object 數據傳輸對象(DTO)作用參數調用一個應用服務,應用服務則通過調用領域對象執行一些具體的業務邏輯並在有需要時返回一個DTO給展示層。
我們在 .Application 項目中創建第一個應用服務 TaskAppService ,該服務將執行與任務相關的應用程序邏輯。首先,我們先來定義一個app 服務接口:
代碼如下:

1 public interface ITaskAppService : IApplicationService 2 { 3 Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input); 4 }
我們推薦先定義接口,但不是非這樣做不可。按照慣例,ABP 中所有的應用服務都需要實現 IApplicationService 接口 (它只是一個空的標記接口)。我們創建了一個 GetAll 方法去查詢任務列表,同時,我們定義了如下的 DTOs :
代碼如下:

1 public class GetAllTasksInput 2 { 3 public TaskState? State { get; set; } 4 } 5 6 [AutoMapFrom(typeof(Task))] 7 public class TaskListDto : EntityDto, IHasCreationTime 8 { 9 public string Title { get; set; } 10 11 public string Description { get; set; } 12 13 public DateTime CreationTime { get; set; } 14 15 public TaskState State { get; set; } 16 }
- GetAllTasksInput DTO 為 GetAll 應用服務方法定義了一個輸入參數 。 我們將 狀態 state 定義為 DTO 對象 而不定義為方法的參數。 這樣我們將來需要的時候可以在這個DTO增加其他的參數,同時兼容現有的客戶端 (當然我們也可以在方法里加一個 state 參數)。
- TaskListDto 用開返回任務數據。該Dto 從 EntityDto 繼承,EntityDto 只是定義了 Id 屬性(我們可以不繼承 EntityDto ,直接自己將 Id 加到我們的Dto里)。我們定義了[AutoMapFrom] 特性來創建 AutoMapper 自動映射任務實體到任務列表Dto TaskListDto 。這個特性在 Abp.AutoMapper nuget 包里進行了定義。
- ListResultDto 是一個簡單的類,包含了一個列表(我們可以直接返回一個 List<TaskListDto> 列表)。
現在,我們可以實現 ITaskAppService 了。
代碼如下:

1 using System.Collections.Generic; 2 using System.Linq; 3 using System.Threading.Tasks; 4 using Abp.Application.Services.Dto; 5 using Abp.Domain.Repositories; 6 using Abp.Linq.Extensions; 7 using Acme.SimpleTaskApp.Tasks.Dtos; 8 using Microsoft.EntityFrameworkCore; 9 10 namespace Acme.SimpleTaskApp.Tasks 11 { 12 public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService 13 { 14 private readonly IRepository<Task> _taskRepository; 15 16 public TaskAppService(IRepository<Task> taskRepository) 17 { 18 _taskRepository = taskRepository; 19 } 20 21 public async Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input) 22 { 23 var tasks = await _taskRepository 24 .GetAll() 25 .WhereIf(input.State.HasValue, t => t.State == input.State.Value) 26 .OrderByDescending(t => t.CreationTime) 27 .ToListAsync(); 28 29 return new ListResultDto<TaskListDto>( 30 ObjectMapper.Map<List<TaskListDto>>(tasks) 31 ); 32 } 33 } 34 }
- TaskAppService 該類從 SimpleTaskAppAppServiceBase 繼承,SimpleTaskAppAppServiceBase (從 ABP 的 ApplicationService 類繼承)在模板里已經自動生成。 TaskAppService 不是必須從 SimpleTaskAppAppServiceBase 繼承,應用服務可以是普通類。但是 ApplicationService 基類有一些預先注入的服務(就像這里使用的 ObjectMapper )
- 我們使用依賴注入 dependency injection 來獲取數據倉儲 repository
- Repositories 數據倉儲用於為數據實體抽象數據庫操作。ABP 為每個實體創建了預定義的數據庫倉儲(就像這里用到了 IRepository<Task> )用於實現通用的任務。IRepository.GetAll() 方法用於查詢數據實體,它返回了一個 IQueryable 接口。
- WhereIf 這是 ABP 里的一個拓展方法,該方法提供了一個 IQueryable.Where 方法的簡便條件語法。
- ObjectMapper 用於將任務對象列表映射到任務列表Dto對象列表 (基於 Application Service 基類並默認實現 AutoMapper )
測試任務服務
在創建用戶接口錢,我們需要測試一下任務應用服務 TaskAppService 。 如果你對自動化測試不感興趣的話,可以忽略這個部分。
我們的模板包含 .Tests 項目,這可以測試我們的代碼。這個項目不使用 SQL Server數據庫,而是使用EF core 的內存數據庫。所以,我們可以不用真實數據庫來進行單元測試。它為每個測試創建了單獨的數據庫。所以每個測試都是隔離的。我們需要在開始測試前使用 TestDataBuilder 類添加初始測試數據到內存數據庫里。我修改了 TestDataBuilder 。
代碼如下:

1 public class TestDataBuilder 2 { 3 private readonly SimpleTaskAppDbContext _context; 4 5 public TestDataBuilder(SimpleTaskAppDbContext context) 6 { 7 _context = context; 8 } 9 10 public void Build() 11 { 12 _context.Tasks.AddRange( 13 new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality."), 14 new Task("Clean your room") { State = TaskState.Completed } 15 ); 16 } 17 }
通過樣例項目的源代碼,你可以看懂 TestDataBuilder 在哪里用,具體怎么用。我們添加2個任務(其中一個已經完成)到數據庫上下文 dbcontext 。我們可以假定數據庫中有2個任務,開始編寫測試用例。 第一個繼承測試用來測試 TaskAppService.GetAll 方法。
代碼如下:

1 public class TaskAppService_Tests : SimpleTaskAppTestBase 2 { 3 private readonly ITaskAppService _taskAppService; 4 5 public TaskAppService_Tests() 6 { 7 _taskAppService = Resolve<ITaskAppService>(); 8 } 9 10 [Fact] 11 public async System.Threading.Tasks.Task Should_Get_All_Tasks() 12 { 13 //Act 14 var output = await _taskAppService.GetAll(new GetAllTasksInput()); 15 16 //Assert 17 output.Items.Count.ShouldBe(2); 18 } 19 20 [Fact] 21 public async System.Threading.Tasks.Task Should_Get_Filtered_Tasks() 22 { 23 //Act 24 var output = await _taskAppService.GetAll(new GetAllTasksInput { State = TaskState.Open }); 25 26 //Assert 27 output.Items.ShouldAllBe(t => t.State == TaskState.Open); 28 } 29 }
我們創建2個不同的測試用例來測試 GetAll 方法。現在,我們打開測試瀏覽器(在VS主菜單的 Test\Windows\Test Explorer 菜單下)開始進行單元測試。
所有測試均成功。最后一個我們現在可以忽略它,他是一個模板生成的測試。
友情提示: ABP 模板默認安裝使用 xUnit 和 Shouldly 。我們使用它們編寫我們的測試。
任務列表展示
現在,我們確定 TaskAppService 服務可以正常工作。 我們可以開始創建頁面來展示所有的任務。
添加菜單
首先,我們在頂級菜單上添加一個新的菜單
代碼如下

1 public class SimpleTaskAppNavigationProvider : NavigationProvider 2 { 3 public override void SetNavigation(INavigationProviderContext context) 4 { 5 context.Manager.MainMenu 6 .AddItem( 7 new MenuItemDefinition( 8 "Home", 9 L("HomePage"), 10 url: "", 11 icon: "fa fa-home" 12 ) 13 ).AddItem( 14 new MenuItemDefinition( 15 "About", 16 L("About"), 17 url: "Home/About", 18 icon: "fa fa-info" 19 ) 20 ).AddItem( 21 new MenuItemDefinition( 22 "TaskList", 23 L("TaskList"), 24 url: "Tasks", 25 icon: "fa fa-tasks" 26 ) 27 ); 28 } 29 30 private static ILocalizableString L(string name) 31 { 32 return new LocalizableString(name, SimpleTaskAppConsts.LocalizationSourceName); 33 } 34 }
模板自帶兩個頁面:首頁和關於頁,如上代碼所示。我們也可以修改它們創建新的頁面。但現在我們不修改首頁和關於頁,我們創建新的菜單項。
創建任務 Controller 和 視圖模型
我們在 .Web 項目下創建一個新的 controller 類,命名為 TasksController 。
代碼如下

1 public class TasksController : SimpleTaskAppControllerBase 2 { 3 private readonly ITaskAppService _taskAppService; 4 5 public TasksController(ITaskAppService taskAppService) 6 { 7 _taskAppService = taskAppService; 8 } 9 10 public async Task<ActionResult> Index(GetAllTasksInput input) 11 { 12 var output = await _taskAppService.GetAll(input); 13 var model = new IndexViewModel(output.Items); 14 return View(model); 15 } 16 }
- TasksController 從 SimpleTaskAppControllerBase ( SimpleTaskAppControllerBase 從 AbpController 繼承)繼承,該類包含應用程序 Controllers 需要的通用基礎代碼。
- 我們反射了 ITaskAppService , 以獲取到所有的任務列表。
- 我們在 .Web 項目中創建了一個 IndexViewModel 類來將數據展示到視圖上,這樣可以不直接將 GetAll 方法的結果直接暴露到視圖上。
代碼如下

1 public class IndexViewModel 2 { 3 public IReadOnlyList<TaskListDto> Tasks { get; } 4 5 public IndexViewModel(IReadOnlyList<TaskListDto> tasks) 6 { 7 Tasks = tasks; 8 } 9 10 public string GetTaskLabel(TaskListDto task) 11 { 12 switch (task.State) 13 { 14 case TaskState.Open: 15 return "label-success"; 16 default: 17 return "label-default"; 18 } 19 } 20 }
我們創建了一個簡單的視圖模型,在它的構造函數中,我們獲取了一個任務列表(由 ITaskAppService 提供)。同時它還有一個 GetTaskLabel 方法,用於在視圖中通過一個 選擇 Bootstrap 標簽來標示任務。
任務列表頁面
最后,完成實際的 Index 視圖。
代碼如下

1 @model Acme.SimpleTaskApp.Web.Models.Tasks.IndexViewModel 2 3 @{ 4 ViewBag.Title = L("TaskList"); 5 ViewBag.ActiveMenu = "TaskList"; //Matches with the menu name in SimpleTaskAppNavigationProvider to highlight the menu item 6 } 7 8 <h2>@L("TaskList")</h2> 9 10 <div class="row"> 11 <div> 12 <ul class="list-group"> 13 @foreach (var task in Model.Tasks) 14 { 15 <li class="list-group-item"> 16 <span class="pull-right label @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span> 17 <h4 class="list-group-item-heading">@task.Title</h4> 18 <div class="list-group-item-text"> 19 @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") 20 </div> 21 </li> 22 } 23 </ul> 24 </div> 25 </div>
我們使用 Bootstrap 的 list group 組件和定義好的模型來渲染視圖。我們使用 IndexViewModel.GetTaskLable() 方法來獲得任務的標簽類型。渲染后的界面如下圖:
本地化
我們在視圖里使用 ABP 框架自帶的 L 方法。 它用於本地化語言。我們在 .Core 項目下的 Localization/Source 文件夾中定義好了本地化字符串,使用 .json 文件。英語版本的本地化語言設置
代碼如下

1 { 2 "culture": "en", 3 "texts": { 4 "HelloWorld": "Hello World!", 5 "ChangeLanguage": "Change language", 6 "HomePage": "HomePage", 7 "About": "About", 8 "Home_Description": "Welcome to SimpleTaskApp...", 9 "About_Description": "This is a simple startup template to use ASP.NET Core with ABP framework.", 10 "TaskList": "Task List", 11 "TaskState_Open": "Open", 12 "TaskState_Completed": "Completed" 13 } 14 }
模板自帶了大多數的文本,當然,它們可以刪除掉。在上面的代碼中我只是加了最后的三行。使用 ABP 的本地化是相當的簡單,如果你想了解本地化系統更多的信息,請查閱文檔 localization document
任務過濾
正如之前說過的,TaskController 實際上使用的是 GetAllTasksInput ,可以靈活的過濾任務。我們可以添加一個任務列表的下拉菜單來過濾任務。首先,我們添加一個下拉菜單到視圖上(我們加到 header 里):
代碼如下

1 <h2> 2 @L("TaskList") 3 <span class="pull-right"> 4 @Html.DropDownListFor( 5 model => model.SelectedTaskState, 6 Model.GetTasksStateSelectListItems(LocalizationManager), 7 new 8 { 9 @class = "form-control", 10 id = "TaskStateCombobox" 11 }) 12 </span> 13 </h2>
然后我修改了 IndexViewModel , 增加了 SeletedTaskState 屬性和 GetTaskStateSelectListItems 方法:
代碼如下

1 public class IndexViewModel 2 { 3 //... 4 5 public TaskState? SelectedTaskState { get; set; } 6 7 public List<SelectListItem> GetTasksStateSelectListItems(ILocalizationManager localizationManager) 8 { 9 var list = new List<SelectListItem> 10 { 11 new SelectListItem 12 { 13 Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, "AllTasks"), 14 Value = "", 15 Selected = SelectedTaskState == null 16 } 17 }; 18 19 list.AddRange(Enum.GetValues(typeof(TaskState)) 20 .Cast<TaskState>() 21 .Select(state => 22 new SelectListItem 23 { 24 Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, $"TaskState_{state}"), 25 Value = state.ToString(), 26 Selected = state == SelectedTaskState 27 }) 28 ); 29 30 return list; 31 } 32 }
我們也可以在 controller 里設置 SelectedTaskState :
代碼如下

1 public async Task<ActionResult> Index(GetAllTasksInput input) 2 { 3 var output = await _taskAppService.GetAll(input); 4 var model = new IndexViewModel(output.Items) 5 { 6 SelectedTaskState = input.State 7 }; 8 return View(model); 9 }
現在,我們運行程序,可以看到視圖的右上角有個下拉框,如圖:
我們添加了下拉框,但它現在還不能用。我們需要編寫一些簡單的 javascript 代碼,當下拉框內容更改后可以重新請求/刷新任務列表頁面。我們在 .Web 項目里創建了 wwwroot\js\views\tasks\index.js 文件
代碼如下

1 (function ($) { 2 $(function () { 3 4 var _$taskStateCombobox = $('#TaskStateCombobox'); 5 6 _$taskStateCombobox.change(function() { 7 location.href = '/Tasks?state=' + _$taskStateCombobox.val(); 8 }); 9 10 }); 11 })(jQuery);
我們首先添加 Bundler & Minifier 擴展程序(這是 ASP.NET Core 項目標配的壓縮文件)來壓縮腳本的大小, 然后開始在視圖里編寫 javascript :
這將在 .Web 項目中的 bundleconfig.json 中添加以下代碼
代碼如下

1 { 2 "outputFileName": "wwwroot/js/views/tasks/index.min.js", 3 "inputFiles": [ 4 "wwwroot/js/views/tasks/index.js" 5 ] 6 }
同時創建了 script 的壓縮版本
無論我何時修改了index.js , index.min.js 都會自動重新生成。現在,我們可以在我們的頁面里插入 javascript 文件了。
代碼如下

1 @section scripts 2 { 3 <environment names="Development"> 4 <script src="~/js/views/tasks/index.js"></script> 5 </environment> 6 7 <environment names="Staging,Production"> 8 <script src="~/js/views/tasks/index.min.js"></script> 9 </environment> 10 }
至此,我們的視圖將在開發環境下使用 index.js 包,而在生產環境中使用 index.min.js (壓縮版本)包。這是在 ASP.Net Core MVC 項目中通用的做法。
任務列表頁面的自動化測試
ASP.NET Core MVC 基礎框架中集成了一個繼承測試模塊。我們可以完整的測試我們的服務端代碼了。如果你對自動化測試不感興趣的話,你可以忽略這個部分。
ABP 模板中自帶 .Web.Tests 項目。我們創建一個普通的測試來請求 TaskController.Index , 然后檢查反饋內容:
代碼如下

1 public class TasksController_Tests : SimpleTaskAppWebTestBase 2 { 3 [Fact] 4 public async System.Threading.Tasks.Task Should_Get_Tasks_By_State() 5 { 6 //Act 7 8 var response = await GetResponseAsStringAsync( 9 GetUrl<TasksController>(nameof(TasksController.Index), new 10 { 11 state = TaskState.Open 12 } 13 ) 14 ); 15 16 //Assert 17 18 response.ShouldNotBeNullOrWhiteSpace(); 19 } 20 }
GetResponseAsStringAsync 和 GetUrl 是 ABP 的 AbpAspNetCoreIntrgratedTestBase 類中很有用的方法。使用這些快捷方法我們可以比較容易的創建請求,如果直接使用客戶端請求(一個 HttpClient 的實例)會相對復雜一些。如果想深入了解,請參考 ASP.NET Core 的 integration testing documentation
當我們開始 debug 測試模塊式,我們可以看到反饋的 HTML 如下圖
上圖顯示 Index 頁面的反饋很正常。但是,我們更想知道返回的 HTML 是否正如我們所預期的那樣。 有很多類庫可以用來解析 HTML 。ABP 模板的 .Web.Tests 項目預先安裝了其中的一個類庫 AngleSharp 我們用它來檢查創建的 HTML 代碼。
代碼如下

1 public class TasksController_Tests : SimpleTaskAppWebTestBase 2 { 3 [Fact] 4 public async System.Threading.Tasks.Task Should_Get_Tasks_By_State() 5 { 6 //Act 7 8 var response = await GetResponseAsStringAsync( 9 GetUrl<TasksController>(nameof(TasksController.Index), new 10 { 11 state = TaskState.Open 12 } 13 ) 14 ); 15 16 //Assert 17 18 response.ShouldNotBeNullOrWhiteSpace(); 19 20 //Get tasks from database 21 var tasksInDatabase = await UsingDbContextAsync(async dbContext => 22 { 23 return await dbContext.Tasks 24 .Where(t => t.State == TaskState.Open) 25 .ToListAsync(); 26 }); 27 28 //Parse HTML response to check if tasks in the database are returned 29 var document = new HtmlParser().Parse(response); 30 var listItems = document.QuerySelectorAll("#TaskList li"); 31 32 //Check task count 33 listItems.Length.ShouldBe(tasksInDatabase.Count); 34 35 //Check if returned list items are same those in the database 36 foreach (var listItem in listItems) 37 { 38 var header = listItem.QuerySelector(".list-group-item-heading"); 39 var taskTitle = header.InnerHtml.Trim(); 40 tasksInDatabase.Any(t => t.Title == taskTitle).ShouldBeTrue(); 41 } 42 } 43 }
你可以深入檢查 HTML 的更多細節。但一般來說,檢查基本的標簽就夠了。
其他相關內容
第二篇 Second article 接着開發這個應用服務。
(第二篇翻譯鏈接)
文章修改歷史
- 2017-07-30: 將文章中的 ListResultOutput 替換為 ListResultDto
- 2017-06-02: 將文章和解決方案修改為支持 .net core
- 2016-08-08: 添加 第二篇 的鏈接
- 2016-08-01: 第一次發布
版權所有
該文章和其中的任何源代碼和文件的版權均歸 The Code Project Open License (CPOL) 所有