手把手引进门之 ASP.NET Core Entity Framework Core(官方教程翻译版 版本3.2.5)

以下是手把手引进门教程,基于 ASP.NET Core, Entity Framework Core ,ABP 框架 创建Web 应用, PS: 自带自动的测试模块哦。

  • 样例下载 (上 github 的请自便)

介绍

这是系列文章的第一部分:使用 ASP.NET Core, Entity Framework Core 和 ASP.NET Boilerplate 创建N层Web应用 

在本文中,我将指导大家创建一个样例(跨平台的多层Web应用),该样例会用到如下工具(请读者提前准备):

  • Net Core  跨平台应用的基础开发框架

  • ASP.NET Boilerplate (ABP)  开发的基础框架模板

  • ASP.NET Core web开发框架

  • Entity Framework Core ORM 数据框架

  • Twitter Bootstrap HTML&CSS 前端开发框架

  • jQuery 客户端 AJAX/DOM 类库

  • xUnit 和 Shouldly 服务端测试工具(单元测试/集成测试)

 ABP 框架中会默认使用 Log4Net 和 AutoMapper 。

我们同时还会使用以下技术:

  • Layered Architecture  分层架构

  • Domain Driven Design (DDD)  DDD领域模型

  • Dependency injection (DI)  DI 依赖注入

  • Integration Testing  集成测试

演示的开发项目是一个简单的任务管理应用,用于将任务分配出去。我不会一层一层的进行开发,而是随着应用的拓展直接切换所需的层次。随着应用的推拓展,我将会介绍所需的ABP和其他框架的特性。

 

前期准备

开发样例时需要以下工具,请提前在你的机器上进行安装:

  • Visual Studio 2017

  • SQL Server (你可以更改连接字符串为 localdb)

  • Visual Studio Extensions:

    • Bundler & Minifier

    • Web Compiler

创建应用

首先使用ABP模版(http://www.aspnetboilerplate.com/Templates)创建一个web应用项目,命名为"Acme.SimpleTaskApp" 。创建模板时可以设置自己的公司名称(比如Acme)。

本样例使用MPA(Multi Page Web Application)多页面模式(注:即使用MVC和Razor技术)进行开发,本文不使用SPA(注:土牛的SPA是使用Angular)单页面模式。同时为了使用最基础的开发模板功能,本文不使用Module Zero模块。

 

ABP 模版会创建一个多层的解决方案,如下图:

 

模板会根据输入的名字自动创建6个项目。

  • core  领域层/业务层,包含实体Entity,领域服务 domain service 等等

  • Application 应用层 , 包含DTO,应用服务 application service 等等

  • Entity Framework 基础设施层 ,EF core 数据库集成处理 (从其他层抽象出来的EF core)

  • Web 展示层 , 即Asp.net MVC层

  • Tests 单元测试和集成测试,含应用层,领域层,基础设施层,不含Web展示层

  • Web.Tests ASP.NET Core集成测试,包含web展示层的全部集成测试

以上是没有选择zero的项目结果,如果你选择了zero,项目结构就会变成下图:

当你把应用运行起来后,你会看到下图所示的用户界面:

 

这个应用包含一个顶级菜单栏,包含空的首页,关于页,还有一个语言的下拉选项。

 

正式开发

创建任务实体 Entity

我们从创建一个简单的任务实体 Task Entity 开始,由于它属于领域层,把它加到 core 项目里。

 

代码如下: 

using System;

using System.ComponentModel.DataAnnotations;

using System.ComponentModel.DataAnnotations.Schema;

using Abp.Domain.Entities;

using Abp.Domain.Entities.Auditing;

using Abp.Timing;


namespace Acme.SimpleTaskApp.Tasks

{

    [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

    }

}

  • Task 实体从 ABP 的 Entity 基类继承,Entity 基类默认ID属性是 int 类型。如果主键类型为非 int 类型,也可以选择范型版本的 Entity<TPrimaryKey>.

  • IHasCreationTime 是一个简单的接口,只定义了 CreationTime 属性 (统一规范 CreationTime 的名字)

  • Task 实体定义了一个必填的 Title 和 非必填的 Description

  • TaskState 是一个简单枚举,定义了 Task 任务的状态

  • Clock.Now 返回默认的 DateTime.Now 。但它提供了一个抽象方法,使得我们可以在将来有需要的时候很轻松就可以转换为 DateTime.UtcNow 。在 ABP 框架中总是使用Clock.Now 而不使用 DateTime.Now 。

  • 将 Task 实体存储到数据库的 AppTasks 表中。

 

将任务添加到数据库上下文 DbContext

.EntityFrameworkCore 包含一个预定义的 DbContext 。将 Task 实体的 DbSet 加到 DbContext 里。

代码如下:

public class SimpleTaskAppDbContext : AbpDbContext

{

    public DbSet&lt;Task> Tasks { get; set; }


    public SimpleTaskAppDbContext(DbContextOptions&lt;SimpleTaskAppDbContext> options) 

        : base(options)

    {


    }

}

现在,EF core 知道我们有了一个 Task 的实体。

 

创建第一个数据迁移

我们将创建一个初始化数据库迁移文件,它会自动创建数据库和数据库表 AppTasks 。打开源管理器 Package Manager Console from Visual Studio , 执行 Add-Migration 命令(默认的项目必须是 .EntityFrameworkCore 项目),如图:

这个命令会在 . EntityFrameworkCore 项目下创建一个迁移( Migrations )文件夹,文件夹包含一个迁移类和数据库模型的快照,如图:

 

如下代码所示,自动创建了 “初始化 ( Initial )”迁移类:

public partial class Initial : Migration

{

    protected override void Up(MigrationBuilder migrationBuilder)

    {

        migrationBuilder.CreateTable(

            name: "AppTasks",

            columns: table => new

            {

                Id = table.Column&lt;int>(nullable: false)

                    .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),

                CreationTime = table.Column&lt;DateTime>(nullable: false),

                Description = table.Column&lt;string>(maxLength: 65536, nullable: true),

                State = table.Column&lt;byte>(nullable: false),

                Title = table.Column&lt;string>(maxLength: 256, nullable: false)

            },

            constraints: table =>

            {

                table.PrimaryKey("PK_AppTasks", x => x.Id);

            });

    }


    protected override void Down(MigrationBuilder migrationBuilder)

    {

        migrationBuilder.DropTable(

            name: "AppTasks");

    }

}

当我们执行数据库迁移命令时,这些代码会创建 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&lt;ListResultDto&lt;TaskListDto>> GetAll(GetAllTasksInput input);
4 }

我们推荐先定义接口,但不是非这样做不可。按照惯例,ABP 中所有的应用服务都需要实现 IApplicationService 接口 (它只是一个空的标记接口)。我们创建了一个 GetAll 方法去查询任务列表,同时,我们定义了如下的 DTOs :

 代码如下: 

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

}

  • GetAllTasksInput DTO 为 GetAll 应用服务方法定义了一个输入参数 。 我们将 状态 state 定义为 DTO 对象 而不定义为方法的参数。 这样我们将来需要的时候可以在这个DTO增加其他的参数,同时兼容现有的客户端 (当然我们也可以在方法里加一个 state 参数)。

  • TaskListDto 用开返回任务数据。该Dto 从 EntityDto 继承,EntityDto 只是定义了 Id 属性(我们可以不继承 EntityDto ,直接自己将 Id 加到我们的Dto里)。我们定义了[AutoMapFrom] 特性来创建 AutoMapper 自动映射任务实体到任务列表Dto TaskListDto 。这个特性在 Abp.AutoMapper nuget 包里进行了定义。

  • ListResultDto 是一个简单的类,包含了一个列表(我们可以直接返回一个 List<TaskListDto> 列表)。

现在,我们可以实现 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 Acme.SimpleTaskApp.Tasks.Dtos;

using Microsoft.EntityFrameworkCore;


namespace Acme.SimpleTaskApp.Tasks

{

    public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService

    {

        private readonly IRepository&lt;Task> _taskRepository;


        public TaskAppService(IRepository&lt;Task> taskRepository)

        {

            _taskRepository = taskRepository;

        }


        public async Task&lt;ListResultDto&lt;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&lt;TaskListDto>(

                ObjectMapper.Map&lt;List&lt;TaskListDto>>(tasks)

            );

        }

    }

}

  • TaskAppService 该类从 SimpleTaskAppAppServiceBase 继承,SimpleTaskAppAppServiceBase (从 ABP 的 ApplicationService 类继承)在模板里已经自动生成。 TaskAppService 不是必须从 SimpleTaskAppAppServiceBase 继承,应用服务可以是普通类。但是 ApplicationService 基类有一些预先注入的服务(就像这里使用的 ObjectMapper )

  • 我们使用依赖注入 dependency injection 来获取数据仓储 repository 

  • Repositories 数据仓储用于为数据实体抽象数据库操作。ABP 为每个实体创建了预定义的数据库仓储(就像这里用到了 IRepository<Task> )用于实现通用的任务。IRepository.GetAll() 方法用于查询数据实体,它返回了一个 IQueryable 接口。

  • WhereIf 这是 ABP 里的一个拓展方法,该方法提供了一个 IQueryable.Where 方法的简便条件语法。

  • ObjectMapper 用于将任务对象列表映射到任务列表Dto对象列表 (基于 Application Service 基类并默认实现 AutoMapper ) 

 

测试任务服务

在创建用户接口钱,我们需要测试一下任务应用服务 TaskAppService 。 如果你对自动化测试不感兴趣的话,可以忽略这个部分。

我们的模板包含 .Tests 项目,这可以测试我们的代码。这个项目不使用 SQL Server数据库,而是使用EF core 的内存数据库。所以,我们可以不用真实数据库来进行单元测试。它为每个测试创建了单独的数据库。所以每个测试都是隔离的。我们需要在开始测试前使用 TestDataBuilder 类添加初始测试数据到内存数据库里。我修改了 TestDataBuilder 。

代码如下:

public class TestDataBuilder

{

    private readonly SimpleTaskAppDbContext _context;


    public TestDataBuilder(SimpleTaskAppDbContext 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 在哪里用,具体怎么用。我们添加2个任务(其中一个已经完成)到数据库上下文 dbcontext 。我们可以假定数据库中有2个任务,开始编写测试用例。 第一个继承测试用来测试 TaskAppService.GetAll 方法。

代码如下:

public class TaskAppService_Tests : SimpleTaskAppTestBase

{

    private readonly ITaskAppService _taskAppService;


    public TaskAppService_Tests()

    {

        _taskAppService = Resolve&lt;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);

    }

}

我们创建2个不同的测试用例来测试 GetAll 方法。现在,我们打开测试浏览器(在VS主菜单的 Test\Windows\Test Explorer 菜单下)开始进行单元测试。

 

所有测试均成功。最后一个我们现在可以忽略它,他是一个模板生成的测试。

友情提示: ABP 模板默认安装使用 xUnit 和 Shouldly 。我们使用它们编写我们的测试。

 

任务列表展示 

现在,我们确定 TaskAppService 服务可以正常工作。 我们可以开始创建页面来展示所有的任务。

 

添加菜单

首先,我们在顶级菜单上添加一个新的菜单

代码如下

public class SimpleTaskAppNavigationProvider : NavigationProvider

{

    public override void SetNavigation(INavigationProviderContext context)

    {

        context.Manager.MainMenu

            .AddItem(

                new MenuItemDefinition(

                    "Home",

                    L("HomePage"),

                    url: "",

                    icon: "fa fa-home"

                    )

            ).AddItem(

                new MenuItemDefinition(

                    "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, SimpleTaskAppConsts.LocalizationSourceName);

    }

}

模板自带两个页面:首页和关于页,如上代码所示。我们也可以修改它们创建新的页面。但现在我们不修改首页和关于页,我们创建新的菜单项。

 

创建任务 Controller 和 视图模型

我们在 .Web 项目下创建一个新的 controller 类,命名为 TasksController 。

代码如下

public class TasksController : SimpleTaskAppControllerBase

{

    private readonly ITaskAppService _taskAppService;


    public TasksController(ITaskAppService taskAppService)

    {

        _taskAppService = taskAppService;

    }


    public async Task&lt;ActionResult> Index(GetAllTasksInput input)

    {

        var output = await _taskAppService.GetAll(input);

        var model = new IndexViewModel(output.Items);

        return View(model);

    }

}

  • TasksController 从 SimpleTaskAppControllerBase ( SimpleTaskAppControllerBase 从 AbpController 继承)继承,该类包含应用程序 Controllers 需要的通用基础代码。

  • 我们反射了 ITaskAppService , 以获取到所有的任务列表。

  • 我们在 .Web 项目中创建了一个 IndexViewModel 类来将数据展示到视图上,这样可以不直接将 GetAll 方法的结果直接暴露到视图上。

代码如下

public class IndexViewModel

{

    public IReadOnlyList&lt;TaskListDto> Tasks { get; }


    public IndexViewModel(IReadOnlyList&lt;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 视图。

代码如下

@model Acme.SimpleTaskApp.Web.Models.Tasks.IndexViewModel


@{

    ViewBag.Title = L("TaskList");

    ViewBag.ActiveMenu = "TaskList"; //Matches with the menu name in SimpleTaskAppNavigationProvider to highlight the menu item

}


&lt;h2>@L("TaskList")&lt;/h2>


&lt;div class="row">

    &lt;div>

        &lt;ul class="list-group">

            @foreach (var task in Model.Tasks)

            {

                &lt;li class="list-group-item">

                    &lt;span class="pull-right label @Model.GetTaskLabel(task)">@L($"TaskState_{task.State}")&lt;/span>

                    &lt;h4 class="list-group-item-heading">@task.Title&lt;/h4>

                    &lt;div class="list-group-item-text">

                        @task.CreationTime.ToString("yyyy-MM-dd HH:mm:ss")

                    &lt;/div>

                &lt;/li>

            }

        &lt;/ul>

    &lt;/div>

&lt;/div>

我们使用 Bootstrap 的 list group 组件和定义好的模型来渲染视图。我们使用 IndexViewModel.GetTaskLable() 方法来获得任务的标签类型。渲染后的界面如下图:

 

本地化

我们在视图里使用 ABP 框架自带的 L 方法。 它用于本地化语言。我们在 .Core 项目下的 Localization/Source 文件夹中定义好了本地化字符串,使用 .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

 

任务过滤

正如之前说过的,TaskController 实际上使用的是 GetAllTasksInput ,可以灵活的过滤任务。我们可以添加一个任务列表的下拉菜单来过滤任务。首先,我们添加一个下拉菜单到视图上(我们加到 header 里):

代码如下

&lt;h2>

    @L("TaskList")

    &lt;span class="pull-right">

        @Html.DropDownListFor(

           model => model.SelectedTaskState,

           Model.GetTasksStateSelectListItems(LocalizationManager),

           new

           {

               @class = "form-control",

               id = "TaskStateCombobox"

           })

    &lt;/span>

&lt;/h2>

然后我修改了 IndexViewModel , 增加了 SeletedTaskState 属性和 GetTaskStateSelectListItems 方法:

代码如下

public class IndexViewModel

{

    //...


    public TaskState? SelectedTaskState { get; set; }


    public List&lt;SelectListItem> GetTasksStateSelectListItems(ILocalizationManager localizationManager)

    {

        var list = new List&lt;SelectListItem>

        {

            new SelectListItem

            {

                Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, "AllTasks"),

                Value = "",

                Selected = SelectedTaskState == null

            }

        };


        list.AddRange(Enum.GetValues(typeof(TaskState))

                .Cast&lt;TaskState>()

                .Select(state =>

                    new SelectListItem

                    {

                        Text = localizationManager.GetString(SimpleTaskAppConsts.LocalizationSourceName, $"TaskState_{state}"),

                        Value = state.ToString(),

                        Selected = state == SelectedTaskState

                    })

        );


        return list;

    }

}

我们也可以在 controller 里设置 SelectedTaskState :

代码如下

public async Task&lt;ActionResult> Index(GetAllTasksInput input)

{

    var output = await _taskAppService.GetAll(input);

    var model = new IndexViewModel(output.Items)

    {

        SelectedTaskState = input.State

    };

    return View(model);

}

现在,我们运行程序,可以看到视图的右上角有个下拉框,如图:

我们添加了下拉框,但它现在还不能用。我们需要编写一些简单的 javascript 代码,当下拉框内容更改后可以重新请求/刷新任务列表页面。我们在 .Web 项目里创建了 wwwroot\js\views\tasks\index.js 文件

代码如下

(function ($) {

    $(function () {


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


        _$taskStateCombobox.change(function() {

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

        });


    });

})(jQuery);

我们首先添加 Bundler & Minifier 扩展程序(这是 ASP.NET Core 项目标配的压缩文件)来压缩脚本的大小, 然后开始在视图里编写 javascript :

这将在 .Web 项目中的 bundleconfig.json 中添加以下代码

代码如下

{

  "outputFileName": "wwwroot/js/views/tasks/index.min.js",

  "inputFiles": [

    "wwwroot/js/views/tasks/index.js"

  ]

}

同时创建了 script 的压缩版本

无论我何时修改了index.js , index.min.js 都会自动重新生成。现在,我们可以在我们的页面里插入 javascript 文件了。

代码如下

@section scripts

{

    &lt;environment names="Development">

        &lt;script src="~/js/views/tasks/index.js">&lt;/script>

    &lt;/environment>


    &lt;environment names="Staging,Production">

        &lt;script src="~/js/views/tasks/index.min.js">&lt;/script>

    &lt;/environment>

}

至此,我们的视图将在开发环境下使用 index.js 包,而在生产环境中使用 index.min.js (压缩版本)包。这是在 ASP.Net Core MVC 项目中通用的做法。

 

任务列表页面的自动化测试

ASP.NET Core MVC 基础框架中集成了一个继承测试模块。我们可以完整的测试我们的服务端代码了。如果你对自动化测试不感兴趣的话,你可以忽略这个部分。

ABP 模板中自带 .Web.Tests 项目。我们创建一个普通的测试来请求 TaskController.Index , 然后检查反馈内容:

代码如下

public class TasksController_Tests : SimpleTaskAppWebTestBase

{

    [Fact]

    public async System.Threading.Tasks.Task Should_Get_Tasks_By_State()

    {

        //Act


        var response = await GetResponseAsStringAsync(

            GetUrl&lt;TasksController>(nameof(TasksController.Index), new

                {

                    state = TaskState.Open

                }

            )

        );


        //Assert


        response.ShouldNotBeNullOrWhiteSpace();

    }

}

GetResponseAsStringAsync 和 GetUrl 是 ABP 的 AbpAspNetCoreIntrgratedTestBase 类中很有用的方法。使用这些快捷方法我们可以比较容易的创建请求,如果直接使用客户端请求(一个 HttpClient 的实例)会相对复杂一些。如果想深入了解,请参考 ASP.NET Core 的  integration testing documentation

当我们开始 debug 测试模块式,我们可以看到反馈的 HTML 如下图

上图显示 Index 页面的反馈很正常。但是,我们更想知道返回的 HTML 是否正如我们所预期的那样。 有很多类库可以用来解析 HTML 。ABP 模板的 .Web.Tests 项目预先安装了其中的一个类库 AngleSharp 我们用它来检查创建的 HTML 代码。

代码如下

public class TasksController_Tests : SimpleTaskAppWebTestBase

{

    [Fact]

    public async System.Threading.Tasks.Task Should_Get_Tasks_By_State()

    {

        //Act


        var response = await GetResponseAsStringAsync(

            GetUrl&lt;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().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 的更多细节。但一般来说,检查基本的标签就够了。 

其他相关内容

第二篇 Second article 接着开发这个应用服务。


原文地址:http://www.cnblogs.com/yabu007/p/8067694.html


.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/322451.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图像识别:微信跳一跳机器人

准备IDE&#xff1a;VisualStudioLanguage&#xff1a;VB.NET/C#GitHub&#xff1a;AutoJump.NET本文将向你介绍一种通过图像识别实现“跳一跳”机器人的方法。 第一节 图像识别文中提到的所有方法和步骤均仅涉及简单的向量计算。需要哪些计算&#xff1f;比较像素点的颜色求向…

(七)HTML和CSS 、JavaScript 和Java到底有什么区别,今天终于明白了!!!

有人曾经问过&#xff0c;“HTML CSS 和 JavaScript 以及 Java有什么区别” TCP VS UDP Java VS C 功能先上了再说 高级开发人员作为一个团队进行编程 调试CSS 高级开发人员重构代码 能一个手指完成的绝不用两只手 看实习生编码的时候&#xff0c;我的表情…… 当我尝试进入B…

基于Accord.Audio和百度语言识别

目标需求使用录音形式&#xff0c;模拟微信语音聊天。按住录音&#xff0c;松开发送语音&#xff0c;并完成语音识别。ps&#xff1a;百度的语言识别有60秒长度限制&#xff0c;需要自己做好控制。实现方案采用C# winform 程序实现桌面版&#xff0c;采用Accord 实现语音录制停…

(八)Spring与MyBatis整合

持久层 目录 Mybatis 开发步骤回顾Mybatis 开发中存在的问题Spring 与 Mybatis 整合思路Spring 与 Mybatis 整合的开发步骤Spring 与 Mybatis 整合的编码搭建开发环境 pom.xmlSpring 配置文件的配置编码Spring 与 Mybatis 整合细节持久层整合总述 1、Spring 框架为什么要与持…

Git 企业开发者教程

为什么要写这样一个面向企业开发者的Git教程&#xff1f;这个问题也困扰我自己很久。其实我使用git的时间也不短了&#xff0c;但是就和正在阅读本文的每一位一样&#xff0c;常用的基本就是那么几个(git clone, git push)等等。然而git其实有着非常强大的功能&#xff0c;如果…

基于百度理解与交互技术实现机器问答

一、前言我们都知道现在聊天对话机器是一个很有意思的东西&#xff0c;比如说苹果siri&#xff0c;比如说微软的小冰。聊天对话机器的应用场景也很广泛&#xff0c;比如说&#xff1a;银行的自助办卡机器人、展会讲解解说等等。我们对机器人说句话&#xff0c;机器人从听取&…

(十)Spring 与 MVC 框架整合

Spring 整合 MVC 目录 MVC 框架整合思想为什么要整合 MVC 框架搭建 Web 运行环境Spring 整合 MVC 框架的核心思路1. 准备工厂2. 代码整合Spring 整合 Struts2MVC 框架整合思想 为什么要整合 MVC 框架 MVC 框架提供了控制器&#xff08;Controller&#xff09;调用 Servlet …

利用VSTS跟Kubernetes整合进行CI/CD

为什么VSTS要搭配Kubernetes&#xff1f;通常我们在开发管理软件项目的时候都会碰到一个很头痛的问题&#xff0c;就是开发、测试、生产环境不一致&#xff0c;导致开发人员和测试人员甚至和运维吵架。因为常见的物理环境甚至云环境中&#xff0c;这些部署环境都是由运维人员提…

(十一)Spring 基础注解(对象创建相关注解、注入相关注解)

注解编程 目录 注解基础概念注解的作用Spring 注解的发展历程Spring 基础注解&#xff08;Spring 2.x&#xff09;对象创建相关注解ComponentRepository、Service、ContollerScopeLazy生命周期注解 PostConstruct、PreDestroy注入相关注解用户自定义类型 AutowiredJDK 类型注…

使用 ASP.NET Core, Entity Framework Core 和 ABP 创建N层Web应用 第二篇

介绍这是“使用 ASP.NET Core &#xff0c;Entity Framework Core 和 ASP.NET Boilerplate 创建N层 Web 应用”系列文章的第二篇。以下可以看其他篇目&#xff1a;使用 ASP.NET Core &#xff0c;Entity Framework Core 和 ASP.NET Boilerplate 创建N层 Web 应用 第一篇 &…

揭秘微软6万工程师DevOps成功转型的技术「武器」

在微软&#xff0c;通过其自身数年的 DevOps 转型&#xff0c; 6 万名工程师实现了更好的软件平台创新和快速迭代。微软有庞大的技术产品矩阵&#xff0c;同时也具有每天发布的能力&#xff0c;其中&#xff0c;微软研发云是支撑整个开发过程与运维最重要的基础平台。微软研发云…

Flowable学习笔记(一、入门)

转载自 Flowable学习笔记&#xff08;一、入门&#xff09; 一、Flowable简介 1、Flowable是什么 Flowable是一个使用Java编写的轻量级业务流程引擎。Flowable流程引擎可用于部署BPMN 2.0流程定义&#xff08;用于定义流程的行业XML标准&#xff09;&#xff0c; 创建这些流…

01-MyBatis入门程序

MyBatis入门程序 目录 1. 下载 Mybatis 核心包2. 创建工程&#xff0c;引入 MyBatis 核心包及依赖包3. 创建 customer 表&#xff0c;建立与表对应的 domain使用 lombok&#xff0c;开启注解创建 Customer 类4. 创建 MyBatis 核心配置文件 SqlMappingConfig.xml5. 创建表对象…

角落的开发工具集之Vs(Visual Studio)2017插件推荐

“ 工具善其事&#xff0c;必先利其器&#xff01;装好这些插件让vs更上一层楼”因为最近录制视频的缘故&#xff0c;很多朋友都在QQ群留言&#xff0c;或者微信公众号私信我&#xff0c;问我一些工具和一些插件啊&#xff0c;怎么使用的啊&#xff1f;那么今天我忙里偷闲整理一…

02-MyBatis配置SQL打印

MyBatis 配置SQL打印 在 SqlMappingConfig.xml 中配置以下代码&#xff1a; <!--配置sql打印--> <settings><setting name"logImpl" value"STDOUT_LOGGING"/> </settings>运行效果&#xff1a;会显示 SQL 语句&#xff0c;查询结…

Flowable学习笔记(二、BPMN 2.0-基础 )

转载自 Flowable学习笔记&#xff08;二、BPMN 2.0-基础 &#xff09; 1、BPMN简介 业务流程模型和标记法&#xff08;BPMN, Business Process Model and Notation&#xff09;是一套图形化表示法&#xff0c;用于以业务流程模型详细说明各种业务流程。 它最初由业务流程管理…

ASP.NET Core文件上传与下载(多种上传方式)

前言前段时间项目上线,实在太忙,最近终于开始可以研究研究ASP.NET Core了.打算写个系列,但是还没想好目录,今天先来一篇,后面在整理吧.ASP.NET Core 2.0 发展到现在,已经很成熟了.下个项目争取使用吧.正文1.使用模型绑定上传文件(官方例子)官方机器翻译的地址:https://docs.mic…

03-映射文件的sql语句中 #{} 和 ${} 的区别以及实现模糊查询

映射文件的sql语句中 #{} 和 ${} 区别以及实现模糊查询 目录 sql 语句中的 #{}#{} 模糊查询错误用法#{} 实现模糊查询sql 语句中的 ${}${} 实现模糊查询#{} 与 ${} 对比sql 语句中的 #{} 表示一个占位符号&#xff0c;通过 #{} 可以实现 preparedStatement 向占位符中设置值…

SpringBoot集成Flowable

一、项目结构 二、maven配置 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.a…

04-插入操作更新操作删除操作

保存更新删除 目录 插入操作获取插入的最后一个id更新操作删除操作插入操作 映射文件 Customer.xml &#xff1a; 插入数据的标签为 insert&#xff0c;与查询 select 区分开来。 parameterType 是输入参数类型&#xff0c;这里指定为 Customer 对象&#xff0c;即需要传入一…