背景

我们正在根据不同需求创建不同的应用程序。但是,至少在某种程度上一遍又一遍地实施共同和类似的结构。授权,验证,异常处理,日志记录,本地化,数据库连接管理,设置管理,审核日志记录是其中一些常见的结构。此外,我们正在建立架构结构和最佳实践,如分层和模块化架构,域驱动设计,依赖注入等。并试图根据一些惯例开发应用程序。

由于所有这些都是非常耗时且难以为每个项目建立单独的,许多公司创建私有框架。他们正在使用这些框架更少的错误来更快地开发新的应用程序。当然,并不是所有的公司都很幸运。他们大多数没有时间,预算和团队来发展这样的框架。即使他们有可能创建一个框架,很难记录,培训开发人员并维护它。

ASP.NET Boilerplate(ABP)是一个开放源码和文档良好的应用程序框架,开始了“为所有公司和所有开发人员制定通用框架”的想法。这不仅仅是一个框架,而且还提供了一个基于领驱动设计(DDD)和最佳实践的强大的架构模型。

“DRY——避免重复造轮子”,这也是ASP.NET Boilerplate(ABP)的重要思想,作者之所以把项目命名为“ASP.NET Boilerplate”,就是希望它能成为开发一般企业WEB应用的新起点,直接把ABP作为项目模板,避免做重复的工作,让开发人员只关注具体业务逻辑的实现,这样也大大提高了开发效率。

使用框架当然有代价,你必须受到框架强API的侵入,抑或要使用他的方言。而且这个框架想要吃透,也要付出很大的学习成本。但是好处也是显而易见的。业界顶尖的架构师已经为你搭建好了一套基础架构,很好的回应了关于一个软件系统应该如何设计,如何规划的问题,并且提供了一套最佳实践和范例。

学习虽然要付出成本,但是经过漫长的跋涉,我们从一无所知已经站到了工业级开发的门槛上。基于这个框架,我们可以很好的来划分任务,进行单元测试等。大大降低了软件出现BUG的几率。

什么是ASP.NET Boilerplate(ABP)?

ASP.NET Boilerplate是使用最佳实践和最受欢迎工具的新型现代Web应用程序的起点。 它的目标是成为一个坚实的模型,通用应用程序框架和项目模板。 它能做什么?

服务端(Server side)

  • 基于最新的.NET技术 (目前是ASP.NET MVC 5、Web API 2、C# 5.0,在ASP.NET 5正式发布后会升级)
  • 实现领域驱动设计(实体、仓储、领域服务、领域事件、应用服务、数据传输对象,工作单元等等)
  • 实现分层体系结构(领域层,应用层,展现层和基础设施层)
  • 提供了一个基础架构来开发可重用可配置的模块
  • 集成一些最流行的开源框架/库,也许有些是你正在使用的。
  • 提供了一个基础架构让我们很方便地使用依赖注入(使用Castle Windsor作为依赖注入的容器)
  • 提供Repository仓储模式支持不同的ORM(已实现Entity Framework 、NHibernate、MangoDb和内存数据库)
  • 支持并实现数据库迁移(EF 的 Code first)
  • 模块化开发(每个模块有独立的EF DbContext,可单独指定数据库)
  • 包括一个简单的和灵活的多语言/本地化系统
  • 包括一个 EventBus来实现服务器端全局的领域事件
  • 统一的异常处理(应用层几乎不需要处理自己写异常处理代码)
  • 数据有效性验证(Asp.NET MVC只能做到Action方法的参数验证,ABP实现了Application层方法的参数有效性验证)
  • 通过Application Services自动创建Web Api层(不需要写ApiController层了)
  • 提供基类和帮助类让我们方便地实现一些常见的任务
  • 使用“约定优于配置原则”

客户端(Client side)

  • 提供单页面应用程序(使用AngularJs和Durandaljs)和多页面应用程序的项目模板。 模板基于Twitter Bootstrap。
  • 大多数使用的JavaScript库都包含在默认情况下配置的。
  • 创建动态JavaScript代理来轻松调用应用程序服务(使用动态Web API层)。
  • 封装一些Javascript 函数:显示警报和通知,阻止UI,制作AJAX请求…

除了这些共同的基础设施,正在开发一个名为“Zero”的模块。 它将提供基于角色和权限的授权系统(使用最新的ASP.NET身份框架),设置系统,多租户,审计日志等。

ABP不是什么?

ASP.NET Boilerplate提供了具有最佳实践的应用程序开发模型。 它具有基础类,接口和工具,使得易于构建可维护的大规模应用程序。

  • 但它不是RAD(快速应用程序开发)工具之一,它们尝试为无需编码的应用程序提供基础架构。 相反,它提供了一个基础设施来编写最佳实践。
  • 它不是代码生成工具。 虽然它具有在运行时构建动态代码的几个功能,但它不生成代码。
  • 这不是一个一体化的框架。 相反,它为特定任务使用了众所周知的工具/库(如用于O / RM的NHibernate和EntityFramework,用于日志记录的Log4Net,作为DI容器的Castle Windsor,SPA框架的AngularJS)。

入门(Getting started)

在本文中,我将展示如何使用ASP.NET Boilerplate去除单页面和响应性Web应用程序(我现在称之为ABP)。 我将在这里使用DurandalJs作为SPA框架和NHibernate作为ORM框架。 我准备了另一篇用AngularJs和EntityFramework实现相同应用的文章。

此示例应用程序被命名为“简单任务系统”,它由两个页面组成:一个用于列出任务,另一个是添加新任务。 一个任务可以与一个人相关,可以是活动的或完成的。 应用程序以两种语言进行本地化。 应用程序中任务列表的屏幕截图如下所示:
task

从模板创建空的web应用程序

ABP为新项目提供启动模板(即使您可以手动创建项目并从nuget获取ABP包,模板方式更容易)。 请访问www.aspnetboilerplate.com/Templates以从模板创建应用程序。 您可以选择具有可选AngularJs或DurandalJs的SPA(单页面应用)项目。 或者您可以选择MPA(经典的,多页面应用程序)项目。 那么你可以选择EntityFramework或NHibernate作为ORM框架。

create_template_v2

我将我的项目命名为SimpleTaskSystem,并与Durandal和NHibernate建立了一个SPA项目。 下载项目为zip文件。 当我打开zip文件时,我看到一个解决方案已经准备好,包含每个域驱动设计层的程序集(项目):
project_files2

就这样,你的项目已经准备好运行了! 在VS2013中打开它,然后按F5:
first_run

领域层(Domain layer)

负责代表业务概念,有关业务情况的信息和业务规则”(Eric Evans)。 在领域驱动设计(DDD)中,核心层是领域层。 领域层定义您的实体,实现您的业务规则等。

实体(Entities)

实体是DDD的核心概念之一。 埃里克·埃文斯(Eric Evans)将其描述为“一个不是由其属性从根本上定义的对象,而是一个连贯性和身份的线索”。 因此,实体具有Id并存储在数据库中。

任务(Task)实体类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}
}

它是从具有长主键类型的实体基类派生的简单类。 TaskState是一个具有“Active”和“Completed”的枚举。

人(Person)实体类定义如下:

1
2
3
4
public class Person : Entity
{
public virtual string Name { get; set; }
}

任务与一个人有关系,这就是这个简单的应用。

实体在ABP中必须实现IEntity 接口。 因此,如果主键的类型是Long,则必须实现IEntity 。 如果您的Entity的主键是int,您无需定义主键类型并直接实现IEntity接口。 实际上,您可以轻松地从Entity或Entity 派生(如上图所示)(Task和Person)。 IEntity定义了Entity的Id属性。

仓储(Repositories)

“使用类收集接口访问领域对象,在领域和数据映射层之间进行中介”(Martin Fowler)。 实际上,仓储用于对域对象(实体或值类型)执行数据库操作。

通常,每个实体(或聚合根)使用分隔的仓储。 ASP.NET Boilerplate为每个实体提供默认仓储(我们将看到如何使用默认仓储)。 如果我们需要定义其他方法,我们可以扩展IRepository接口。 我将其扩展到了Task库:

1
2
3
4
public interface ITaskRepository : IRepository<Task, long>
{
List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state);
}

为每个Repository定义一个接口是很好的。 因此,我们可以将接口与实现分开。 仓储接口定义了仓储的常见方法:

first_run

它定义了基本的CRUD方法。 所以,所有仓储都会自动实现所有这些方法。 除了标准的基础方法之外,还可以添加特定于该仓储的方法,就像我定义了GetAllWithPeople方法一样。

基础设施层(Infrastructure layer)

“提供支持更高层次的通用技术功能”(Eric Evans)。 它用于使用第三方库和框架(如对象关系映射)实现应用程序的抽象。 在这个应用程序中,我将使用基础设施层:

  • 使用FluentMigrator创建数据库迁移系统。
  • 实现存储库并使用NHibernate和FluentNHibernate映射实体。

数据库迁移(Database Migrations)

“进化数据库设计:在过去几年中,我们开发了许多技术,允许数据库设计随着应用程序的发展而发展,这对于敏捷方法来说是非常重要的。” Martin Fowler在他的网站中说。 数据库迁移是支持这一想法的重要技术。 在没有这种技术的情况下,很难在多个生产环境中维护应用程序的数据库。 即使你只有一个在线系统,这是至关重要的。

FluentMigrator 是数据库迁移的好工具。 它支持大多数常见的数据库系统。 在这里,我的迁移代码为Person和Task表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
[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。 它定义了迁移类的版本号。 所有迁移都按此版本号排序。 它可以是任何长的数字。 我使用一个标识迁移类创建日期的数字加上同一天的增量值(例如:2014年4月24日第二个迁移类,版本为“2014042402”)。 这完全取决于你 只有重要的是他们的相对秩序。

FluentMigrator将最新的应用版本号存储在数据库的表中。 因此,它仅适用于那些大于数据库版本的迁移。 默认情况下,它使用’VersionInfo’表。 如果要更改表名,可以创建一个类:

1
2
3
4
5
6
7
8
9
10
11
[VersionTableMetaData]
public class VersionTable : DefaultVersionTableMetaData
{
public override string TableName
{
get
{
return "StsVersionInfo";
}
}
}

如你所见,我为所有表写了一个前缀Sts(简单任务系统)。 这对于模块化应用程序很重要,所以所有模块都可以使用其特定的前缀来标识模块特定的表。

要在数据库中创建表,我使用这种“命令行”命令使用FluentMigrator的Migrate.exe工具:

1
Migrate.exe /connection "Server=localhost; Database=SimpleTaskSystemDb; Trusted_Connection=True;" /db sqlserver /target "SimpleTaskSystem.Infrastructure.NHibernate.dll"

对于快捷方式,ABP模板包含RunMigrations.bat文件。 在Debug模式下编译项目后,我运行“RunMigrations.bat”:

migration

正如你看到的,两种迁移文件执行,表创建:

migration

有关FluentMigrator的更多信息,请参阅它的网站。

实体映射(Entity mappings)

为了获取/存储实体到数据库中,我们应该使用数据库表映射实体。 NHibernate有几个选择可以实现。 在这里,我将使用Fluent Mapping手册(您可以使用传统的自动映射,请参阅FluentNHibernate的网站):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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属性,并在构造函数中获取表名。所以,我从它派生和映射等性能。

仓储实现

我定义的接口为域中层任务储存库(ITaskRepository)。在这里,我将在这里NHibernate的实现它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 ,所以你可以写额外的条件,直到调用ToList()。

如果标准仓储方法足够用于该实体,则无需为实体定义或实现仓储。 所以,我没有为Person实体实现仓储。

应用层(Application layer)

“定义软件应该做的工作,并指导表达领域的对象解决问题”(Eric Evans)。 应用层在理想应用中不包括域信息和业务规则(这在现实生活中可能是不可能的,但我们应该将其最小化)。 它介于表示层和域层之间。

应用服务和数据传输对象(DTO)

应用服务提供应用层的功能。 应用程序服务方法将数据传输对象作为参数,并返回数据传输对象。 直接返回的实体(或其他领域对象)有许多问题(如数据隐藏,序列化和延迟加载问题)。 我强烈建议不要从应用程序服务获取/返回实体或任何其他域对象。 他们应该只是返回DTO。 因此,显示(Presentation)层与领域(Domain)层完全隔离。

所以,让我们从简单的一,个人应用服务入手:

1
2
3
4
public interface IPersonAppService : IApplicationService
{
GetAllPeopleOutput GetAllPeople();
}

所有应用程序服务按惯例执行IApplicationService。 它确保依赖注入,并提供一些ABP的内置功能(如验证,审核日志和授权)。 我只定义了一个名为GetAllPeople()的方法,并返回一个名为GetAllPeopleOutput的DTO。 我将DTO命名为:方法名称加输入或输出后缀。 参见GetAllPeopleOutput类:

1
2
3
4
public class GetAllPeopleOutput
{
public List<PersonDto> People { get; set; }
}

PersonDto是将Person信息传递给表示层的另一个DTO类:

1
2
3
4
5
[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的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 作为参数。 ABP的内置依赖注入系统使用Castle Windsor处理它。 所有存储库和应用程序服务都会自动注册到IOC(控制反转)容器作为临时对象。 所以,你没有想到DI细节。 此外,ABP可以在不定义或实现存储库的情况下为实体创建标准存储库。

GetAllPeople()方法只是从数据库中获取所有人(使用ABP的开箱即用)列表,并使用AutoMapper [6]库将其转换为PersonDto对象列表。 AutoMapper使得使用约定(如果需要的话)将一个类映射到另一个类是非常容易的。 ABP的MapTo扩展方法在内部使用AutoMapper进行转换。

1
Mapper.CreateMap<Person, PersonDto>();

要获取有关AutoMapper的更多信息,请参阅它的网站。 其他应用程序服务是TaskAppService实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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方法中,我从任务存储库获取任务实体,并设置更改的属性。 状态或/和AssignedPersonId可能会更改。 请注意,我没有调用_taskRepository.Update或任何其他方法来保存对数据库的更改。 因为在ASP.NET Boilerplate中,应用程序服务方法是默认的工作单元。 对于单位工作方法,它基本上打开数据库连接,并在方法开始时开始事务,并在方法结束时将所有更改(提交事务)保存到数据库。 如果在方法的执行中抛出异常,它将回滚事务。 如果一个工作单元的方法调用另一个工作单元方法,则它们使用相同的事务。 第一个称为工作单位的方法自动处理连接和事务管理。

要了解有关ASP.NET Boilerplate中工作单元的更多信息,请参阅文档。

数据传输对象验证(DTO Validation)

验证在应用程序开发中是一个重要且关键但有点乏味的概念。 ABP提供基础设施,使验证更容易和更好。 验证用户输入是一个应用层任务。 如果给定输入无效,则应用程序服务方法应验证输入并抛出异常。 ASP.NET MVC和Web API具有内置的验证系统,可以使用数据注释(如Required)来实现。 但是应用程序服务是一个简单的类,不是从Controller派生的。 幸运的是,ABP为普通应用服务方法提供了类似的机制(使用Castle Dynamic代理和拦截):

1
2
3
4
5
6
7
public class CreateTaskInput
{
public int? AssignedPersonId { get; set; }
[Required]
public string Description { get; set; }
}

在此输入DTO中,只需要“描述”属性。 在调用应用程序服务方法之前,ABP会自动检查它,如果它为空或为空则抛出异常。 System.ComponentModel.DataAnnotations命名空间中的所有验证属性都可以在这里使用。 如果这些标准属性对你来说还不够,可以实现ICustomValidate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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检查服务方法的输入参数是否为空。 所以,你不需要写保护条款。

我建议为每个应用程序服务方法创建分离的输入和输出类,即使它只有一个输入参数。 当通过向该方法添加其他参数来扩展应用程序时,这是很好的。 它提供了一种在不破坏现有客户端的情况下向应用程序服务方法添加参数的方法。

动态Web API控制器(Dynamic Web API Controllers)

应用程序服务由表示层消耗。 在单页面应用程序中,所有的数据都是使用AJAX在javascript和服务器之间发送/接收的。 ABP极大地简化了从javascript调用应用程序服务方法。 这是怎么做到的 让我来解释一下

一个应用程序服务不能直接通过javascript调用。 我们可以使用ASP.NET Web API来向客户端公开服务(还有许多其他框架,如Web服务,WCF,SignalR等)。 所以,可能会有这样的一个流程:

calling_webapi_ajax

Javascript通过AJAX调用Web API控件的操作,Web API控制器的操作然后调用相应的应用程序服务的方法,获取结果并返回给客户端。 这很漂亮的机器人。 ABP自动执行此操作,并可为应用程序服务动态创建Web API控制器。 这里是为我的应用服务创建Web API控制器的所有代码:任务服务和个人服务:

1
2
3
DynamicApiControllerBuilder
.ForAll<IApplicationService>(Assembly.GetAssembly(typeof (SimpleTaskSystemApplicationModule)), "tasksystem")
.Build();

因此,使用ASP.NET Web API(ABP流畅的动态控制器创建API支持从Web API隐藏方法或选择特定应用程序服务,自己尝试),任务和个人应用程序服务的所有方法都暴露给客户端。 在演示层部分,我们将看到如何使用ABP的动态JavaScript代理调用这些Web API控制器。

显示层(Presentation layer)

“负责向用户显示信息并解释用户的命令”(Eric Evans)。 DDD中最明显的一层是Presentation Layer,因为我们可以看到它,我们可以点击它:)。

单页应用(Single page applications)

维基百科说SPA:

  1. 单页面应用程序(SPA)也称为单页面接口(SPI),是一个适用于单个网页的Web应用程序或网站,目标是提供类似桌面应用程序的更流畅的用户体验。

  2. 在SPA中,通过单个页面加载检索所有必需的代码(HTML,JavaScript和CSS),或者根据需要动态加载适当的资源并将其添加到页面,通常是响应用户操作。 尽管现代网络技术(如HTML5中包含的)技术可以在应用程序中提供单独逻辑页面的感知和导航性,但该页面在此过程中的任何时间点都不会重新加载,也不会将其转移到其他页面。 与单页应用程序的交互通常涉及与Web服务器的幕后动态通信。

有许多框架和库提供了构建SPA的基础设施。 ASP.NET Boilerplate可以与任何SPA框架一起使用,但可以提供简单的基础设施,以便与DurandalJs和AngularJs配合使用(请参阅AngularJs开发的相同应用程序)。

Durandal [7]是这些框架之一,我认为这是一个非常成功的开源项目。 它基于成功和大多数使用的项目:jQuery(用于DOM操作和AJAX),knockout.js(用于MVVM,使用HTML绑定JavaScript模型)和require.js(用于管理javascript依赖关系并从服务器动态加载javascript)。 有关更多信息和丰富的文档,请访问Durandal的网站。

本地化

ABP提供了一个强大而灵活的本地化系统。你可以存储在资源文件,XML文件,甚至在自定义源您的本地化的文本。在本节中,我将展示使用XML文件。简单的任务系统项目包括本地化文件夹中的XML文件:

localization_files2

在这里,SimpleTaskSystem.xml的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?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文件,包括所有可本地化文本的名称 - 值对。 文化属性定义文件的文化。 在解决方案中还有一个土耳其语(tr)本地化的XML文件。 本地化文件应该注册到ABP,以便可以在C#和javascript中使用:

1
2
3
4
5
6
Configuration.Localization.Sources.Add(
new XmlLocalizationSource(
"SimpleTaskSystem",
HttpContext.Current.Server.MapPath("~/Localization/SimpleTaskSystem")
)
);

本地化源必须是唯一的名称(SimpleTaskSystem这里)。 因此,可以在应用程序中使用不同的源(以不同的格式和数据源存储)。 XmlLocalizationSource还需要一个文件夹(/ Localization / SimpleTaskSystem here)来读取本地化文件。

然后我们可以在需要时获得本地化的文本。 在C#中,我们有两个选项来获取本地化文本:

1
2
3
4
5
6
//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,以获得本地化的文本:

1
2
3
4
5
6
//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。 您可以调用ABP的通知API而不是直接调用插件的通知API。 在这里,我将解释一些API。

日志记录API

当你想要写一些简单的日志中的客户端,你可以如你所知使用的console.log(“…”)API。但它不是所有的浏览器都支持,你的脚本可能被打破。所以,你应该先检查一下。此外,您可能想要写日志别处。 ABP定义安全日志记录功能:

1
2
3
4
5
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:

1
2
3
4
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库实现。您可以在自己喜欢的通知库实现它。

MessageBox API

MessageBox API用于显示一个消息给用户。用户点击OK,关闭消息窗口/对话框。例子:

1
2
3
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 Block API

此API用于阻止整个页面或页面上的元素。因此,用户不能点击它。 ABP API的是:

1
2
3
4
5
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 Busy API

有时你可能需要做一些页面/元素忙。例如,你可能想阻止一个表格,然后让一个忙碌的指标,同时提交表单到服务器。 ABP提供API为:

1
2
abp.ui.setBusy('#MyRegisterForm');
abp.ui.clearBusy('#MyRegisterForm');

setBusy可以采取承诺的第二个参数来自动调用clearBusy时承诺完成。请参见newtask视图模型示例项目(和文章)的使用。

模块系统

Abp的设计是模块化的。它提供了基础设施来创建通用模块那些可以在不同的应用中使用。一个模块可以依赖于其他模块。一个应用程序是由模块组成。模块是包括从AbpModule衍生的模块类的组件。在示例应用程序在这篇文章中所解释的,所有层被定义为分隔模块。例如,应用层定义那样的模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <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();
}
}

Abp分别在应用程序启动时调用模块PreInitialize,Initialize和PostInitialize方法。 如果模块A依赖于模块B,模块B在模块A之前初始化。所有方法的准确顺序:PreInitialize-B,PreInitialize-A,Initialize-B,Initialize-A,PostInitialize-B和PostInitialize-A。 所有依赖图都是这样的。

Initialize是应该放置依赖注入配置的方法。 在这里,您将看到该模块以常规方式注册其组装中的所有类(请参阅下一节)。 然后它使用AutoMapper库映射类(它是此应用程序的特定的)。 该模块还定义依赖关系(应用程序层仅依赖于应用程序的域(核心)层)。

依赖注入和惯例

当你写遵循最佳实践和一些约定您的应用程序ASP.NET样板几乎使无形的使用依赖注入系统。它会自动注册所有存储库,域名服务,应用服务,自动MVC控制器和Web API控制器。例如,你可能有一个IPersonAppService接口和实现它PersonAppService类:

1
2
3
4
5
6
7
8
9
public interface IPersonAppService : IApplicationService
{
//...
}
public class PersonAppService : IPersonAppService
{
//...
}

ASP.NET Boilerplate自动注册它,因为它实现了IApplicationService接口(它只是一个空的接口)。 它被注册为transient(每个用法创建的实例)。 当您注册(使用构造函数注入)IPersonAppService接口到一个类时,将创建一个PersonAppService对象并自动传递到构造函数中。 请参阅有关依赖注入的详细文档,并在ASP.NET Boilerplate中实现。

总结

Abp提供了一个基于DDD的一款优秀的asp.net框架,提供了基本基本常用的api,可以让开发者只关注具体的业务逻辑,省去了很多重复造轮子的工作,虽然abp为开发者提供了创建项目的一个好的模块、入口的基本框架。但abp也对开发者本身所掌握的知识和经验有一定的要求,如果你使用abp作为你项目的框架,同时意味着你必须熟悉DDD知识并且拥有一定的经验。