本文是为了学习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,可是在大多数状况下,检查基本标签就足够了。
后面我会更新翻译第二部分。。。。。。