ASP.NET Boilerplate 入门






4.96/5 (175投票s)
ASP.NET Boilerplate 是一个使用最佳实践和最流行工具构建现代 Web 应用程序的起点。
目录
- 引言
- 什么是 ASP.NET Boilerplate?
- ASP.NET Boilerplate 不是什么?
- 入门
- 从模板创建空 Web 应用程序
- 领域层
- 基础设施层
- 应用层
- 表示层
- 更多
- 关于 AngularJS & EntityFramework 示例
- 摘要
- 文章历史
- 参考文献
引言
重要提示:虽然本文档介绍了使用 Durandal 开发用户界面,但已无法从 http://aspnetboilerplate.com 创建 Durandal 模板。但本文档对于希望在 ABP 框架中使用 NHibernate 作为 ORM 的用户以及理解 ABP 的用户仍然有用。单击 此处 查看本文档的 Angular & EntityFramework 版本。
DRY - Don't Repeat Yourself!是优秀开发人员在开发软件时遵循的主要思想之一。我们努力将其从简单的方法实现到类和模块。那么开发新的基于 Web 的应用程序呢?我们软件开发人员在开发企业 Web 应用程序时有类似的需求。
企业 Web 应用程序需要登录页面、用户/角色管理基础结构、用户/应用程序设置管理、本地化等。此外,高质量和大规模的软件会实现最佳实践,如分层架构、领域驱动设计(DDD)、依赖注入(DI)。此外,我们使用工具来进行对象关系映射(ORM)、数据库迁移、日志记录等等。当涉及到用户界面(UI)时,情况也大相径庭。
启动一个新的企业 Web 应用程序是一项艰巨的任务。由于所有应用程序都需要一些共同的任务,因此我们一直在重复自己。许多公司正在开发自己的应用程序框架或库来处理这些共同任务,以避免重新开发相同的东西。另一些则复制现有应用程序的部分内容,并为新应用程序准备起点。如果您的公司足够大并且有时间开发这样的框架,那么第一种方法就非常好。
作为一名软件架构师,我也曾在我的公司开发过这样的框架。但是,有些时候我感到不舒服:许多公司重复相同的任务。如果我们能共享更多,重复更少呢?如果 DRY 原则能在通用层面而不是每个项目或每个公司层面实现呢?这听起来有些不切实际,但我认为这可能是一个起点!
什么是 ASP.NET Boilerplate?
ASP.NET Boilerplate [1] 是一个使用最佳实践和最流行工具构建新的现代 Web 应用程序的起点。它旨在成为一个坚实模型、一个通用应用程序框架和一个项目模板。它做什么?
- 服务器端
- 基于最新的ASP.NET MVC和Web API。
- 实现了领域驱动设计(实体、存储库、领域服务、应用服务、DTO、工作单元……等等)。
- 实现了分层架构(领域、应用、表示和基础设施层)。
- 提供了开发可重用和可组合模块以用于大型项目的机制。
- 使用了最流行的框架/库,就像您可能已经在使用的那样。
- 提供了机制并简化了依赖注入的使用(使用 Castle Windsor 作为 DI 容器)。
- 提供了严格的模型和基类,以便轻松使用对象关系映射(直接支持 EntityFramework 和 NHibernate)。
- 支持并实现了数据库迁移。
- 包含一个简单灵活的本地化系统。
- 包含一个用于服务器端全局领域事件的事件总线。
- 管理异常处理和验证。
- 为应用服务创建动态 Web API 层。
- 提供基类和帮助类以实现一些常见任务。
- 遵循“约定优于配置”原则。
- 客户端
- 提供项目模板,用于单页应用程序(使用AngularJS和DurandalJS)和多页应用程序。模板基于 Twitter Bootstrap。
- 默认包含并配置了大多数常用的 JavaScript 库。
- 创建动态 JavaScript 代理以轻松调用应用服务(使用动态 Web API 层)。
- 包含一些常见任务的独特 API:显示警报和通知、阻塞 UI、进行 AJAX 请求……
除了这些通用基础设施之外,还有一个名为 zero 的模块正在开发中。它将提供基于角色和权限的授权系统(实现最新的 ASP.NET Identity Framework)、一个设置系统、多租户等。
ASP.NET Boilerplate 不是什么?
ASP.NET Boilerplate 提供了一个带有最佳实践的应用程序开发模型。它具有基类、接口和工具,可以轻松构建可维护的大型应用程序。但是……
- 它不是 RAD(快速应用程序开发)工具之一,这类工具试图为构建应用程序提供无需编码的基础设施。相反,它提供了一个遵循最佳实践进行编码的基础设施。
- 它不是一个代码生成工具。虽然它有许多在运行时构建动态代码的特性,但它并不生成代码。
- 它不是一个一体化的框架。相反,它为特定任务使用了知名的工具/库(如 NHibernate 和 EntityFramework 用于 O/RM,Log4Net 用于日志记录,Castle Windsor 作为 DI 容器,AngularJS 作为 SPA 框架)。
入门
在本文中,我将展示如何使用 ASP.NET Boilerplate(我将从现在开始称之为ABP)开发一个单页响应式 Web 应用程序。我将在此处使用DurandalJS作为 SPA 框架,并使用NHibernate作为 ORM 框架。我准备了另一篇文章,其中使用AngularJS和EntityFramework实现了相同的应用程序。
这个示例应用程序名为“简单任务系统”,它包含两个页面:一个用于任务列表,另一个用于添加新任务。任务可以与一个人相关联,可以处于活动状态或已完成状态。该应用程序支持两种语言的本地化。下面是应用程序中任务列表的截图。
从模板创建空 Web 应用程序
ABP 为新项目提供启动模板(即使您可以手动创建项目并从 nuget 获取 ABP 包,模板方式也更简单)。访问 www.aspnetboilerplate.com/Templates 以从模板创建您的应用程序。您可以选择带有 AngularJS 或 DurandalJS 的 SPA(单页应用程序)项目。或者您可以选择 MPA(经典,多页应用程序)项目。然后,您可以选择 EntityFramework 或 NHibernate 作为 ORM 框架。
我将项目命名为SimpleTaskSystem,并创建了一个使用 Durandal 和 NHibernate 的SPA项目。将项目下载为zip文件。当我打开 zip 文件时,我看到一个已准备就绪的解决方案,其中包含领域驱动设计每个层的程序集(项目)。
创建的项目运行环境是.NET Framework 4.5.1,我建议使用Visual Studio 2013打开。运行项目所需的唯一先决条件是创建一个数据库。SPA 模板假定您正在使用SQL Server 2008 或更高版本。但您可以轻松将其更改为其他 DBMS。请参阅 Web 项目的 web.config 文件中的连接字符串。
<add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystemDb; Trusted_Connection=True;" />
您可以在此处更改连接字符串。我没有更改数据库名称,因此我将在 SQL Server 中创建一个名为SimpleTaskSystemDb的空数据库。
就是这样,您的项目已准备就绪,可以运行了!在 VS2013 中打开它并按 F5。
模板包含两个页面:一个是主页,另一个是关于页面。它支持英语和土耳其语本地化。而且它是单页应用程序!尝试在页面之间导航,您会看到只有内容在变化,导航菜单是固定的,所有脚本和样式只加载一次。而且它是响应式的。尝试更改浏览器的大小。
现在,我将一层一层地展示如何将应用程序更改为简单的任务系统应用程序。
领域层
“负责表示业务的概念、业务状况信息和业务规则”(Eric Evans)[2]。在领域驱动设计(DDD)中,核心层是领域层。领域层定义了您的实体、实现了您的业务规则等等。
实体
实体是 DDD 的核心概念之一。Eric Evans 将其描述为“一个其本质不是由其属性定义,而是由连续性和身份的线索定义的*对象*”。因此,实体具有 Id,并存储在数据库中。
我的第一个实体是Task。
public class Task : Entity<long>
{
public virtual Person AssignedPerson { get; set; }
public virtual string Description { get; set; }
public virtual DateTime CreationTime { get; set; }
public virtual TaskState State { get; set; }
public Task()
{
CreationTime = DateTime.Now;
State = TaskState.Active;
}
}
这是一个简单的类,继承自Entity基类,具有long主键类型。TaskState是一个枚举,包含“Active”和“Completed”成员。第二个实体是Person。
public class Person : Entity
{
public virtual string Name { get; set; }
}
一个任务可以与一个人相关联,对于这个简单的应用程序来说,这就是全部。
实体在 ABP 中实现了IEntity<TPrimaryKey>接口。因此,如果您的主键类型是 long,它必须实现IEntity<long>。如果您的 Entity 的主键是int,您可以不定义主键类型,直接实现IEntity接口。在实践中,您可以轻松地继承自Entity或Entity<TPrimaryKey>类,如上所示(Task 和 Person)。IEntity 为 Entity 定义了Id属性。
存储库
“使用类似集合的接口来访问领域对象,并在领域和数据映射层之间进行中介”(Martin Fowler)[3]。在实践中,存储库用于对领域对象(实体或值类型)执行数据库操作。
通常,每个实体(或聚合根)都有一个单独的存储库。ASP.NET Boilerplate 为每个实体提供默认存储库(我们将看到如何使用默认存储库)。如果我们 G 需要定义其他方法,可以扩展 IRepository 接口。我为 Task 存储库扩展了它。
public interface ITaskRepository : IRepository<Task, long>
{
List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
}
为每个存储库定义一个接口是个好主意。这样,我们就可以分离接口与实现。IRepository接口为存储库定义了最通用的方法。
它定义了基本的CRUD方法。因此,所有存储库都自动实现所有这些方法。除了标准的基方法外,您还可以添加此存储库特有的方法,我在此定义了GetAllWithPeople方法。
关于命名空间
当您研究示例应用程序的源代码时,您会发现类是按意图打包的,领域相关的类/接口/枚举在同一个命名空间中,而不是按基础设施打包。我会将Task类放在 TaskSystem.Entities 命名空间,ITaskRepository放在 TaskSystem.Repositories 命名空间,TaskState放在 TaskSystem.Enums 命名空间,依此类推……相反,我将所有这些类放在TTaskSystem.Tasks命名空间,因为所有这些都相互关联。这更符合领域驱动设计的本质。因此,我认为将所有实体放在 TaskSystem.Entities 命名空间不是一个好做法。有些人可能不这么认为。我可以理解,因为我直到不久前还在这样做。但我已经看到了问题,我强烈建议将相关的类/接口/枚举放在同一个命名空间中,也许是不同的程序集,但相同的命名空间。您可以阅读 Eric Evans 的领域驱动设计[2]一书中的“基础设施驱动打包的陷阱”部分。
基础设施层
“提供支持更高层级的通用技术能力”(Eric Evans)。它用于使用第三方库和框架(如对象关系映射)来实现应用程序的抽象。在此应用程序中,我将使用基础设施层进行
- 使用FluentMigrator创建数据库迁移系统。
- 使用NHibernate和FluentNHibernate实现存储库和映射实体。
数据库迁移
“进化式数据库设计:在过去的几年里,我们开发了许多技术,允许数据库设计在应用程序开发过程中不断演进。这对敏捷方法来说是一项非常重要的能力。”Martin Fowler 在他的网站 [3] 上说道。数据库迁移是一项支持这一思想的重要技术。如果没有这些技术,很难在多个生产环境中维护应用程序的数据库。即使只有一个活动系统,它也是至关重要的。
FluentMigrator [4] 是一个用于数据库迁移的优秀工具。它支持大多数常用数据库系统。在此,我提供了Person和Task表的迁移代码。
[Migration(2014041001)]
public class _01_CreatePersonTable : AutoReversingMigration
{
public override void Up()
{
Create.Table("StsPeople")
.WithColumn("Id").AsInt32().Identity().PrimaryKey().NotNullable()
.WithColumn("Name").AsString(32).NotNullable();
Insert.IntoTable("StsPeople")
.Row(new { Name = "Douglas Adams" })
.Row(new { Name = "Isaac Asimov" })
.Row(new { Name = "George Orwell" })
.Row(new { Name = "Thomas More" });
}
}
[Migration(2014041002)]
public class _02_CreateTasksTable : AutoReversingMigration
{
public override void Up()
{
Create.Table("StsTasks")
.WithColumn("Id").AsInt64().Identity().PrimaryKey().NotNullable()
.WithColumn("AssignedPersonId").AsInt32().ForeignKey("TsPeople", "Id").Nullable()
.WithColumn("Description").AsString(256).NotNullable()
.WithColumn("State").AsByte().NotNullable().WithDefaultValue(1) //1: TaskState.New
.WithColumn("CreationTime").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime);
}
}
在 FluentMigrator 中,迁移在继承自Migration的类中定义。如果您的迁移可以自动回滚,则AutoReversingMigration是一个快捷方式。迁移类应具有MigrationAttribute。它定义了迁移类的版本号。所有迁移都按此版本号的顺序应用。它可以是任何long数字。我使用一个数字来标识迁移类的创建日期加上同一天的增量值(例如:2014 年 4 月 24 日的第二个迁移类,版本号是‘2014042402’)。这完全取决于您。唯一重要的是它们的相对顺序。
FluentMigrator 将最新的已应用版本号存储在数据库中的一个表中。因此,它只应用大于数据库版本号的迁移。默认情况下,它使用“VersionInfo”表。如果您想更改表名,可以创建一个这样的类。
[VersionTableMetaData]
public class VersionTable : DefaultVersionTableMetaData
{
public override string TableName
{
get
{
return "StsVersionInfo";
}
}
}
正如您所见,我为所有表添加了前缀Sts(Simple Task System)。这对于模块化应用程序很重要,因此所有模块都可以拥有其特定的前缀来标识模块特定的表。
为了在数据库中创建我的表,我使用 FluentMigrator 的Migrate.exe工具,并执行以下‘命令行’命令。
Migrate.exe /connection "Server=localhost; Database=SimpleTaskSystemDb; Trusted_Connection=True;" /db sqlserver /target "SimpleTaskSystem.Infrastructure.NHibernate.dll"
为了方便起见,ABP 模板包含RunMigrations.bat文件。在Debug模式下编译项目后,我运行“RunMigrations.bat”。
正如您所见,两个迁移文件被执行,并且表被创建。
有关 FluentMigrator 的更多信息,请参阅其网站 [4]。
实体映射
为了在数据库中获取/存储实体,我们需要将实体与数据库表进行映射。NHibernate 有多种方法可以完成此任务。在此,我将使用手动Fluent Mapping(您可以使用约定式自动映射,请参阅FluentNHibernate的网站 [5])。
public class PersonMap : EntityMap<Person> { public PersonMap() : base("StsPeople") { Map(x => x.Name); } } public class TaskMap : EntityMap<Task, long> { public TaskMap() : base("StsTasks") { Map(x => x.Description); Map(x => x.CreationTime); Map(x => x.State).CustomType<TaskState>(); References(x => x.AssignedPerson).Column("AssignedPersonId").LazyLoad(); } }
EntityMap是 ABP 的一个类,它在构造函数中自动映射Id属性并获取表名。因此,我继承自它并映射其他属性。
存储库实现
我在领域层中为 Task 存储库(ITaskRepository)定义了接口。在这里,我将在 NHibernate 中实现它。
public class TaskRepository : NhRepositoryBase<Task, long>, ITaskRepository { public List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state) { //In repository methods, we do not deal with create/dispose DB connections (Session) and transactions. ABP handles it. var query = GetAll(); //GetAll() returns IQueryable<T>, so we can query over it. //var query = Session.Query<Task>(); //Alternatively, we can directly use NHibernate's Session //Add some Where conditions... if (assignedPersonId.HasValue) { query = query.Where(task => task.AssignedPerson.Id == assignedPersonId.Value); } if (state.HasValue) { query = query.Where(task => task.State == state); } return query .OrderByDescending(task => task.CreationTime) .Fetch(task => task.AssignedPerson) //Fetch assigned person in a single query .ToList(); } }
NhRepositoryBase实现了IRepository接口中定义的所有方法。因此,您只需实现自定义方法,就像我为GetAllWithPeople所做的那样。
GetAll()方法返回IQueryable<TEntity>,因此在调用ToList()之前,您可以添加其他条件。
如果一个实体的标准存储库方法已经足够,则无需为其定义或实现存储库。因此,我没有为 Person 实体实现存储库。
应用层
“定义了软件应该完成的工作,并指导表达性的领域对象来解决问题”(Eric Evans)。在理想的应用程序中,应用层不包含领域信息和业务规则(这在现实生活中可能无法实现,但我们应该尽量减少)。它在表示层和领域层之间进行中介。
应用服务和数据传输对象 (DTO)
应用服务提供应用层的功y。应用服务方法接收数据传输对象作为参数并返回数据传输对象。直接返回实体(或其他领域对象)会带来许多问题(如数据隐藏、序列化和延迟加载问题)。我强烈建议不要从应用服务获取/返回实体或任何其他领域对象。它们只应该获取/返回 DTO。这样,表示层就与领域层完全隔离了。
那么,让我们从简单的开始,Person 应用服务。
public interface IPersonAppService : IApplicationService { GetAllPeopleOutput GetAllPeople(); }
根据约定,所有应用服务都实现IApplicationService。它确保了依赖注入并提供了 ABP 的一些内置功能(如验证、审计日志和授权)。我只定义了一个名为GetAllPeople()的方法,并返回一个名为GetAllPeopleOutput的 DTO。我通常这样命名 DTO:方法名加上Input或Output后缀。请看GetAllPeopleOutput类。
public class GetAllPeopleOutput { public List<PersonDto> People { get; set; } }
PersonDto是另一个 DTO 类,用于将Person信息传递到表示层。
[AutoMapFrom(typeof(Person))] //AutoMapFrom attribute maps Person -> PersonDto public class PersonDto : EntityDto { public string Name { get; set; } }
EntityDto是 ABP 的另一个帮助类,它定义了Id属性。AutoMapFrom 属性为 AutoMapper 创建了从 Person 到 PersonDto 的自动映射配置。IPersonAppService的实现如下所示。
public class PersonAppService : IPersonAppService //Optionally, you can derive from ApplicationService as we did for TaskAppService class. { private readonly IRepository<Person> _personRepository; //ABP provides that we can directly inject IRepository<Person> (without creating any repository class) public PersonAppService(IRepository<Person> personRepository) { _personRepository = personRepository; } public GetAllPeopleOutput GetAllPeople() { var people = await _personRepository.GetAllListAsync(); return new GetAllPeopleOutput { People = people.MapTo<List<PersonDto>>() }; } }
PersonAppService在其构造函数中接收IRepository<Person>作为参数。ABP 的内置依赖注入系统使用Castle Windsor来处理它。所有存储库和应用服务都自动注册为 IOC(控制反转)容器中的瞬态对象。因此,您无需考虑 DI 的细节。此外,ABP 可以在不定义或实现存储库的情况下为实体创建标准存储库。
GetAllPeople()方法简单地从数据库中获取所有人的列表(使用 ABP 的开箱即用实现),并使用AutoMapper [6] 库将其转换为PersonDto对象列表。AutoMapper 使用约定(和必要的配置)使一个类映射到另一个类变得极其容易。ABP 的 MapTo 扩展方法在内部使用 AutoMapper 进行转换。
Mapper.CreateMap<Person, PersonDto>();
有关 AutoMapper 的更多信息,请参阅其网站 [6]。另一个应用服务是TaskAppService,实现如下。
public class TaskAppService : ApplicationService, ITaskAppService { //These members set in constructor using constructor injection. private readonly ITaskRepository _taskRepository; private readonly IRepository<Person> _personRepository; /// <summary> ///In constructor, we can get needed classes/interfaces. ///They are sent here by dependency injection system automatically. /// </summary> public TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository) { _taskRepository = taskRepository; _personRepository = personRepository; } public GetTasksOutput GetTasks(GetTasksInput input) { //Called specific GetAllWithPeople method of task repository. var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State); //Used AutoMapper to automatically convert List<Task> to List<TaskDto>. return new GetTasksOutput { Tasks = Mapper.Map<List<TaskDto>>(tasks) }; } public void UpdateTask(UpdateTaskInput input) { //We can use Logger, it's defined in ApplicationService base class. Logger.Info("Updating a task for input: " + input); //Retrieving a task entity with given id using standard Get method of repositories. var task = _taskRepository.Get(input.TaskId); //Updating changed properties of the retrieved task entity. if (input.State.HasValue) { task.State = input.State.Value; } if (input.AssignedPersonId.HasValue) { task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value); } //We even do not call Update method of the repository. //Because an application service method is a 'unit of work' scope as default. //ABP automatically saves all changes when a 'unit of work' scope ends (without any exception). } public void CreateTask(CreateTaskInput input) { //We can use Logger, it's defined in ApplicationService class. Logger.Info("Creating a task for input: " + input); //Creating a new Task entity with given input's properties var task = new Task { Description = input.Description }; if (input.AssignedPersonId.HasValue) { task.AssignedPersonId = input.AssignedPersonId.Value; } //Saving entity with standard Insert method of repositories. _taskRepository.Insert(task); } }
在UpdateTask方法中,我从 task 存储库获取Task实体并设置更改的属性。State或/和AssignedPersonId可能已更改。请注意,我没有调用 _taskRepository.Update或任何其他方法来将更改保存到数据库。因为,在 ASP.NET Boilerplate 中,应用服务方法默认是工作单元。对于一个工作单元方法,它基本上会在方法开始时打开一个数据库连接并开始事务,并在方法结束时自动将所有更改保存到数据库(提交事务)。如果在方法执行过程中引发异常,它将回滚事务。如果一个工作单元方法调用另一个工作单元方法,它们将使用相同的事务。第一个调用工作单元方法会自动处理连接和事务管理。
有关 ASP.NET Boilerplate 中工作单元系统的更多信息,请参阅文档。
DTO 验证
验证是应用程序开发中一个重要且关键但有些繁琐的概念。ABP 提供了基础y,使验证更轻松、更友好。验证用户输入是一项应用层任务。应用服务方法应该验证输入,并在输入无效时抛出异常。ASP.NET MVC 和 Web API 具有内置的验证系统,可以使用数据注解(如 Required)来实现。但是,应用服务是一个普通类,不继承自 Controller。幸运的是,ABP 为普通应用服务方法提供了类似的机制(使用 Castle Dynamic 代理和拦截)。
public class CreateTaskInput
{
public int? AssignedPersonId { get; set; }
[Required]
public string Description { get; set; }
}
在此输入 DTO 中,只有 Description 属性是必需的。ABP 在调用应用服务方法之前会自动检查它,并在其为 null 或空时抛出异常。System.ComponentModel.DataAnnotations命名空间中的所有验证属性都可以在这里使用。如果这些标准属性对您来说不够,您可以实现ICustomValidate。
public class CreateTaskInput : IInputDto, ICustomValidate
{
public int? AssignedPersonId { get; set; }
public bool SendEmailToAssignedPerson { get; set; }
[Required]
public string Description { get; set; }
public void AddValidationErrors(List<ValidationResult> results)
{
if (SendEmailToAssignedPerson && (!AssignedPersonId.HasValue || AssignedPersonId.Value <= 0))
{
results.Add(new ValidationResult("AssignedPersonId must be set if SendEmailToAssignedPerson is true!"));
}
}
}
还有一点:ABP 会检查服务方法的输入参数是否为 null。因此,您无需为此编写保护子句。
我建议为每个应用服务方法创建单独的输入和输出类,即使它只有一个输入参数。这在扩展应用程序时添加其他参数到此方法会很有用。它提供了一种在不破坏现有客户端的情况下向应用服务方法添加参数的方法。
动态 Web API 控制器
应用服务由表示层消费。在单页应用程序中,所有数据都通过 AJAX 在 JavaScript 和服务器之间发送/接收。ABP 极大地简化了从 JavaScript 调用应用服务方法。它是如何做到的?让我来解释一下……
JavaScript 不能直接调用应用服务。我们可以使用 ASP.NET Web API向客户端公开服务(还有许多其他框架可以做到这一点,如 Web Services、WCF、SignalR 等)。因此,可能存在这样的流程:
JavaScript 通过 AJAX 调用 Web API 控制器的操作,然后 Web API 控制器的操作调用相应的应用服务方法,获取结果并返回给客户端。这非常机械。ABP 会自动执行此操作,并且可以动态地为应用服务创建一个 Web API 控制器。这是为我的应用服务(task 服务和 person 服务)创建 Web API 控制器的所有代码。
DynamicApiControllerBuilder .ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem") .Build();
因此,通过 ASP.NET Web API,task 和 person 应用服务的所有方法都向客户端公开了(ABP 的流畅动态控制器创建 API 支持隐藏方法不暴露给 Web API 或选择特定的应用服务,您可以自己尝试)。在表示层部分,我们将看到如何使用 ABP 的动态 JavaScript 代理调用这些 Web API 控制器。
表示层
“负责向用户显示信息和解释用户的命令”(Eric Evans)。DDD 中最明显的层是表示层,因为我们可以看到它,可以点击它:)。
单页应用
维基百科关于 SPA 的解释:
单页应用程序(SPA),也称为单页界面(SPI),是一种 Web 应用程序或网站,它仅在一个网页上运行,旨在提供更流畅的用户体验,类似于桌面应用程序。
在 SPA 中,所有必需的代码——HTML、JavaScript 和 CSS——都通过一次页面加载检索,或者在需要时动态加载并添加到页面中,通常是对用户操作的响应。在此过程中,页面不会在任何时候重新加载,控制也不会转移到另一个页面,尽管现代 Web 技术(如 HTML5 中包含的)可以提供应用程序中不同逻辑页面的感知和导航性。与单页应用程序的交互通常涉及在后台与 Web 服务器进行动态通信。
有许多框架和库提供构建 SPA 的基础y。ASP.NET Boilerplate 可以与任何 SPA 框架配合使用,但它提供了简单的基础y,可以更轻松地与DurandalJS和AngularJS配合使用(请参阅使用 AngularJs 和 EntityFramework 开发的相同应用程序的另一篇文章)。
Durandal [7] 就是其中一个框架,我认为它是一个非常成功的开源项目。它建立在成功且使用最广泛的项目之上:jQuery(用于 DOM 操作和 AJAX)、knockout.js(用于 MVVM,将 JavaScript 模型绑定到 HTML)和require.js(用于管理 JavaScript 依赖关系并动态从服务器加载 JavaScript)。有关更多信息和丰富的文档,请访问 Durandal 的网站。
视图和视图模型
在 Durandal 中,页面的一部分由一个视图和一个视图模型组成。在 ABP 的启动模板中,有三个视图:layout、home和about。Layout 提供菜单和页面容器。home 和 about 是动态加载到页面中的视图。我更改了视图和模型,以创建我的简单任务系统应用程序。更改视图后,这里是所有视图和视图模型的文件。
我有一个布局和两个视图:任务列表和新建任务。让我们开始研究视图。
任务列表
在任务列表中,列出了所有任务,包括任务描述、分配给的人员和创建日期。有一个组合框用于过滤所有/活动/已完成的任务。已完成的任务显示为灰色(带有一个 OK 图标),活动任务显示为粗体(带有一个减号图标)。唯一已完成的任务是分配给我的“完成 Codeproject 文章!”。这样,您就可以阅读本文了:)。
让我从视图模型开始。视图模型用于与服务器通信以执行用户操作(列出任务、更改组合框、单击图标更改任务状态)并提供要在视图中显示的模型。
define(['service!tasksystem/task'],
function (taskService) {
return function () {
var that = this; //an alias of this
that.tasks = ko.mapping.fromJS([]); //list of tasks
that.localize = abp.localization.getSource('SimpleTaskSystem');
that.selectedTaskState = ko.observable(0); //'All tasks' option is selected in combobox as default
that.activate = function () {
that.refreshTasks();
};
that.refreshTasks = function () {
abp.ui.setBusy( //Set whole page busy until getTasks complete
null,
taskService.getTasks({
state: that.selectedTaskState() > 0 ? that.selectedTaskState() : null
}).done(function(data) {
ko.mapping.fromJS(data.tasks, that.tasks);
})
);
};
that.changeTaskState = function (task) {
var newState;
if (task.state() == 1) {
newState = 2;
} else {
newState = 1;
}
taskService.updateTask({
taskId: task.id(),
state: newState
}).done(function () {
task.state(newState);
abp.notify.info(that.localize('TaskUpdatedMessage'));
});
};
};
});
第一行调用 require.js 的define函数来注册模块并声明依赖项。依赖项通常是另一个模块(甚至可能是视图模型)。‘service!tasksystem/task’是 ABP 的特殊语法,它引用了task 应用服务的动态 Web API 控制器(还记得我在动态 Web API 控制器部分是如何定义 task 服务的吗)。getTasks 函数是由 ABP动态创建的。
define 函数的第二个参数是模块本身。它应该是一个定义模块的函数。它的参数由 Durandal 根据您的依赖列表自动填充。
that.tasks是一个knockout observable 数组。它使用knockout.mapping函数创建。that.localize是一个用于本地化的函数。这是 ABP 的一项功能,可以在 JavaScript 中动态本地化文本(将在本地化部分详细介绍)。that.selectedTaskState是一个observable,它绑定到组合框以显示所有/活动/已完成的任务。
activate是 Durandal 的一个特殊函数。当此视图被激活时,Durandal 会自动调用此函数。因此,我们可以在用户进入视图时编写一些要运行的代码。
在refreshTasks方法中,我调用了Task 应用服务的getTasks方法从服务器加载任务。通过 ABP 的动态 Web API 控制器和动态JavaScript 客户端代理,从 JavaScript 调用应用服务就是如此简单。getTasks 函数接收与TaskAppService.GetTasks相同的参数。该函数将返回一个 jQuery promise,因此您可以编写一个done处理程序来获取 task 应用服务 getTasks 方法的返回值。taskService.getTasks 还会处理错误,并在需要时向用户显示错误消息。如果调用了 done 处理程序,您就可以确定没有错误。在 done 处理程序中,我将检索到的任务添加到 that.task observable 数组中。
changeTaskState也非常相似。它用于将任务的状态从活动更改为已完成或反之。在 done 处理程序中,您可以看到本地化和通知 API 的用法。
就像视图模型是一个 JavaScript 文件一样,视图是一个 HTML 文件。在此 HTML 文件中,您可以将视图模型绑定到 HTML 元素。ASP.NET Boilerplate 通过允许将视图定义为Razor 视图,进一步将其向前推进:动态的cshtml文件而不是静态的html文件。因此,您可以编写 C# 代码来创建视图。请看tasklist 视图。
<div class="panel panel-default">
<div class="panel-heading" style="position: relative;">
<div class="row">
<h3 class="panel-title col-xs-6">
@L("TaskList") - <span data-bind="text: abp.utils.formatString(localize('Xtasks'), tasks().length)"></span>
</h3>
<div class="col-xs-6 text-right">
<select data-bind="value: selectedTaskState, event: { change: refreshTasks }">
<option value="0">@L("AllTasks")</option>
<option value="1">@L("ActiveTasks")</option>
<option value="2">@L("CompletedTasks")</option>
</select>
</div>
</div>
</div>
<ul class="list-group" data-bind="foreach: tasks">
<div class="list-group-item">
<span class="task-state-icon glyphicon" data-bind="click: $parent.changeTaskState, css: { 'glyphicon-minus': state() == 1, 'glyphicon-ok': state() == 2 }"></span>
<span data-bind="html: description(), css: { 'task-description-active': state() == 1, 'task-description-completed': state() == 2 }"></span>
<br />
<span data-bind="visible: assignedPersonId()">
<span class="task-assignedto" data-bind="text: assignedPersonName"></span>
</span>
<span class="task-creationtime" data-bind="text: moment(creationTime()).fromNow()"></span>
</div>
</ul>
</div>
此视图仅为与 Twitter Bootstrap 配合使用而设计。CSS 类是 bootstrap 的类。但这并不重要。这里有两个要点:
第一:我们可以使用@L("TaskList")来获取本地化字符串。L方法定义在AbpWebViewPage类中(请参阅派生自 AbpWebViewPage 的 SimpleTaskSystemWebViewPageBase 类)。您可以在视图中使用LocalizationHelper.GetString(...)的L快捷方式(有关详细信息,请参阅本地化部分)。由于这是一个 Razor 视图,我们可以在视图中直接使用 C# 代码。因此,我们可以创建服务器端动态 HTML。请记住,由于这是一个 SPA,此视图只会加载一次!
第二:我们可以使用 knockout 的data-bind属性(如 data-bind="foreach: tasks")和其他 JavaScript 方法(如abp.utils.formatString)来使用 JavaScript 视图模型的字段。因此,我们可以创建客户端动态 HTML。请参阅“click: $parent.changeTaskState”,它用于将图标的单击事件绑定到 JavaScript 视图模型代码中的changeTaskState函数。类似地,我们将组合框的change事件绑定到refreshTasks函数。有关更多信息,请参阅knockout.js网站。
新建任务
“新建任务”视图相对简单。有一个任务描述和一个可选的人员选择。
此页面的视图模型如下所示。
define(['service!tasksystem/person', 'service!tasksystem/task', 'plugins/history'],
function (personService, taskService, history) {
var localize = abp.localization.getSource('SimpleTaskSystem');
return function () {
var that = this;
var _$view = null;
var _$form = null;
that.people = ko.mapping.fromJS([]);
that.task = {
description: ko.observable(''),
assignedPersonId: ko.observable(0)
};
that.canActivate = function () {
return personService.getAllPeople().done(function (data) {
ko.mapping.fromJS(data.people, that.people);
});
};
that.attached = function (view, parent) {
_$view = $(view);
_$form = _$view.find('form');
_$form.validate();
};
that.saveTask = function () {
if (!_$form.valid()) {
return;
}
abp.ui.setBusy(_$view,
taskService.createTask(ko.mapping.toJS(that.task))
.done(function() {
abp.notify.info(abp.utils.formatString(localize("TaskCreatedMessage"), that.task.description()));
history.navigate('');
})
);
};
};
});
canActivate是 Durandal 的一个特殊函数。在此函数中,您可以返回 true/false 来允许/阻止进入页面。Durandal 还接受一个promise。在这种情况下,它会等待 promise 的结果来决定是否激活视图。您的 promise 应该返回 true/false。ABP 重写了此行为,使其可以接受除 true/false 之外的任何数据类型的 promise。因此,我们可以直接返回 ABP 的动态 JavaScript 代理返回的 promise(如上面 canActivate 方法所示)。
attached也是 Durandal 的另一个特殊函数,当您的视图附加到 DOM(文档对象模型)并且可以安全地对其进行操作时,就会调用它。
saveTask用于将给定任务保存到服务器。它首先验证表单,然后调用taskService.createTask函数(请记住,此函数由 ABP 自动动态创建,并返回一个 promise)。这里,您看到了 ABP 的两个 API 调用。abp.notify.info用于在将任务保存到服务器后显示通知。abp.ui.setBusy用于将 DOM 元素设置为忙碌状态(显示加载指示器并阻塞 UI 元素,在此情况下是整个视图)。它接受一个 promise,并在 promise 返回时(成功或失败)清除忙碌状态。ABP 使用多个 jQuery 插件来实现此功能。
本地化
ABP 提供了一个强大且灵活的本地化系统。您可以将本地化文本存储在资源文件、XML 文件甚至自定义源中。在本节中,我将展示如何使用 XML 文件。简单任务系统项目在 Localization 文件夹中包含 XML 文件。
这是 SimpleTaskSystem.xml 的内容。
<?xml version="1.0" encoding="utf-8" ?>
<localizationDictionary culture="en">
<texts>
<text name="TaskSystem" value="Task System" />
<text name="TaskList" value="Task List" />
<text name="NewTask" value="New Task" />
<text name="Xtasks" value="{0} tasks" />
<text name="AllTasks" value="All tasks" />
<text name="ActiveTasks" value="Active tasks" />
<text name="CompletedTasks" value="Completed tasks" />
<text name="TaskDescription" value="Task description" />
<text name="EnterDescriptionHere" value="Task description" />
<text name="AssignTo" value="Assign to" />
<text name="SelectPerson" value="Select person" />
<text name="CreateTheTask" value="Create the task" />
<text name="TaskUpdatedMessage" value="Task has been successfully updated." />
<text name="TaskCreatedMessage" value="Task {0} has been created successfully." />
</texts>
</localizationDictionary>
它是一个简单的 XML 文件,包含所有本地化文本的名称-值对。culture 属性定义了文件的文化。解决方案中还有一个土耳其语(tr)本地化 XML 文件。必须向 ABP 注册本地化文件才能在 C# 和 JavaScript 中使用。
Configuration.Localization.Sources.Add(
new XmlLocalizationSource(
"SimpleTaskSystem",
HttpContext.Current.Server.MapPath("~/Localization/SimpleTaskSystem")
)
);
本地化源必须具有唯一名称(此处为 SimpleTaskSystem)。因此,应用程序可以使用不同的源(以不同格式和数据源存储)。XmlLocalizationSource 还需要一个文件夹(此处为/Localization/SimpleTaskSystem)来读取本地化文件。
然后我们就可以在需要时获取本地化文本了。在 C# 中,我们有两种方式可以获取本地化文本:
//Use directly
var s1 = LocalizationHelper.GetString("SimpleTaskSystem", "NewTask");
//Use after get source
var source = LocalizationHelper.GetSource("SimpleTaskSystem");
var s2 = source.GetString("NewTask");
它返回当前语言的本地化文本(通过使用当前线程的 CurrentUICulture)。还有一些重载方法可以获取特定语言环境的文本。JavaScript 中也有类似的 API 来获取本地化文本。
//Use directly
var s1 = abp.localization.localize('NewTask', 'SimpleTaskSystem');
//Use after get source
var source = abp.localization.getSource('SimpleTaskSystem');
var s2 = source('NewTask');
这些方法也获取当前语言的本地化文本。
JavaScript API
每个应用程序在客户端都需要一些通用功能,以 JavaScript 的形式。例如:显示成功通知、阻塞 UI 元素、显示消息框等。有许多库(jQuery 插件)可以实现这些功能。但它们都有不同的 API。ASP.NET Boilerplate 为此类任务定义了一些通用 API。因此,如果您想以后更改通知插件,只需要实现一个简单的 API。此外,jQuery 插件可以直接实现 ABP API。与其直接调用插件的通知 API,不如调用 ABP 的通知 API。在此,我将解释其中一些 API。
日志 API
当您想在客户端编写一些简单的日志时,您可以像您知道的那样使用 console.log('...') API。但并非所有浏览器都支持它,并且您的脚本可能会中断。因此,您应该先检查它。此外,您可能想将日志写入其他地方。ABP 定义了安全的日志记录函数。
abp.log.debug('...');
abp.log.info('...');
abp.log.warn('...');
abp.log.error('...');
abp.log.fatal('...');
此外,您可以通过将 abp.log.level 设置为 abp.log.levels 中的一个来更改日志级别(例如:abp.log.levels.INFO 以不写入调试日志)。默认情况下,这些函数将日志写入控制台。但您可以轻松地覆盖此行为。
通知 API
当我们喜欢在某些事情发生时显示一些漂亮的自动消失通知时,例如项目已保存或出现了问题。ABP 为此定义了 API。
abp.notify.success('a message text', 'optional title');
abp.notify.info('a message text', 'optional title');
abp.notify.warn('a message text', 'optional title');
abp.notify.error('a message text', 'optional title');
通知 API 默认由toastr库实现。您可以在您喜欢的通知库中实现它。
消息框 API
Message API 用于向用户显示消息。用户单击 OK 并关闭消息窗口/对话框。示例:
abp.message.info('some info message', 'some optional title'); abp.message.warn('some warning message', 'some optional title'); abp.message.error('some error message', 'some optional title');
目前尚未实现。您可以实现它来显示对话框或消息框。
UI 块 API
此 API 用于阻塞整个页面或页面上的一个元素。因此,用户无法单击它。ABP API 是:
abp.ui.block(); //Block all page
abp.ui.block($('#MyDivElement')); //You can use any jQuery selection..
abp.ui.block('#MyDivElement'); //..or directly selector
abp.ui.unblock(); //Unblock all page
abp.ui.unblock('#MyDivElement'); //Unblock specific element
UI 忙碌 API
有时您可能希望使某些页面/元素忙碌。例如,当表单提交到服务器时,您可能希望阻塞表单并显示忙碌指示器。ABP 提供了用于此目的的 API。
abp.ui.setBusy('#MyRegisterForm');
abp.ui.clearBusy('#MyRegisterForm');
setBusy的第二个参数可以是一个promise,以便在 promise 完成时自动调用 clearBusy。有关用法,请参阅示例项目(以及本文档)中的newtask视图模型。
其他
将来,ABP 将添加其他有用的 API 来标准化常见任务。还有一些用于常见用法的实用函数(如 abp.utils.formatString,其功能类似于 C# 中的 string.Format)。
注意:如果您想实现这些 API,请将它们实现到单独的文件中,并在 abp.js 之后将此单独的 js 文件包含到您的页面中。
更多
模块系统
ASP.NET Boilerplate 被设计成模块化的。它提供了开发可用于不同应用程序的通用模块的基础y。一个模块可以依赖于其他模块。一个应用程序由模块组成。模块是一个包含派生自AbpModule的模块类的程序集。在本文档介绍的示例应用程序中,所有层都被定义为独立的模块。例如,应用层定义了一个模块如下。
/// <summary>
/// 'Application layer module' for this project.
/// </summary>
[DependsOn(typeof(SimpleTaskSystemCoreModule))]
public class SimpleTaskSystemApplicationModule : AbpModule
{
public override void Initialize()
{
//This code is used to register classes to dependency injection system for this assembly using conventions.
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
//We must declare mappings to be able to use AutoMapper
DtoMappings.Map();
}
}
ASP.NET Boilerplate 在应用程序启动时分别调用模块的 PreInitialize、Initialize 和 PostInitialize 方法。如果模块 A 依赖于模块 B,则模块 B 在模块 A 之前初始化。所有方法的确切顺序是:PreInitialize-B、PreInitialize-A、Initialize-B、Initialize-A、PostInitialize-B 和 PostInitialize-A。这对所有依赖图都是如此。
Initialize 是应放置依赖注入配置的方法。在此,您可以看到此模块根据约定注册了其程序集中的所有类(请参阅下一节)。然后它使用 AutoMapper 库映射类(这特定于此应用程序)。此模块还定义了依赖关系(应用层仅依赖于应用程序的领域(核心)层)。
依赖注入和约定
通过遵循最佳实践和一些约定,ASP.NET Boilerplate 几乎使您在编写应用程序时无需关心依赖注入系统的使用。它会自动注册所有存储库、领域服务、应用服务、MVC 控制器和Web API 控制器。例如,您可能有一个 IPersonAppService 接口和一个实现它的 PersonAppService 类。
public interface IPersonAppService : IApplicationService
{
//...
}
public class PersonAppService : IPersonAppService
{
//...
}
ASP.NET Boilerplate 会自动注册它,因为它实现了 IApplicationService 接口(它只是一个空接口)。它被注册为瞬态(每次使用时创建实例)。当您将 IPersonAppService 接口注入(使用构造函数注入)到类中时,会自动创建一个 PersonAppService 对象并将其传递到构造函数中。有关依赖注入及其在 ASP.NET Boilerplate 中的实现的详细文档。
关于 AngularJS & EntityFramework 示例
我写了一篇文章“使用 AngularJS、ASP.NET MVC、Web API 和 EntityFramework 构建 N 层单页 Web 应用程序”,该文章解释了使用 Angular 和 EntityFramework 的相同示例应用程序。
摘要
在本文中,我介绍了一个新的应用程序框架:ASP.NET Boilerplate。它是使用最佳实践和最流行工具开发现代 Web 应用程序的起点。由于它相对较新,可能还有一些概念缺失。但这并不限制您。您可以将其作为起点来构建您的应用程序。它是一个正在开发和扩展的新框架。我已在我的公司生产环境中使用它。
您可以将 bug 报告、问题和功能请求提交到 github 页面(https://github.com/aspnetboilerplate/aspnetboilerplate/issues)。由于这是一个开源项目,您可以 fork 它并发送 pull requests 来贡献 ABP 的源代码。我希望它成为我们所有 .NET 开发人员的一个起点,所以一起开发它将是一件好事。
文章历史
- 2016-07-19:更新了文章和源代码以适配 ABP v0.10。
- 2014-11-02:更新了文章和源代码以适配 ABP v0.4.1。
- 2014-09-06:文章已完全修订并更新了 ABP v0.3.2 的示例应用程序。
- 2014-07-26:更新了文章和源代码以适配 ABP v0.3.0.1。
- 2014-07-01:添加了指向解释 AngularJS & EntityFramework 集成的其他文章的链接。
- 2014-06-16:添加了 AngularJS&EntityFramework 示例。更新到 ABP v0.2.3。
- 2014-06-09:文章和示例项目已更新到 ABP v0.2.2.1。
- 2014-05-22:已将示例项目更新到最新的 ABP 版本。
- 2014-05-20:向文章添加了部分内容。
- 2014-05-13:向文章添加了详细信息和部分内容。
- 2014-05-08:向文章添加了详细信息和部分内容。
- 2014-05-05:文章首次发布。
参考文献
[1] ASP.NET Boilerplate 官方网站:http://www.aspnetboilerplate.com
[2] 书籍:“Domain Driven Design: Tackling Complexity in the Heart of Software” by Eric Evans。
[3] Martin Fowler 的网站:https://martinfowler.com.cn
[4] Fleunt Migrator:https://github.com/schambers/fluentmigrator
[5] FluentNHibernate:http://www.fluentnhibernate.org
[6] AutoMapper:http://automapper.org/r.org/
[7] Durandaljs:http://durandaljs.com/
[8] Knockout.js:http://knockoutjs.com/