本文是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) 全部