本文是ABP官方文檔翻譯版,翻譯基於 3.2.5 版本javascript
官方文檔分四部分html
1、 教程文檔前端
2、ABP 框架java
3、zero 模塊jquery
4、其餘(中文翻譯資源)git
本篇是第一部分的第一篇。github
第一部分分三篇web
1-1 手把手引進門數據庫
1-2 進階apache
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 ,ABP 框架 建立Web 應用, PS: 自帶自動的測試模塊哦。
本文目錄以下:
介紹
前期準備
建立應用
正式開發
建立任務實體 Entity
將任務添加到數據庫上下文 DbContext
建立第一個數據遷移
建立數據庫
編寫任務服務
測試任務服務
任務列表展現
添加菜單
建立任務 Controller 和 視圖模型
任務列表頁面
本地化
任務過濾
任務列表頁面的自動化測試
其餘相關內容
文章修改歷史
版權全部
這是系列文章的第一部分:使用 ASP.NET Core, Entity Framework Core 和 ASP.NET Boilerplate 建立N層Web應用
在本文中,我將指導你們建立一個樣例(跨平臺的多層Web應用),該樣例會用到以下工具(請讀者提早準備):
ABP 框架中會默認使用 Log4Net 和 AutoMapper 。
咱們同時還會使用如下技術:
演示的開發項目是一個簡單的任務管理應用,用於將任務分配出去。我不會一層一層的進行開發,而是隨着應用的拓展直接切換所需的層次。隨着應用的推拓展,我將會介紹所需的ABP和其餘框架的特性。
開發樣例時須要如下工具,請提早在你的機器上進行安裝:
首先使用ABP模版(http://www.aspnetboilerplate.com/Templates)建立一個web應用項目,命名爲"Acme.SimpleTaskApp" 。建立模板時能夠設置本身的公司名稱(好比Acme)。
本樣例使用MPA(Multi Page Web Application)多頁面模式(注:即便用MVC和Razor技術)進行開發,本文不使用SPA(注:土牛的SPA是使用Angular)單頁面模式。同時爲了使用最基礎的開發模板功能,本文不使用Module Zero模塊。
ABP 模版會建立一個多層的解決方案,以下圖:
模板會根據輸入的名字自動建立6個項目。
以上是沒有選擇zero的項目結果,若是你選擇了zero,項目結構就會變成下圖:
當你把應用運行起來後,你會看到下圖所示的用戶界面:
這個應用包含一個頂級菜單欄,包含空的首頁,關於頁,還有一個語言的下拉選項。
咱們從建立一個簡單的任務實體 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 }
.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 }
如今,咱們能夠實現 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 。 若是你對自動化測試不感興趣的話,能夠忽略這個部分。
咱們的模板包含 .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 }
模板自帶兩個頁面:首頁和關於頁,如上代碼所示。咱們也能夠修改它們建立新的頁面。但如今咱們不修改首頁和關於頁,咱們建立新的菜單項。
咱們在 .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 }
代碼以下
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 接着開發這個應用服務。
(第二篇翻譯連接)
該文章和其中的任何源代碼和文件的版權均歸 The Code Project Open License (CPOL) 全部