如何使用ABP开发一

本文是 Introduction With AspNet Core And Entity Framework Core Part 1 的翻译版本,有少量改动,可以参考原文

 

本文将介绍如何从数据库设计开始,设计并实现一个基本的任务管理应用。这个应用具有查询任务,修改任务指派人员等简单功能。

系统准备

Visual Studio 2017

SQL Server or MySql (修改abp数据库为mysql

VS扩展(用于前端资源打包,再具体用到时展开):

创建应用

从这里下载模板(http://www.aspnetboilerplate.com/Templates),取一个自己喜欢的名字,比如“myAbpBasic”,不要勾选include login...选项,并下载到本地。由于不熟悉spa框架angularjs等,所以选择了mpa应用。下载完成后解压,用vs2017打开sln。

项目结构如下图

现在可以运行这个项目,可以看到一个不包含登录功能的简单应用,有首页,about页,右上角还有多语言选项。

Template Home Page

开发步骤

创建任务实体

我们设想有一个任务实体,每个任务有执行人这个属性,执行人id字段作为外键关联到执行人表。首先我们先实现任务实体的增删改查功能。

数据实体属于领域层的范围,所以我们将实体添加到Core这个项目。

Task实体代码如下:

using Abp.Domain.Entities;
using Abp.Domain.Entities.Auditing;
using Abp.Timing;
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using myAbpBasic.People;

namespace myAbpBasic.Tasks
{
    [Table("AppTasks")]
    public class Task : Entity, IHasCreationTime
    {
        public const int MaxTitleLength = 256;
        public const int MaxDescriptionLength = 64 * 1024; //64KB

        [Required]
        [StringLength(MaxTitleLength)]
        public string Title { get; set; }

        [StringLength(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, Guid? assignedPersonId = null)
            : this()
        {
            Title = title;
            Description = description; 
        }
    }

    public enum TaskState : byte
    {
        Open = 0,
        Completed = 1
    }
}

 

 这里,我们定义了以下信息:

  • 表名称AppTasks
  • 两个常量:title、描述字段的最大长度;
  • 4个字段,title,description,creationtime,state,
  • task构造函数中给creationtime、state赋予默认值,然后重载构造函数,初始化task实体时,可以直接传入title,description(其中description非必填);
  • 一个枚举类型,用于定义任务状态
  • 实体继承了ABP的基础类型Entity类,它包含了Id属性,默认类型为int,可以使用重载类型Entity<TPrimaryKey>来使用其他的主键类型,如Entity<Guid>.
  • IHasCreationTime接口仅仅定义了CreationTime属性(为了更好地统一名称,继承该接口后需要在当前类实现该接口)
  • Clock.Now方法返回当前时间DateTime.Now,但因为抽象了一层,如果需要的话,我们可以在未来轻松地替换为DateTime.UtcNow等其他实现。
  • 建议安装ReSharper神器,这里用到了反编译功能。

定义DbContext

定义好entity之后,我们需要告诉dbcontext有Task这么一个实体类型。

DbContext类代码如下:

using Abp.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using myAbpBasic.People;
using myAbpBasic.Tasks;

namespace myAbpBasic.EntityFrameworkCore
{
    public class myAbpBasicDbContext : AbpDbContext
    {
        //Add DbSet properties for your entities...
        public DbSet<Task> Tasks { get; set; }

        public myAbpBasicDbContext(DbContextOptions<myAbpBasicDbContext> options)
            : base(options)
        {

        }
    }
}

创建Database Migration

由于默认为code-first模式,所以定义好entity后我们需要生成migration并同步到数据库

打开程序包管理器控制台

务必选择项目为EFCore项目

执行完成后会在当前项目Migrations文件夹下生成增量文件(多余文件是后续教程中生成的),文件内容如下:

using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;

namespace myAbpBasic.Migrations
{
    public partial class task : Migration
    {
        protected override void Up(MigrationBuilder migrationBuilder)
        {
            migrationBuilder.CreateTable(
                name: "AppTasks",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.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");
        }
    }
}

创建数据库

继续回到程序包管理器,选择EFCore项目,执行“Update-Database”命令即可。完成后数据库中会产生AppTask表,并和我们在Entity中定义的字段一致。

随意添加几条数据,用于后面实现查询页面数据展示

注:连接字符串位于Web项目appsettings.json配置文件

{
  "ConnectionStrings": {
    "Default": "Server=192.168.8.150;Port=3306;Database=abp;Uid=root;Pwd=123456;SslMode=none;"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    }
  }
}

应用服务层(Application Service)

这里所说的服务不同于常见的微服务,服务化,而是一个应用层的另一个说法,因为我们在应用层也会抽象接口,并定义接口实现,该接口可以被展现层调用,所以有时候也称应用层为应用服务层。

应用服务层的作用是从领域层获取数据实体,做一些业务逻辑处理,并将数据转换为Data Transfer Object(DTO)实体,然后传递给展现层。展现层不存在数据实体,数据实体在到达应用服务层后就消失了,转换为Dto后到达展现层。

我们在应用层定义一个ITaskAppService接口,TaskAppService实现,相关的Dto模型,一般而言,把task相关的都放在一个文件夹下

在ITaskAppService中定义一个GetAll查询接口

using Abp.Application.Services;
using Abp.Application.Services.Dto;
using myAbpBasic.Tasks.Dto;
using System.Threading.Tasks;

namespace myAbpBasic.Tasks
{
    public interface ITaskAppService : IApplicationService
    {
        Task<ListResultDto<TaskListDto>> GetAll(GetAllTasksInput input);

    }


}

官方文档提到:定义接口并不是必须的,但我们建议这么做。约定俗成地,所有应用服务必须实现IApplicationService接口(一个空的接口标记,猜测用于依赖注入)。

同时我们也定义了相关的Dto模型。官方文档把dto分别放在多个类中,此处我把所有task相关的dto模型放在TasksDto类中。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Text;
using Abp.Application.Services.Dto;
using Abp.AutoMapper;
using Abp.Domain.Entities.Auditing;

namespace myAbpBasic.Tasks.Dto
{

    [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; }
    }

    public class GetAllTasksInput
    {
        public TaskState? State { get; set; }
    }

  
}
  • GetAllTasksInput 模型是GetAll接口的参数。尽管这个dto模型只有一个状态属性,但我们也不建议直接使用state字段作为参数,而是应该把state放到dto中,把dto作为参数,这样就可以为dto任意添加其他参数字段,增强灵活性。
  • TaskListDto 是返回查询任务结果的dto,它继承了EntityDto(包含了id属性)
  • ListResultDto 是一个数据集合,也可以直接使用List<TaskListDto>
  • 约定俗成的,从展现层传递过来的dto模型一般以input结尾,从应用层返回的dto模型一般以Dto结尾。

下面是ITaskAppService的具体实现

using Abp.Application.Services.Dto;
using Abp.Domain.Repositories;
using Abp.Linq.Extensions;
using Microsoft.EntityFrameworkCore;
using myAbpBasic.Tasks.Dto;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace myAbpBasic.Tasks
{
    public class TaskAppService : myAbpBasicAppServiceBase, 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()
                .Include(t => t.AssignedPerson)
                .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 继承了myAbpBasicAppServiceBase类,非必须,但myAbpBasicAppServiceBase中有一些预先注入的服务,例如ObjectMapper类
  • 使用依赖注入来取得Repository
  • Repositories 是数据库操作的抽象,使用者无需关注具体实现
  • WhereIf 是ABP的一个扩展方法,用于简化IQueryable.Where方法的使用
  • ObjectMapper 具体实现依赖于AutoMapper,用于实体类型转换。
  • 再次提醒,安装Resharper或其他反编译工具来查看相关源码,个人推荐ReSharper

单元测试TaskAppService

单元测试,在开发过程中是及其重要的,它可以局部验证我们的函数是否有正确的输出,避免在整合前端时才发现bug,可以极大地从总体上提高开发效率。此处可以详细展开单元测试、自动化测试的相关内容,但是限于篇幅,只介绍ABP的测试项目如何使用。阅读本节前,请先了解单元测试的相关基本概念。

回到整个解决方案,test文件夹下有两个测试项目,分别对应后台AppService层和前端Web层的测试

ABP已经包含了测试项目用于测试代码。它使用了EF Core提供的内存数据库(EF Core In-Memory Database Provider),而不是直接使用SQL Server(真实数据库数据不可控),这样我们的测试工作就可以脱离真实数据库进行。各个测试之间的内存数据库都是隔离的,不会相互影响,这样测试用例之间是相互隔离的。我们用TestDataBuilder类向内存数据库中写入初始数据用于运行测试,代码如下:

using myAbpBasic.EntityFrameworkCore;
using myAbpBasic.People;
using myAbpBasic.Tasks;

namespace myAbpBasic.Tests.TestDatas
{
    public class TestDataBuilder
    {
        private readonly myAbpBasicDbContext _context;

        public TestDataBuilder(myAbpBasicDbContext 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是在什么地方如何使用的,请阅读项目源码。 在上述代码中,我们往内存数据库的AppTask表写入两条数据,分别Title为“Follow the white rabbit”和“Clean your room”的两条记录,其中第二条状态为已完成。所以当编写测试用例时,可以认为数据库中已经存在这两条数据。我们先创建下面两个单元测试(代码位于TaskAppService_Tests.cs):

using myAbpBasic.Tasks;
using myAbpBasic.Tasks.Dto;
using Shouldly;
using System.Linq;
using Abp.Runtime.Validation;
using Xunit;

namespace myAbpBasic.Tests.Tasks
{
    public class TaskAppService_Tests : myAbpBasicTestBase
    {
        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);
        }
    }
}

译者注:测试用例一般包含两部分代码,Act数据操作,以及Assert结果验证。如第一个用例,Act获取了Task表所有数据,Assert验证数据条数是否为2.第二个用例,Act获取状态为open的所有记录,Assert验证是否全部为open状态的记录。

测试代码左侧会有快捷按钮(xUnit插件提供的功能),也可以通过测试-窗口-测试资源管理器查看所有测试项目,进行测试。

注意:ABP集成了xUnit和Shouldly插件。xUnit是测试框架,和MSTest作用相同,但功能更丰富。Shouldly是轻量断言(Assertion)框架,它将焦点放在当断言失败时如何简单精准的给出很好的错误信息。还提供了ShouldBe,ShouldNotBe,ShouldAllBe等扩展方法。这里是更详细的官方文档

任务列表视图

应用服务开发、测试完成后,我们就正式开始前端展现层开发了。

添加菜单

给顶部菜单创建一个新的页面

using Abp.Application.Navigation;
using Abp.Localization;

namespace myAbpBasic.Web.Startup
{
    /// <summary>
    /// This class defines menus for the application.
    /// </summary>
    public class myAbpBasicNavigationProvider : 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, myAbpBasicConsts.LocalizationSourceName);
        }
    }
}

初始模板包含了两个页面,首页Home,关于About。在SetNavigation方法中新增一项AddItem.分别定义菜单名称,本土化翻译后的名称,指向地址,图标。本土化翻译将在后面详细解释。 

创建控制器和视图模型

在Web项目创建TasksController

using Microsoft.AspNetCore.Mvc;
using myAbpBasic.Tasks;
using myAbpBasic.Tasks.Dto;
using myAbpBasic.Web.Models;
using System.Threading.Tasks;
using myAbpBasic.Common;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Linq;
using Abp.Application.Services.Dto;

namespace myAbpBasic.Web.Controllers
{
    public class TasksController : myAbpBasicControllerBase
    {
        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);
        }
    }
}
  • TasksController继承了myAbpBasicControllerBase,这个控制器基类定义了一些常用的基础代码。
  • 此处注入了ITaskAppService来获取任务列表
  • 我们使用一个新建的IndexViewModel来传递查询结果,而不是直接返回GetAll方法返回的变量。IndexViewModel代码如下:

using System;
using Microsoft.AspNetCore.Mvc.Rendering;
using myAbpBasic.Tasks;
using System.Collections.Generic;
using System.Linq;
using Abp.Localization;
using myAbpBasic.Tasks.Dto;

namespace myAbpBasic.Web.Models
{
    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";
            }
        }
    }
}

 这个简单的视图模型在它的构造函数取得了任务数据集合,它同时也提供GetTaskLabel方法,用于MVC视图中获取任务状态对应的Bootstrap样式。

创建视图

在TasksController控制器的Index方法上右键-创建视图,代码如下:

@model myAbpBasic.Web.Models.IndexViewModel

@{
    ViewBag.Title = L("TaskList");
    ViewBag.ActiveMenu = "TaskList"; //Matches with the menu name in SimpleTaskAppNavigationProvider to highlight the menu item
}
@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>
}
<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组件。我们使用刚才视图模型提供的GetTaskLabel()方法来获取label样式。运行起来,渲染后的页面样式如下:

你看到的样式可能与上图有出入,比如Task List上有方括号,也不会出现Add New按钮和右侧的All Tasks筛选框。没关系,这都会在后面小节讲到。这时候你会发现,右上角的Localization并不会起作用,下面我们就讲讲怎么使用Localization功能。

 

本土化翻译

ABP基础框架Abp.AspNetCore.Mvc.Views.AbpRazorPage类提供了一个L方法,用于本土化翻译。翻译文本定义在Core项目/Localization/Source文件夹,默认只包含了英语和土耳其语。

打开myAbpBasic.json,并新增任务视图中用到的三个新的本土化索引(最后三个)

{
  "culture": "en",
  "texts": {
    "HelloWorld": "Hello World!",
    "ChangeLanguage": "Change language",
    "HomePage": "HomePage",
    "About": "About",
    "Home_Description": "Welcome to SimpleTaskApp...",
    "About_Description": "This is a simple startup template to use ASP.NET Core with ABP framework.",
    "TaskList": "Task List",
    "TaskState_Open": "Open",
    "TaskState_Completed": "Completed"
  }
}

 Abp的本土化翻译功能并不复杂,想了解更多可以参考官方文档 Localization Document

拓展阅读:我们修改一下以支持中文

复制一个myAbpBasic.json,并重命名为myAbpBasic-zh-Hans.json。修改myAbpBasicLocalizationConfigurer

using System.Reflection;
using Abp.Configuration.Startup;
using Abp.Localization;
using Abp.Localization.Dictionaries;
using Abp.Localization.Dictionaries.Json;
using Abp.Reflection.Extensions;

namespace myAbpBasic.Localization
{
    public static class myAbpBasicLocalizationConfigurer
    {
        public static void Configure(ILocalizationConfiguration localizationConfiguration)
        {
            localizationConfiguration.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flags england", isDefault: true));
            localizationConfiguration.Languages.Add(new LanguageInfo("tr", "Türkçe", "famfamfam-flags tr"));
            localizationConfiguration.Languages.Add(new LanguageInfo("zh-Hans", "简体中文", "famfamfam-flags cn"));

            localizationConfiguration.Sources.Add(
                new DictionaryBasedLocalizationSource(myAbpBasicConsts.LocalizationSourceName,
                    new JsonEmbeddedFileLocalizationDictionaryProvider(
                        typeof(myAbpBasicLocalizationConfigurer).GetAssembly(),
                        "myAbpBasic.Localization.SourceFiles"
                    )
                )
            );
        }
    }
}

这样运行起来右上角就有了中文的选项,最后把zh-Hans.json文件翻译为中文就可以了!

 

{
  "culture": "zh-Hans",
  "texts": {
    "HelloWorld": "Hello World!",
    "ChangeLanguage": "更换语言",
    "HomePage": "主页",
    "About": "关于",
    "Home_Description": "欢迎来到myAbpBasic...。我是谁??",
    "About_Description": "这特么就是一个简单的基于ASP.Net Core的初始模板!没别的卵东西!!",
    "TaskList": "任务列表",
    "TaskState_Open": "进行中",
    "TaskState_Completed": "已完成",
    "AllTasks": "全部",
    "Unassigned": "未指派",
    "AddNew": "添加",
    "NewTask": "添加任务",
    "Title": "标题",
    "Description": "描述",
    "AssignedPerson": "指派人员",
    "Save": "保存",
    "PersonList": "人员列表"
  }
}

条件过滤

刚才我们加载了全部的任务列表,控制器Index方法的参数GetAllTasksInput可以用于传递过滤条件。

现在增加一个下拉框过滤,可以按状态过滤任务。在index视图h2中添加以下代码:

<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方法:

using System;
using Microsoft.AspNetCore.Mvc.Rendering;
using myAbpBasic.Tasks;
using System.Collections.Generic;
using System.Linq;
using Abp.Localization;
using myAbpBasic.Tasks.Dto;

namespace myAbpBasic.Web.Models
{
    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";
            }
        }


        //以下为本次新增代码
        public TaskState? SelectedTaskState { get; set; }

        public List<SelectListItem> GetTasksStateSelectListItems(ILocalizationManager localizationManager)
        {
            var list = new List<SelectListItem>
            {
                new SelectListItem
                {
                    Text = localizationManager.GetString(myAbpBasicConsts.LocalizationSourceName, "AllTasks"),
                    Value = "",
                    Selected = SelectedTaskState == null
                }
            };

            list.AddRange(Enum.GetValues(typeof(TaskState))
                .Cast<TaskState>()
                .Select(state =>
                    new SelectListItem
                    {
                        Text = localizationManager.GetString(myAbpBasicConsts.LocalizationSourceName, $"TaskState_{state}"),
                        Value = state.ToString(),
                        Selected = state == SelectedTaskState
                    })
            );

            return list;
        }
    }
}

修改控制器中Index方法,设置State

    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);
        }

这时候运行项目已经会出现下拉框,但改变后数据并不会改变。我们需要写js代码来触发页面刷新

 

(function ($) {
    $(function () {

        var _$taskStateCombobox = $('#TaskStateCombobox');

        _$taskStateCombobox.change(function() {
            location.href = '/Tasks?state=' + _$taskStateCombobox.val();
        });

    });
})(jQuery);

 下一步是把js文件引入到页面中。此处需要介绍一下 Bundler & Minifier 插件,这是一个打包程序方便将js,css,html文件混淆的插件。选择需要混淆的index.js,右键选择Minify File.

Minify js

操作完成后,程序将自动在Web项目根目录下bundleconfig.json文件中添加以下内容:

{
  "outputFileName": "wwwroot/js/views/tasks/index.min.js",
  "inputFiles": [
    "wwwroot/js/views/tasks/index.js"
  ]
}

 同时创建了index.min.js

回到index视图,引入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.

 

自动化测试任务列表页

我们可以为mvc项目创建集成测试,这样我们就能测试全部服务端代码。如果你对自动化测试没有兴趣,可以跳过本节。

P.S. 译者不建议跳过,完善的单元测试、集成测试可以显著提高开发完成度,减少后续测试时间。

测试项目为Web.Tests,创建TasksController_Tests

ABP的AbpAspNetCoreIntegratedTestBase类提供了一些基础方法来发起http请求、获取请求地址。这些封装的方法可以简化代码,提高开发效率。

Web test

从接口返回的response为html,我们用ABP预置的AngleSharp插件来解析html。完整代码如下:

using AngleSharp.Html.Parser;
using Microsoft.EntityFrameworkCore;
using myAbpBasic.Tasks;
using myAbpBasic.Web.Controllers;
using Shouldly;
using System.Linq;
using Xunit;

namespace myAbpBasic.Web.Tests.Controllers
{
    public class TasksController_Tests : myAbpBasicWebTestBase
    {

        [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();

            //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().ParseDocument(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中发掘更多信息,但是绝大多数情况下,检查一些标志性的标签就已经足够了。

 

总结

本篇介绍了开发时如何编写数据库、实体、应用服务、MVC的相关代码,但只是简单的单表查询、过滤,并未涉及多表关联、增删改等功能,下一篇将介绍这些内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值