65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 ASP.NET Core、Entity Framework Core 和 ASP.NET Boilerplate 创建多层 Web 应用程序(第二部分)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (18投票s)

2016年8月8日

CPOL

5分钟阅读

viewsIcon

58656

downloadIcon

420

一个基于 ASP.NET Core、Entity Framework Core 和 ASP.NET Boilerplate 框架,并包含自动化测试的多层 Web 应用程序创建分步指南。

目录

引言

这是“使用 ASP.NET Core、Entity Framework Core 和 ASP.NET Boilerplate 创建多层 Web 应用程序”文章系列的第二部分。查看其他部分

开发应用程序

创建 Person 实体

我将向应用程序添加 **Person** 概念,以便 **将任务分配** 给人员。因此,我定义了一个简单的 **Person 实体**

[Table("AppPersons")]
public class Person : AuditedEntity<Guid>
{
    public const int MaxNameLength = 32;

    [Required]
    [MaxLength(MaxNameLength)]
    public string Name { get; set; }

    public Person()
    {
            
    }

    public Person(string name)
    {
        Name = name;
    }
}

这次,我将 Id(主键)类型设置为 **Guid**,以作演示。我还从 **AuditedEntity**(具有 CreationTime、CreaterUserId、LastModificationTime 和 LastModifierUserId 属性)继承,而不是直接继承自基类 Entity。

将 Person 关联到 Task 实体

我还向 **Task** 实体添加了 **AssignedPerson** 属性(此处仅共享更改的部分)

[Table("AppTasks")]
public class Task : Entity, IHasCreationTime
{
    //...

    [ForeignKey(nameof(AssignedPersonId))]
    public Person AssignedPerson { get; set; }
    public Guid? AssignedPersonId { get; set; }

    public Task(string title, string description = null, Guid? assignedPersonId = null)
        : this()
    {
        Title = title;
        Description = description;
        AssignedPersonId = assignedPersonId;
    }
}

AssignedPerson 是 **可选的**。因此,任务可以分配给一个人,也可以不分配。

将 Person 添加到 DbContext

最后,我将新的 Person 实体添加到 DbContext 类中

public class SimpleTaskAppDbContext : AbpDbContext
{
    public DbSet<Person> People { get; set; }
    
    //...
}

为 Person 实体添加新的迁移

现在,我在 **程序包管理器控制台** 中运行以下命令

Add new migration

这会在项目中创建一个新的迁移类

public partial class Added_Person : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "AppPersons",
            columns: table => new
            {
                Id = table.Column<Guid>(nullable: false),
                CreationTime = table.Column<DateTime>(nullable: false),
                CreatorUserId = table.Column<long>(nullable: true),
                LastModificationTime = table.Column<DateTime>(nullable: true),
                LastModifierUserId = table.Column<long>(nullable: true),
                Name = table.Column<string>(maxLength: 32, nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_AppPersons", x => x.Id);
            });

        migrationBuilder.AddColumn<Guid>(
            name: "AssignedPersonId",
            table: "AppTasks",
            nullable: true);

        migrationBuilder.CreateIndex(
            name: "IX_AppTasks_AssignedPersonId",
            table: "AppTasks",
            column: "AssignedPersonId");

        migrationBuilder.AddForeignKey(
            name: "FK_AppTasks_AppPersons_AssignedPersonId",
            table: "AppTasks",
            column: "AssignedPersonId",
            principalTable: "AppPersons",
            principalColumn: "Id",
            onDelete: ReferentialAction.SetNull);
    }

    //...
}

我已将 ReferentialAction.Restrict 更改为 ReferentialAction.SetNull。这样做的效果是:如果我删除了一个人,分配给该人的任务将变为未分配。这在此演示中并不重要。但我想展示您可以在需要时修改迁移代码。实际上,您应该始终在将生成的代码应用到数据库之前对其进行审查。之后,我们可以 **应用迁移** 到我们的数据库。

Update-Database

当我们打开数据库时,我们可以看到新表和列,并添加一些测试数据

Person table

我添加了一个 Person 并将其分配给了第一个任务

Tasks table

在任务列表中返回分配的 Person

我将更改 **TaskAppService** 以返回已分配的 Person 信息。首先,我向 **TaskListDto** 添加了两个属性

[AutoMapFrom(typeof(Task))]
public class TaskListDto : EntityDto, IHasCreationTime
{
    //...

    public Guid? AssignedPersonId { get; set; }

    public string AssignedPersonName { get; set; }
}

并将 Task.AssignedPerson 属性包含在查询中。刚刚添加了 **Include** 行

public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService
{
    //...

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

因此,GetAll 方法将返回任务的已分配 Person 信息。由于我们使用了 AutoMapper,新属性也将自动复制到 DTO。

更改单元测试以测试已分配的 Person

此时,我们可以更改单元测试,以查看在检索任务列表时是否检索到已分配的 Person。首先,我更改了 TestDataBuilder 类中的初始测试数据,将一个 Person 分配给一个任务。

public class TestDataBuilder
{
    //...

    public void Build()
    {
        var neo = new Person("Neo");
        _context.People.Add(neo);
        _context.SaveChanges();

        _context.Tasks.AddRange(
            new Task("Follow the white rabbit", "Follow the white rabbit in order to know the reality.", neo.Id),
            new Task("Clean your room") { State = TaskState.Completed }
            );
    }
}

然后,我将更改 TaskAppService_Tests.Should_Get_All_Tasks() 方法,以检查检索到的任务中是否有分配了 Person(请参阅最后添加的行)。

[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);
    output.Items.Count(t => t.AssignedPersonName != null).ShouldBe(1);
}

注意:Count 扩展方法需要 *using System.Linq;* 语句。

在任务列表页面显示已分配的 Person 名称

最后,我们可以更改 **Tasks\Index.cshtml** 以显示 **AssignedPersonName**

@foreach (var task in Model.Tasks)
{
    <li class="list-group-item">
        <span class="pull-right label label-lg @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") | @(task.AssignedPersonName ?? L("Unassigned"))
        </div>
    </li>
}

运行应用程序时,我们可以在任务列表中看到它

Task list with person name

创建任务的新应用程序服务方法

我们可以列出任务,但我们还没有 **任务创建页面**。首先,向 **ITaskAppService** 接口添加一个 **Create** 方法

public interface ITaskAppService : IApplicationService
{
    //...

    System.Threading.Tasks.Task Create(CreateTaskInput input);
}

并在 TaskAppService 类中实现它

public class TaskAppService : SimpleTaskAppAppServiceBase, ITaskAppService
{
    private readonly IRepository<Task> _taskRepository;

    public TaskAppService(IRepository<Task> taskRepository)
    {
        _taskRepository = taskRepository;
    }

    //...

    public async System.Threading.Tasks.Task Create(CreateTaskInput input)
    {
        var task = ObjectMapper.Map<Task>(input);
        await _taskRepository.InsertAsync(task);
    }
}

Create 方法自动将给定的输入 **映射** 到 Task 实体,并使用存储库将其插入数据库。**CreateTaskInput** DTO 如下所示

using System;
using System.ComponentModel.DataAnnotations;
using Abp.AutoMapper;

namespace Acme.SimpleTaskApp.Tasks.Dtos
{
    [AutoMapTo(typeof(Task))]
    public class CreateTaskInput
    {
        [Required]
        [MaxLength(Task.MaxTitleLength)]
        public string Title { get; set; }

        [MaxLength(Task.MaxDescriptionLength)]
        public string Description { get; set; }

        public Guid? AssignedPersonId { get; set; }
    }
}

配置它以映射到 Task 实体(使用 AutoMapTo 属性)并添加数据注释以应用 验证。我们使用了 Task 实体中的常量来使用相同的最大长度。

测试任务创建服务

我在 TaskAppService_Tests 类中添加了一些集成测试来测试 Create 方法

using Acme.SimpleTaskApp.Tasks;
using Acme.SimpleTaskApp.Tasks.Dtos;
using Shouldly;
using Xunit;
using System.Linq;
using Abp.Runtime.Validation;

namespace Acme.SimpleTaskApp.Tests.Tasks
{
    public class TaskAppService_Tests : SimpleTaskAppTestBase
    {
        private readonly ITaskAppService _taskAppService;

        public TaskAppService_Tests()
        {
            _taskAppService = Resolve<ITaskAppService>();
        }

        //...

        [Fact]
        public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title()
        {
            await _taskAppService.Create(new CreateTaskInput
            {
                Title = "Newly created task #1"
            });

            UsingDbContext(context =>
            {
                var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1");
                task1.ShouldNotBeNull();
            });
        }

        [Fact]
        public async System.Threading.Tasks.Task Should_Create_New_Task_With_Title_And_Assigned_Person()
        {
            var neo = UsingDbContext(context => context.People.Single(p => p.Name == "Neo"));

            await _taskAppService.Create(new CreateTaskInput
            {
                Title = "Newly created task #1",
                AssignedPersonId = neo.Id
            });

            UsingDbContext(context =>
            {
                var task1 = context.Tasks.FirstOrDefault(t => t.Title == "Newly created task #1");
                task1.ShouldNotBeNull();
                task1.AssignedPersonId.ShouldBe(neo.Id);
            });
        }

        [Fact]
        public async System.Threading.Tasks.Task Should_Not_Create_New_Task_Without_Title()
        {
            await Assert.ThrowsAsync<AbpValidationException>(async () =>
            {
                await _taskAppService.Create(new CreateTaskInput
                {
                    Title = null
                });
            });
        }
    }
}

第一个测试创建了一个带有 **标题** 的任务,第二个测试创建了一个带有 **标题** 和 **已分配 Person** 的任务,最后一个测试尝试创建一个 **无效** 任务以显示 **异常** 情况。

任务创建页面

我们知道 TaskAppService.Create 工作正常。现在,我们可以创建一个页面来添加新任务。最终页面将是这样的

Create task page

首先,我在 TaskController 中添加了一个 **Create** 操作,以准备上述页面

using System.Threading.Tasks;
using Abp.Application.Services.Dto;
using Acme.SimpleTaskApp.Tasks;
using Acme.SimpleTaskApp.Tasks.Dtos;
using Acme.SimpleTaskApp.Web.Models.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Linq;
using Acme.SimpleTaskApp.Common;
using Acme.SimpleTaskApp.Web.Models.People;

namespace Acme.SimpleTaskApp.Web.Controllers
{
    public class TasksController : SimpleTaskAppControllerBase
    {
        private readonly ITaskAppService _taskAppService;
        private readonly ILookupAppService _lookupAppService;

        public TasksController(
            ITaskAppService taskAppService,
            ILookupAppService lookupAppService)
        {
            _taskAppService = taskAppService;
            _lookupAppService = lookupAppService;
        }

        //...
        
        public async Task<ActionResult> Create()
        {
            var peopleSelectListItems = (await _lookupAppService.GetPeopleComboboxItems()).Items
                .Select(p => p.ToSelectListItem())
                .ToList();

            peopleSelectListItems.Insert(0, new SelectListItem { Value = string.Empty, Text = L("Unassigned"), Selected = true });

            return View(new CreateTaskViewModel(peopleSelectListItems));
        }
    }
}

我注入了 ILookupAppService,它用于获取 Person 的下拉列表项。虽然我可以在这里直接注入并使用 IRepository<Person, Guid>,但我更喜欢这种方式来构建更好的分层和可重用性。ILookupAppService.GetPeopleComboboxItems 在应用程序层定义,如下所示

public interface ILookupAppService : IApplicationService
{
    Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems();
}

public class LookupAppService : SimpleTaskAppAppServiceBase, ILookupAppService
{
    private readonly IRepository<Person, Guid> _personRepository;

    public LookupAppService(IRepository<Person, Guid> personRepository)
    {
        _personRepository = personRepository;
    }

    public async Task<ListResultDto<ComboboxItemDto>> GetPeopleComboboxItems()
    {
        var people = await _personRepository.GetAllListAsync();
        return new ListResultDto<ComboboxItemDto>(
            people.Select(p => new ComboboxItemDto(p.Id.ToString("D"), p.Name)).ToList()
        );
    }
}

**ComboboxItemDto** 是一个简单的类(在 ABP 中定义),用于传输下拉列表项数据。TaskController.Create 方法仅使用此方法,并将返回的列表转换为 **SelectListItem**(在 AspNet Core 中定义)列表,并通过 CreateTaskViewModel 类传递给视图。

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace Acme.SimpleTaskApp.Web.Models.People
{
    public class CreateTaskViewModel
    {
        public List<SelectListItem> People { get; set; }

        public CreateTaskViewModel(List<SelectListItem> people)
        {
            People = people;
        }
    }
}

Create 视图显示如下

@using Acme.SimpleTaskApp.Web.Models.People
@model CreateTaskViewModel

@section scripts
{
    <environment names="Development">
        <script src="~/js/views/tasks/create.js"></script>
    </environment>

    <environment names="Staging,Production">
        <script src="~/js/views/tasks/create.min.js"></script>
    </environment>
}

<h2>
    @L("NewTask")
</h2>

<form id="TaskCreationForm">
    
    <div class="form-group">
        <label for="Title">@L("Title")</label>
        <input type="text" name="Title" class="form-control" placeholder="@L("Title")" required maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxTitleLength">
    </div>

    <div class="form-group">
        <label for="Description">@L("Description")</label>
        <input type="text" name="Description" class="form-control" placeholder="@L("Description")" maxlength="@Acme.SimpleTaskApp.Tasks.Task.MaxDescriptionLength">
    </div>

    <div class="form-group">
        @Html.Label(L("AssignedPerson"))
        @Html.DropDownList(
            "AssignedPersonId",
            Model.People,
            new
            {
                @class = "form-control",
                id = "AssignedPersonCombobox"
            })
    </div>

    <button type="submit" class="btn btn-default">@L("Save")</button>

</form>

我包含了如下定义的 **create.js**

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

        var _$form = $('#TaskCreationForm');

        _$form.find('input:first').focus();

        _$form.validate();

        _$form.find('button[type=submit]')
            .click(function(e) {
                e.preventDefault();

                if (!_$form.valid()) {
                    return;
                }

                var input = _$form.serializeFormToObject();
                abp.services.app.task.create(input)
                    .done(function() {
                        location.href = '/Tasks';
                    });
            });
    });
})(jQuery);

让我们看看这个 JavaScript 代码做了什么

  • 为表单准备 **验证**(使用 jquery validation 插件),并在单击“保存”按钮时进行验证。
  • 使用 **serializeFormToObject** Jquery 插件(在解决方案的 jquery-extensions.js 中定义)将表单数据转换为 JSON 对象(我在 _Layout.cshtml 中将 jquery-extensions.js 添加为最后一个脚本文件)。
  • 使用 **abp.services.task.create** 方法调用 TaskAppService.Create 方法。这是 ABP 的重要功能之一。我们可以在 Javascript 代码中像调用代码中的 Javascript 方法一样使用应用程序服务。请参阅 详细信息

最后,我在 *任务列表* 页面添加了一个“添加任务”按钮,以便导航到 *任务创建页面*

<a class="btn btn-primary btn-sm" asp-action="Create">@L("AddNew")</a>

删除主页和关于页面

如果我们不需要,可以从应用程序中删除主页和关于页面。为此,请首先像这样修改 **HomeController**

using Microsoft.AspNetCore.Mvc;

namespace Acme.SimpleTaskApp.Web.Controllers
{
    public class HomeController : SimpleTaskAppControllerBase
    {
        public ActionResult Index()
        {
            return RedirectToAction("Index", "Tasks");
        }
    }
}

然后删除 **Views/Home** 文件夹,并从 SimpleTaskApp**NavigationProvider** 类中删除菜单项。您还可以从本地化 JSON 文件中删除不必要的键。

源代码

您可以在此处获取最新的源代码 https://github.com/aspnetboilerplate/aspnetboilerplate-samples/tree/master/SimpleTaskSystem-Core

文章历史

  • 2018-02-14:将源代码升级到 ABP v3.4 并更新了下载链接。
  • 2017-07-30:在文章中将 ListResultOutput 替换为 ListResultDto。
  • 2017-06-02:更改了文章和解决方案以支持 .net core。
  • 2016-08-09:根据反馈修改了文章。
  • 2016-08-08:首次发布。
© . All rights reserved.