如何使用ASP.NET Core、EF Core、ABP(ASP.NET Boilerplate)創建分層的Web應用程式(第一部分)

来源:https://www.cnblogs.com/yixuanhan/archive/2018/08/03/9396488.html
-Advertisement-
Play Games

本文是為了學習ABP的使用,是翻譯ABP官方文檔的一篇實戰教程,我暫時是優先翻譯自己感興趣或者比較想學習的部分,後續有時間希望能將ABP系列翻譯出來,除了自己能學習外,有可能的話希望幫助一些英文閱讀能力稍微差一點的同學(當然我自己也不一定翻譯的多好,大家共同學習)。 其實這篇文章也花了我一些時間,突 ...


本文是為了學習ABP的使用,是翻譯ABP官方文檔的一篇實戰教程,我暫時是優先翻譯自己感興趣或者比較想學習的部分,後續有時間希望能將ABP系列翻譯出來,除了自己能學習外,有可能的話希望幫助一些英文閱讀能力稍微差一點的同學(當然我自己也不一定翻譯的多好,大家共同學習)。

其實這篇文章也花了我一些時間,突然感嘆其實寫文章挺不容易的,這次雖然是翻譯,基本內容都是尊重原文的意思翻譯,但是裡面的每一句代碼我都自己寫了也運行測試了,截圖都是自己運行的結果。

這個ABP框架真的挺不錯的,已經有很多人也已經翻譯了,但是好像都是以前的,但是官網有些更新可能沒同步,而且自己翻譯覺得記憶更深刻一些。

接受來自任何小伙伴任何方面的好評與差評!!!!!!!!!!!!!

官網原文鏈接:https://aspnetboilerplate.com/Pages/Documents/Articles/Introduction-With-AspNet-Core-And-Entity-Framework-Core-Part-1/index.html

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

在本文中,我將展示如何使用以下工具創建一個簡單的跨平臺分層web應用程式:

  • .Net Core作為基本的跨平臺應用程式的開發框架
  • ABP(ASP.NET Boilerplate)作為啟動模板和應用框架
  • ASP.NET Core作為Web 框架
  • Entity FrameWork作為ORM框架
  • BootStrap作為HTML/Css框架
  • jQuery作為客戶端Ajax/Dom庫
  • xUnit 和 Shouldly用來對伺服器端的單元/集成測試

我還將Log4NetAutoMapper,這些已經預設包含在ABP模板中。

將要用到的技術(這些技術我們暫時都不做延伸的解釋,後續有時間會有專門的文章進行說明):

  • 分層體繫結構
  • 領域驅動設計(DDD)
  • 依賴註入(DI)
  • 集成測試

我們將要開發一個任務管理的應用程式,任務可以進行分配給某些人。在這裡,我們不用自己一層一層的去開發應用程式,而是在應用程式增長時切換到垂直層。隨著應用程式的發展,我將根據需要介紹ABP和其他框架的一些特性。

前期準備

要運行和開發此示例,請提前在機器上安裝下列工具:

  • Visual Studio 2017
  • SQL Server(可以將連接字元串更改為localdb)
  • Visual Studio擴展:
    • Bundler & Minifier
    • Web Compile

創建應用程式

使用ABP的啟動模板(http://www.aspnetboilerplate.com/Templates)來創建一個名為“acme simpletaskapp”的新web應用程式。公司名稱(這裡的“Acme”)是可選的。我們選擇多頁Web應用程式(Multi Page Web Application),在這裡為了保證最基本的啟動模板功能,我們也不選擇SPA,並且禁用了身份驗證。

它創建了一個分層的解決方案,如下所示:

它包含6個以我們創建模板時輸入的項目名稱開頭的項目。

  • .Core項目用於領域/業務層(實體、領域服務…)
  • .Appilcation項目為應用層(dtos,應用服務…)
  • .EntityFramework項目用於EF Core集成(從其他層抽象EF Core)
  • .Web項目就是ASP.Net MVC
  • .Tests項目用於單元測試和集成測試(直到應用層,不包括web層)
  • .Web.Tests用來對ASP.NET Core的集成測試(包括web層的完整的集成測試)

運行一下應用程式,可以看到如下界面:

它包含一個頂部菜單,空的主頁和About頁面和一個切換語言下拉選項。

開發應用程式

創建一個Task實體

我想從一個簡單的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
    }
}
  • Task繼承ABP的Entity類,它預設包含int類型的Id屬性。我們可以使用通用版本Entity<TPrimaryKey>來選擇不同的PK類型。
  • IHasCreationTime是一個簡單的介面,它只定義了CreationTime屬性(為CreationTime使用一個標準名稱非常好)。
  • Task實體定義一個必填的Title和一個可選的Description。
  • TaskState是一個簡單的定義任務狀態的枚舉。
  • Clock.Now預設情況下返回DateTime.Now,但是它提供了一個抽象,所以有需要的話很容易的切換到DateTime.UtcNow。如果使用ABP框架,請用Clock.Now,而不是DateTime.Now
  • 將Task實體存儲到資料庫中的AppTasks表中。

添加任務到DbContext

.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; }
    }
  • GetAllTasksInput DTO定義了GetAll方法的輸入參數,我沒有直接將狀態作為方法參數,而是將它添加到DTO對象中。這樣的話,之後我們可以在DTO中添加其他參數,而不需要影響現有的客戶端邏輯。
  • TaskListDto用於返回任務數據。它繼承自定義了一個Id屬性的EntityDto(我們可以將Id添加到Dto中,而不是從EntityDto派生出來),我們定義[AutoMapFrom]屬性來創建從任務實體到TaskListDto的自動映射。這個屬性在Abp.AutoMapper nuget包中定義。
  • 最後,ListResultDto是一個包含項目列表的簡單類(我們可以直接返回一個列表)。

現在我們可以去實現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繼承自包含在ABP啟動模板中的SimpleTaskSystemAppServiceBase(它繼承於ABP的ApplicationService類),這不是必需的,應用程式服務可以是普通類。但是ApplicationService基類有一些預先註入的服務(如此處使用的ObjectMapper)。
  • 我用依賴註入去獲得一個 repository
  • Repositories用來抽象對實體的資料庫操作,ABP為每個執行公共任務的實體創建一個預定義的存儲庫(如這裡的 IRepository<Task>), IRepository.GetAll()返回查詢實體的IQueryable。
  • WhereIf 是ABP中的擴展方法,用來簡化IQueryable.Where
  • ObjectMapper(來自ApplicationServiceBase類,預設情況下通過AutoMapper實現)用於將任務對象列表映射到TaskListDtos對象列表中。

測試TaskAppService

在進一步創建用戶界面之前,我想測試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啟動模板預設安裝了xUnitShouldly ,所以我們才可以直接使用。

創建任務列表視圖

現在,我知道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,我們可以修改他們,也可以自己創建新的頁面,在這裡我選擇新創建頁面。

 創建TaskController 和 ViewModel

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);
        }
    }
}
  • TasksController繼承SimpleTaskSystemControllerBase(繼承AbpController),SimpleTaskSystemControllerBase包含此應用程式中控制器的通用基本代碼。
  • 為獲得任務列表我註入了ITaskAppService
  • 我沒有直接將GetAll方法的結果傳遞給視圖,而是在.Web項目中創建了一個IndexViewModel類,如下所示:
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>

我們只是簡單的使用給定的模型以及Bootstraplist 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();
        }
    }

GetResponseAsStringAsyncGetUrl方法是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,但是在大多數情況下,檢查基本標簽就足夠了。

 

後面我會更新翻譯第二部分。。。。。。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • public static string CleanHtml(string strHtml) { strHtml = Regex.Replace(strHtml, @"(\)|(\)", "", RegexOptions.IgnoreCase | RegexOptions.Singleline); ... ...
  • 這次遇到一個需求,就是將整個界面列印在A4紙上。 需求清楚後,Bing一下關於列印,就找打一個類PrintDialog ,其中兩個方法可能會用到: 特別是public void PrintVisual(Visual visual, string description)可以直接傳一個控制項就能列印出來 ...
  • ZedGraph設置輔助線 1.一般來說ZedGraph設置參考線可以用 ZedGraph對象.YAxis.MajorGrid.IsVisible = True '水平參考線 ZedGraph對象.XAxis.MajorGrid.IsVisible = True '垂直參考線 2.就是通過在ZedG ...
  • Jquery AJAX POST與GET之間的區別 Jquery AJAX POST與GET之間的區別 GET 就是一個相同的URL只有一個結果,瀏覽器直接就可以拿出來進行獲取,比如抓取介面get方式的內容,或者說直接獲取網站源碼,可以使用get進行抓取,所以說get主要是用來獲取/抓取。 Ajax ...
  • 1. Swagger是什麼? Swagger 是一個規範和完整的框架,用於生成、描述、調用和可視化 RESTful 風格的 Web 服務。總體目標是使客戶端和文件系統作為伺服器以同樣的速度來更新。文件的方法,參數和模型緊密集成到伺服器端的代碼,允許API來始終保持同步。Swagger 讓部署管理和使 ...
  • 0.簡介 事件匯流排就是訂閱/發佈模式的一種實現,本質上事件匯流排的存在是為了降低耦合而存在的。 從上圖可以看到事件由發佈者發佈到事件匯流排處理器當中,然後經由事件匯流排處理器調用訂閱者的處理方法,而發佈者和訂閱者之間並沒有耦合關係。 像 Windows 本身的設計也是基於事件驅動,當用戶點擊了某個按鈕,那 ...
  • 在我們平時項目中經常會遇到定時任務,比如定時同步數據,定時備份數據,定時統計數據等,定時任務我們都知道使用Quartz.net,此系列寫的也是Quartz,但是在此之前,我們先用其他方式做個簡單的定時任務進行入門。 首先呢,我們現在自己先寫一個簡單的定時迴圈任務,話不多說,直接上代碼: 第一步:創建 ...
  • 專為解答C#初級問題 QQ 群 731738614 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...