本文是爲了學習ABP的使用,是翻譯ABP官方文檔的一篇實戰教程,我暫時是優先翻譯本身感興趣或者比較想學習的部分,後續有時間但願能將ABP系列翻譯出來,除了本身能學習外,有可能的話但願幫助一些英文閱讀能力稍微差一點的同窗(固然我本身也不必定翻譯的多好,你們共同窗習)。javascript
其實這篇文章也花了我一些時間,忽然感嘆其實寫文章挺不容易的,此次雖然是翻譯,基本內容都是尊重原文的意思翻譯,可是裏面的每一句代碼我都本身寫了也運行測試了,截圖都是本身運行的結果。html
這個ABP框架真的挺不錯的,已經有不少人也已經翻譯了,可是好像都是之前的,可是官網有些更新可能沒同步,並且本身翻譯以爲記憶更深入一些。java
接受來自任何小夥伴任何方面的好評與差評!!!!!!!!!!!!!web
官網原文連接:https://aspnetboilerplate.com/Pages/Documents/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/index.htmlsql
--------------------------------------------------------------------------------------------------------------------------------------------------------------------數據庫
在本文中,我將展現如何使用如下工具建立一個簡單的跨平臺分層web應用程序:json
我還將Log4Net和AutoMapper,這些已經默認包含在ABP模板中。服務器
將要用到的技術(這些技術咱們暫時都不作延伸的解釋,後續有時間會有專門的文章進行說明):app
咱們將要開發一個任務管理的應用程序,任務能夠進行分配給某些人。在這裏,咱們不用本身一層一層的去開發應用程序,而是在應用程序增加時切換到垂直層。隨着應用程序的發展,我將根據須要介紹ABP和其餘框架的一些特性。框架
要運行和開發此示例,請提早在機器上安裝下列工具:
使用ABP的啓動模板(http://www.aspnetboilerplate.com/Templates)來建立一個名爲「acme simpletaskapp」的新web應用程序。公司名稱(這裏的「Acme」)是可選的。咱們選擇多頁Web應用程序(Multi Page Web Application),在這裏爲了保證最基本的啓動模板功能,咱們也不選擇SPA,而且禁用了身份驗證。
它建立了一個分層的解決方案,以下所示:
它包含6個以咱們建立模板時輸入的項目名稱開頭的項目。
運行一下應用程序,能夠看到以下界面:
它包含一個頂部菜單,空的主頁和About頁面和一個切換語言下拉選項。
我想從一個簡單的Task實體開始。因爲實體是域層的一部分,因此我將它添加到.Core項目中:
using Abp.Domain.Entities; using Abp.Domain.Entities.Auditing; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text; namespace Acme.SimpleTaskSystem { [Table("AppTasks")] public class Task : Entity, IHasCreationTime { public const int MaxTitleLength = 256; public const int MaxDescriptionLength = 64 * 1024; //64KB [Required] [MaxLength(MaxTitleLength)] public string Title { get; set; } [MaxLength(MaxDescriptionLength)] public string Description { get; set; } public DateTime CreationTime { get; set; } public TaskState State { get; set; } public Task() { CreationTime = Clock.Now; State = TaskState.Open; } public Task(string title, string description = null) : this() { Title = title; Description = description; } } public enum TaskState : byte { Open = 0, Completed = 1 } }
.EntityFrameworkCore項目預約義了一個DbContext,咱們應該在DbContext中添加一個Task實體的DbSet:
using Abp.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace Acme.SimpleTaskSystem.EntityFrameworkCore { public class SimpleTaskSystemDbContext : AbpDbContext { //Add DbSet properties for your entities... public DbSet<Task> Tasks { get; set; } public SimpleTaskSystemDbContext(DbContextOptions<SimpleTaskSystemDbContext> options) : base(options) { } } }
如今EF Core知道咱們已經有了一個Task實體。
咱們將建立一個初始的數據庫遷移來建立數據庫和AppTasks表,從Visual Studio打開包管理器控制檯並運行Add-Migration命令(默認項目必須是.EntityFrameworkCore項目):
此命令在.EntityFrameworkCore項目中建立一個Migrations文件夾,該文件夾包含遷移類和數據庫模型的快照:
自動生成的「Initial」遷移類以下所示:
using System; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Migrations; namespace Acme.SimpleTaskSystem.Migrations { public partial class Initial : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name: "AppTasks", columns: table => new { Id = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Title = table.Column<string>(maxLength: 256, nullable: false), Description = table.Column<string>(maxLength: 65536, nullable: true), CreationTime = table.Column<DateTime>(nullable: false), State = table.Column<byte>(nullable: false) }, constraints: table => { table.PrimaryKey("PK_AppTasks", x => x.Id); }); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable( name: "AppTasks"); } } }
從包管理器控制檯運行Update-Database命令建立數據庫:
這條命令將在本地sql server中建立一個名爲SimpleTaskSystemDb的數據庫,並執行遷移:
如今,我有一個Task實體和並在數據庫中有相應的表,咱們添加幾條示例數據:
注意,數據庫鏈接字符串定義在.Web項目中的appsettings.json文件中。
應用程序服務用於向表示層公開域邏輯,應用程序被表示層經過數據傳輸對象(DTO)做爲參數(若是有須要)調用,使用域對象執行某些特定的業務邏輯,並返回一個DTO到表示層(若是須要)。
咱們在.Application項目中建立一個應用程序服務TaskAppService,以執行與任務相關的應用程序邏輯,首先定義一個應用程序服務的接口。
public interface ITaskAppService : IApplicationService { Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input); }
定義接口不是必須的,可是建議用接口。做爲約定,在ABP中全部App服務都必須實現IApplicationService接口(它只是一個空的標記接口)。我建立了一個用於查詢任務的GetAll方法。爲此,我還定義瞭如下dto:
public class GetAllTasksInput { public TaskState? State { get; set; } } [AutoMapFrom(typeof(Task))] public class TaskListDto : EntityDto, IHasCreationTime { public string Title { get; set; } public string Description { get; set; } public DateTime CreationTime { get; set; } public TaskState State { get; set; } }
如今咱們能夠去實現ITaskAppService
using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Abp.Application.Services.Dto; using Abp.Domain.Repositories; using Abp.Linq.Extensions; using Microsoft.EntityFrameworkCore; namespace Acme.SimpleTaskSystem { public class TaskAppService : SimpleTaskSystemAppServiceBase, ITaskAppService { private readonly IRepository<Task> _taskRepository; public TaskAppService(IRepository<Task> taskRepository) { _taskRepository = taskRepository; } public async Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input) { var tasks = await _taskRepository .GetAll() .WhereIf(input.State.HasValue, t => t.State == input.State.Value) .OrderByDescending(t => t.CreationTime) .ToListAsync(); return new ListResultDto<TaskListDto>( ObjectMapper.Map<List<TaskListDto>>(tasks) ); } } }
在進一步建立用戶界面以前,我想測試TaskAppService。若是您對自動化測試不感興趣,能夠跳過這一部分。
啓動模板包含一個.Tests項目來測試咱們的代碼。它使用EF Core提供的內存數據庫來代替SQL SERVER.所以咱們的單元測試能夠在沒有真正的數據庫下工做,它爲每一個測試建立一個單獨的數據庫。所以,測試是相互隔離的。咱們可使用TestDataBuilder類在運行測試以前向內存數據庫添加一些初始測試數據。我更改TestDataBuilder代碼以下所示:
using Acme.SimpleTaskSystem.EntityFrameworkCore; namespace Acme.SimpleTaskSystem.Tests.TestDatas { public class TestDataBuilder { private readonly SimpleTaskSystemDbContext _context; public TestDataBuilder(SimpleTaskSystemDbContext context) { _context = context; } public void Build() { _context.Tasks.AddRange(new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality."), new Task("Clean your room") { State = TaskState.Completed }); } } }
能夠看下示例項目的源代碼,以瞭解TestDataBuilder在何處以及如何使用。我向dbcontext添加了兩個任務(其中一個已經完成)。我能夠編寫測試,假設數據庫中有兩個任務。個人第一個集成測試測試上面建立的TaskAppService.GetAll()方法:
using Shouldly; using System; using System.Collections.Generic; using System.Text; using Xunit; namespace Acme.SimpleTaskSystem.Tests { public class TaskAppService_Tests : SimpleTaskSystemTestBase { private readonly ITaskAppService _taskAppService; public TaskAppService_Tests() { _taskAppService = Resolve<ITaskAppService>(); } [Fact] public async System.Threading.Tasks.Task Should_Get_All_Tasks() { // act var output = await _taskAppService.GetAll(new GetAllTasksInput()); //Assert output.Items.Count.ShouldBe(2); } [Fact] public async System.Threading.Tasks.Task Should_Get_Filtered_Tasks() { //Act var output = await _taskAppService.GetAll(new GetAllTasksInput { State = TaskState.Open }); //Assert output.Items.ShouldAllBe(t => t.State == TaskState.Open); } } }
我建立了兩個不一樣的tests來測試GetAll()方法,如今咱們從VS打開測試資源管理器(Test\Windows\Test Explorer)來運行單元測試
兩個都成功了。注意ABP啓動模板默認安裝了xUnit 和 Shouldly ,因此咱們才能夠直接使用。
如今,我知道TaskAppService能夠正常工做,我能夠開始建立一個頁面來列出全部的任務。
添加一個新的菜單項
首先在頂部菜單中添加一個新的菜單
using Abp.Application.Navigation; using Abp.Localization; namespace Acme.SimpleTaskSystem.Web.Startup { /// <summary> /// This class defines menus for the application. /// </summary> public class SimpleTaskSystemNavigationProvider : NavigationProvider { public override void SetNavigation(INavigationProviderContext context) { context.Manager.MainMenu .AddItem( new MenuItemDefinition( PageNames.Home, L("HomePage"), url: "", icon: "fa fa-home" ) ).AddItem( new MenuItemDefinition( PageNames.About, L("About"), url: "Home/About", icon: "fa fa-info" ) ).AddItem(new MenuItemDefinition( "TaskList", L("TaskList"), url:"Tasks", icon:"fa fa-tasks")); } private static ILocalizableString L(string name) { return new LocalizableString(name, SimpleTaskSystemConsts.LocalizationSourceName); } } }
如上所示,Startup模板附帶兩個頁面:Home和About,咱們能夠修改他們,也能夠本身建立新的頁面,在這裏我選擇新建立頁面。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; namespace Acme.SimpleTaskSystem.Web.Controllers { public class TasksController : SimpleTaskSystemControllerBase { private readonly ITaskAppService _taskAppService; public TasksController(ITaskAppService taskAppService) { _taskAppService = taskAppService; } public async Task<ActionResult> Index(GetAllTasksInput input) { var output = await _taskAppService.GetAll(input); var model = new IndexViewModel(output.Items); return View(model); } } }
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Acme.SimpleTaskSystem.Web { public class IndexViewModel { public IReadOnlyList<TaskListDto> Tasks { get; } public IndexViewModel(IReadOnlyList<TaskListDto> tasks) { Tasks = tasks; } public string GetTaskLabel(TaskListDto task) { switch(task.State) { case TaskState.Open: return "label-success"; default: return "label-default"; } } } }
這個簡單的視圖模型在其構造函數中獲取任務列表(由ITaskAppService提供)。它還具備GetTaskLabel方法,該方法將在視圖中用於爲給定任務選擇Bootstrap標籤類。
建立任務列表頁
最後Index視圖頁以下所示:
@using Acme.SimpleTaskSystem.Web.Startup @model Acme.SimpleTaskSystem.Web.IndexViewModel @{ ViewBag.Title = L("TaskList"); ViewBag.ActiveMenu = PageNames.TaskList; //和SimpleTaskSystemNavigationProvider定義的菜單名字相匹配,以高亮顯示菜單項 } <h2>@L("TaskList")</h2> <div class="row"> <div> <ul class="list-group" id="TaskList"> @foreach(var task in Model.Tasks) { <li class="list-group-item"> <span class="pull-right label @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")</span> <h4 class="list-group-item-heading">@task.Title</h4> <div class="list-group-item-text"> @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss") </div> </li> } </ul> </div> </div>
咱們只是簡單的使用給定的模型以及Bootstrap的 list group組件去呈現視圖。在這裏,咱們使用了IndexViewModel.GetTaskLabel()方法來獲取任務的標籤類型。渲染的頁面是這樣的:
咱們在視圖中使用ABP框架的L方法,用於定義本地化字符串,咱們已經在.Core項目中的Localization/SourceFiles文件夾下將其定義在.json文件中。en本地化以下:
{ "culture": "en", "texts": { "HelloWorld": "Hello World!", "ChangeLanguage": "Change language", "HomePage": "HomePage", "About": "About", "Home_Description": "Welcome to SimpleTaskSystem...", "About_Description": "This is a simple startup template to use ASP.NET Core with ABP framework.", "TaskList": "TaskList", "Open": "open", "TaskState_Open": "Open", "TaskState_Completed": "Completed" } }
除了最後三行是新加的,其餘全是啓動模板自帶的,咱們能夠根據狀況進行刪除。
正如上面所示,TasksController實際上得到一個GetAllTasksInput,能夠用來過濾任務。咱們能夠在任務列表視圖中添加下拉菜單來過濾任務。這裏咱們將下拉菜單添加到標題標籤中:
<h2>@L("TaskList") <span class="pull-right"> @Html.DropDownListFor( model => model.SelectedTaskState, Model.GetTasksStateSelectListItems(LocalizationManager), new { @class = "form-control", id = "TaskStateCombobox" }) </span> </h2>
而後我在 IndexViewModel中增長SelectedTaskState屬性和GetTasksStateSelectListItems方法:
public TaskState? SelectedTaskState { get; set; } public List<SelectListItem> GetTasksStateSelectListItems(ILocalizationManager localizationManager) { var list = new List<SelectListItem> { new SelectListItem { Text = localizationManager.GetString(SimpleTaskSystemConsts.LocalizationSourceName, "AllTasks"), Value = "", Selected = SelectedTaskState == null } }; list.AddRange(Enum.GetValues(typeof(TaskState)) .Cast<TaskState>() .Select(state => new SelectListItem { Text = localizationManager.GetString(SimpleTaskSystemConsts.LocalizationSourceName, $"TaskState_{state}"), Value = state.ToString(), Selected = state == SelectedTaskState }) ); return list; }
在控制器中設置SelectedTaskState:
public async Task<ActionResult> Index(GetAllTasksInput input) { var output = await _taskAppService.GetAll(input); var model = new IndexViewModel(output.Items) { SelectedTaskState = input.State }; return View(model); }
如今,咱們能夠運行應用程序查看視圖右上角的combobox:
如今這個combobox 只是顯示出來了,還不能用,咱們如今寫一個javascript代碼當combobox值改變時從新請求和刷新任務列表。
咱們在.Web項目中建立wwwroot\js\views\tasks\index.js文件:
(function ($) { $(function () { var _$taskStateCombobox = $("#TaskStateCombobox"); _$taskStateCombobox.change(function () { location.href = '/Tasks?state' + _$taskStateCombobox.val(); }); }); })(jQuery)
在視圖中引用index.js以前,我使用了VS擴展Bundler & Minifier(這是在ASP.Net Core項目中縮小文件的默認方式,在vs->工具->擴展和更新->下載)來縮小腳本:
這將在.Web項目的bundleconfig.json的文件中自動添加以下代碼:
{ "outputFileName": "wwwroot/js/views/tasks/index.min.js", "inputFiles": [ "wwwroot/js/views/tasks/index.js" ] }
並建立一個縮小的index.min.js文件
每當index.js改變時,index.min.js也會自動改變,如今咱們將js文件加到對應的視圖中:
@section scripts { <environment names="Development"> <script src="~/js/views/tasks/index.js"></script> </environment> <environment names="Staging,Production"> <script src="~/js/views/tasks/index.min.js"></script> </environment> }
有了上面的代碼,咱們能夠在開發環境中使用index.js文件,在生產環境使用index.min.js文件,這是ASP.NET Core MVC項目中經常使用的方法。
咱們能夠建立繼承測試,並且這已經被集成到 ASP.NET Core MVC 基礎框架中。若是對自動化測試不感興趣的小夥伴能夠跳過這部分哦。
ABP框架中的 .Web.Tests項目是用來作測試的,我建立一個簡單的測試去請求TaskController.Index,而後看其如何響應:
public class TasksController_Tests: SimpleTaskSystemWebTestBase { [Fact] public async System.Threading.Tasks.Task Should_Get_Tasks_By_State() { //ACT var response = await GetResponseAsStringAsync( GetUrl<TasksController>(nameof(TasksController.Index), new { state = TaskState.Open } ) ); //assert response.ShouldNotBeNullOrWhiteSpace(); } }
GetResponseAsStringAsync和GetUrl方法是ABP框架中AbpAspNetCoreIntegratedTestBase類提供的輔助方法。咱們能夠直接使用Client (HttpClient的一個實例)屬性來發出請求,可是使用這些輔助類會更容易一些。
調試測試,能夠看到響應HTML:
這說明index頁面響應無異常,可是咱們可能還想知道返回的HTML是否是咱們所想要的,有一些庫能夠用來解析HTML。AngleSharp就是其中之一,它預裝在ABP啓動模板中的.Web.Tests項目中。因此我用它來檢查建立的HTML代碼:
//Get tasks from database var tasksInDatabase = await UsingDbContextAsync(async dbContext => { return await dbContext.Tasks .Where(t => t.State == TaskState.Open) .ToListAsync(); }); //Parse HTML response to check if tasks in the database are returned var document = new HtmlParser().Parse(response); var listItems = document.QuerySelectorAll("#TaskList li"); //Check task count listItems.Length.ShouldBe(tasksInDatabase.Count); //Check if returned list items are same those in the database foreach (var listItem in listItems) { var header = listItem.QuerySelector(".list-group-item-heading"); var taskTitle = header.InnerHtml.Trim(); tasksInDatabase.Any(t => t.Title == taskTitle).ShouldBeTrue(); }
咱們能夠更深刻和更詳細地檢查HTML,可是在大多數狀況下,檢查基本標籤就足夠了。
後面我會更新翻譯第二部分。。。。。。