為什么使用ABP 我們近幾年陸續開發了一些Web應用和桌面應用,需求或簡單或復雜,實現或優雅或丑陋。一個基本的事實是:我們只是積累了一些經驗或提高了對,NET的熟悉程度。 隨着軟件開發經驗的不斷增加,我們發現其實很多工作都是重復機械的,而且隨着軟件復雜度的不斷提升,以往依靠經驗來完成一些簡單的增刪改查的做法已經行不通了。特別是用戶的要求越來越高,希望添加的功能越來多,目前這種開發模式,已經捉襟見肘。我很難想象如何在現有的模式下進行多系統的持續集成並添加一些新的特性。 開發一個系統時,我們不可避免的會使用各種框架。數據持久層實現、日志、ASP.NET MVC、IOC以及自動映射等。一個高質量的軟件系統往往還有全局容錯,消息隊列等組件。 把上述這些組件組合到一起的時候,其復雜度會急劇上升。一般個人和小團隊的技術水平,很難設計出一個均衡協調的框架。對於傳統的所謂三層架構,我也是很持懷疑態度的。(月薪15k的程序員搞的三層架構,我也仔細讀過,也是問題多多,並不能解釋為什么要使用三層)。 其實,我們無非是希望在編程的時候,把大部分的注意力全部集中到業務實現上。不要過多的考慮基礎的軟件結構上的種種問題。應該有一個框框或者一種范式來提供基本的服務,如日志、容錯和AOP,DI等。 稍微正規一點的公司經過多年沉淀都形成了自己的內部軟件框架,他們在開發軟件的時候並不是從一片空白開始的。而是從一個非常牢固的基礎平台上開始構建的。這樣大大提高了開發速度,而且一種架構往往也決定了分工協作的模式。我們目前之所以無法分工協作,根本原因也是缺少一套成熟穩定的基礎開發架構和工作流程。 目前.NET上有不少開源框架。比如Apworks和ABP。其中Apworks是中國人寫的一套開源框架。它是一個全功能的,不僅可以寫分布式應用,也可以寫桌面應用。ABP的全稱是Asp.net boilerplate project(asp.net樣板工程)。是github上非常活躍的一個開源項目。它並沒有使用任何新的技術,只是由兩名架構師將asp.net開發中常用的一些工具整合到了一起,並且部分實現了DDD的概念。是一個開箱即用的框架,可以作為asp.net分布式應用的一個良好起點。 使用框架當然有代價,你必須受到框架強API的侵入,抑或要使用他的方言。而且這個框架想要吃透,也要付出很大的學習成本。但是好處也是顯而易見的。業界頂尖的架構師已經為你搭建好了一套基礎架構,很好的回應了關於一個軟件系統應該如何設計,如何規划的問題,並且提供了一套最佳實踐和范例。 學習雖然要付出成本,但是經過漫長的跋涉,我們從一無所知已經站到了工業級開發的門檻上。基於這個框架,我們可以很好的來划分任務,進行單元測試等。大大降低了軟件出現BUG的幾率。
從模板創建空的web應用程序
ABP的官方網站:http://www.aspnetboilerplate.com ABP在Github上的開源項目:https://github.com/aspnetboilerplate ABP提供了一個啟動模板用於新建的項目(盡管你能手動地創建項目並且從nuget獲得ABP包,模板的方式更容易)。 轉到www.aspnetboilerplate.com/Templates從模板創建你的應用程序。 你可以選擇SPA(AngularJs或DurandalJs)或者選擇MPA(經典的多頁面應用程序)項目。可以選擇Entity Framework或NHibernate作為ORM框架。 這里我們選擇AngularJs和Entity Framework,填入項目名稱“SimpleTaskSystem”,點擊“CREATE MY PROJECT”按鈕可以下載一個zip壓縮包,解壓后得到VS2013的解決方案,使用的.NET版本是 4.5.1。
每個項目里引用了Abp組件和其他第三方組件,需要從Nuget下載。
黃色感嘆號圖標,表示這個組件在本地文件夾中不存在,需要從Nuget上還原。操作如下:
要讓項目運行起來,還得創建一個數據庫。這個模板假設你正在使用SQL2008或者更新的版本。當然也可以很方便地換成其他的關系型數據庫。 打開Web.Config文件可以查看和配置鏈接字符串:
<add name="Default" connectionString="Server=localhost; Database=SimpleTaskSystemDb; Trusted_Connection=True;" />

創建實體 把實體類寫在Core項目中,因為實體是領域層的一部分。 一個簡單的應用場景:創建一些任務(tasks)並分配給人。 我們需要Task和Person這兩個實體。 Task實體有幾個屬性:描述(Description)、創建時間(CreationTime)、任務狀態(State),還有可選的導航屬性(AssignedPerson)來引用Person。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public
class
Task : Entity<
long
>
{
[ForeignKey(
"AssignedPersonId"
)]
public
virtual
Person AssignedPerson {
get
;
set
; }
public
virtual
int
? AssignedPersonId {
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;
}
}
|
1
2
3
4
|
public
class
Person : Entity
{
public
virtual
string
Name {
get
;
set
; }
}
|
創建DbContext 使用EntityFramework需要先定義DbContext類,ABP的模板已經創建了DbContext文件,我們只需要把Task和Person類添加到IDbSet,請看代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public
class
SimpleTaskSystemDbContext : AbpDbContext
{
public
virtual
IDbSet<Task> Tasks {
get
;
set
; }
public
virtual
IDbSet<Person> People {
get
;
set
; }
public
SimpleTaskSystemDbContext()
:
base
(
"Default"
)
{
}
public
SimpleTaskSystemDbContext(
string
nameOrConnectionString)
:
base
(nameOrConnectionString)
{
}
}
|
通過Database Migrations創建數據庫表 我們使用EntityFramework的Code First模式創建數據庫架構。ABP模板生成的項目已經默認開啟了數據遷移功能,我們修改SimpleTaskSystem.EntityFramework項目下Migrations文件夾下的Configuration.cs文件:
internal sealed class Configuration :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
DbMigrationsConfiguration<SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext>
{
public
Configuration()
{
AutomaticMigrationsEnabled =
false
;
}
protected
override
void
Seed(SimpleTaskSystem.EntityFramework.SimpleTaskSystemDbContext context)
{
context.People.AddOrUpdate(
p => p.Name,
new
Person {Name =
"Isaac Asimov"
},
new
Person {Name =
"Thomas More"
},
new
Person {Name =
"George Orwell"
},
new
Person {Name =
"Douglas Adams"
}
);
}
}
|
在VS2013底部的“程序包管理器控制台”窗口中,選擇默認項目並執行命令“Add-Migration InitialCreate”
會在Migrations文件夾下生成一個xxxx-InitialCreate.cs文件,內容如下:
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
|
public
partial
class
InitialCreate : DbMigration
{
public
override
void
Up()
{
CreateTable(
"dbo.StsPeople"
,
c =>
new
{
Id = c.Int(nullable:
false
, identity:
true
),
Name = c.String(),
})
.PrimaryKey(t => t.Id);
CreateTable(
"dbo.StsTasks"
,
c =>
new
{
Id = c.Long(nullable:
false
, identity:
true
),
AssignedPersonId = c.Int(),
Description = c.String(),
CreationTime = c.DateTime(nullable:
false
),
State = c.Byte(nullable:
false
),
})
.PrimaryKey(t => t.Id)
.ForeignKey(
"dbo.StsPeople"
, t => t.AssignedPersonId)
.Index(t => t.AssignedPersonId);
}
public
override
void
Down()
{
DropForeignKey(
"dbo.StsTasks"
,
"AssignedPersonId"
,
"dbo.StsPeople"
);
DropIndex(
"dbo.StsTasks"
,
new
[] {
"AssignedPersonId"
});
DropTable(
"dbo.StsTasks"
);
DropTable(
"dbo.StsPeople"
);
}
}
|
1
|
PM> Update-Database
|
數據庫顯示如下:
(以后修改了實體,可以再次執行Add-Migration和Update-Database,就能很輕松的讓數據庫結構與實體類的同步)
定義倉儲接口 通過倉儲模式,可以更好把業務代碼與數據庫操作代碼更好的分離,可以針對不同的數據庫有不同的實現類,而業務代碼不需要修改。 定義倉儲接口的代碼寫到Core項目中,因為倉儲接口是領域層的一部分。 我們先定義Task的倉儲接口:
1
2
3
4
|
public
interface
ITaskRepository : IRepository<Task,
long
>
{
List<Task> GetAllWithPeople(
int
? assignedPersonId, TaskState? state);
}
|
它繼承自ABP框架中的IRepository泛型接口。 在IRepository中已經定義了常用的增刪改查方法:
所以ITaskRepository默認就有了上面那些方法。可以再加上它獨有的方法GetAllWithPeople(...)。
不需要為Person類創建一個倉儲類,因為默認的方法已經夠用了。ABP提供了一種注入通用倉儲的方式,將在后面“創建應用服務”一節的TaskAppService類中看到。
實現倉儲類 我們將在EntityFramework項目中實現上面定義的ITaskRepository倉儲接口。
通過模板建立的項目已經定義了一個倉儲基類:SimpleTaskSystemRepositoryBase(這是一種比較好的實踐,因為以后可以在這個基類中添加通用的方法)。
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
|
public
class
TaskRepository : SimpleTaskSystemRepositoryBase<Task,
long
>, ITaskRepository
{
public
List<Task> GetAllWithPeople(
int
? assignedPersonId, TaskState? state)
{
//在倉儲方法中,不用處理數據庫連接、DbContext和數據事務,ABP框架會自動處理。
var query = GetAll();
//GetAll() 返回一個 IQueryable<T>接口類型
//添加一些Where條件
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)
.Include(task => task.AssignedPerson)
.ToList();
}
}
|
TaskRepository繼承自SimpleTaskSystemRepositoryBase並且實現了上面定義的ITaskRepository接口。
創建應用服務(Application Services) 在Application項目中定義應用服務。首先定義Task的應用服務層的接口:
1
2
3
4
5
6
|
public
interface
ITaskAppService : IApplicationService
{
GetTasksOutput GetTasks(GetTasksInput input);
void
UpdateTask(UpdateTaskInput input);
void
CreateTask(CreateTaskInput input);
}
|
ITaskAppService繼承自IApplicationService,ABP自動為這個類提供一些功能特性(比如依賴注入和參數有效性驗證)。
然后,我們寫TaskAppService類來實現ITaskAppService接口:
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
|
public
class
TaskAppService : ApplicationService, ITaskAppService
{
private
readonly
ITaskRepository _taskRepository;
private
readonly
IRepository<Person> _personRepository;
/// <summary>
/// 構造函數自動注入我們所需要的類或接口
/// </summary>
public
TaskAppService(ITaskRepository taskRepository, IRepository<Person> personRepository)
{
_taskRepository = taskRepository;
_personRepository = personRepository;
}
public
GetTasksOutput GetTasks(GetTasksInput input)
{
//調用Task倉儲的特定方法GetAllWithPeople
var tasks = _taskRepository.GetAllWithPeople(input.AssignedPersonId, input.State);
//用AutoMapper自動將List<Task>轉換成List<TaskDto>
return
new
GetTasksOutput
{
Tasks = Mapper.Map<List<TaskDto>>(tasks)
};
}
public
void
UpdateTask(UpdateTaskInput input)
{
//可以直接Logger,它在ApplicationService基類中定義的
Logger.Info(
"Updating a task for input: "
+ input);
//通過倉儲基類的通用方法Get,獲取指定Id的Task實體對象
var task = _taskRepository.Get(input.TaskId);
//修改task實體的屬性值
if
(input.State.HasValue)
{
task.State = input.State.Value;
}
if
(input.AssignedPersonId.HasValue)
{
task.AssignedPerson = _personRepository.Load(input.AssignedPersonId.Value);
}
//我們都不需要調用Update方法
//因為應用服務層的方法默認開啟了工作單元模式(Unit of Work)
//ABP框架會工作單元完成時自動保存對實體的所有更改,除非有異常拋出。有異常時會自動回滾,因為工作單元默認開啟數據庫事務。
}
public
void
CreateTask(CreateTaskInput input)
{
Logger.Info(
"Creating a task for input: "
+ input);
//通過輸入參數,創建一個新的Task實體
var task =
new
Task { Description = input.Description };
if
(input.AssignedPersonId.HasValue)
{
task.AssignedPersonId = input.AssignedPersonId.Value;
}
//調用倉儲基類的Insert方法把實體保存到數據庫中
_taskRepository.Insert(task);
}
}
|
TaskAppService使用倉儲進行數據庫操作,它通往構造函數注入倉儲對象的引用。
數據驗證
如果應用服務(Application Service)方法的參數對象實現了IInputDto或IValidate接口,ABP會自動進行參數有效性驗證。
CreateTask方法有一個CreateTaskInput參數,定義如下:
1
2
3
4
5
6
7
|
public
class
CreateTaskInput : IInputDto
{
public
int
? AssignedPersonId {
get
;
set
; }
[Required]
public
string
Description {
get
;
set
; }
}
|
Description屬性通過注解指定它是必填項。也可以使用其他 Data Annotation 特性。
如果你想使用自定義驗證,你可以實現ICustomValidate 接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public
class
UpdateTaskInput : IInputDto, ICustomValidate
{
[Range(1,
long
.MaxValue)]
public
long
TaskId {
get
;
set
; }
public
int
? AssignedPersonId {
get
;
set
; }
public
TaskState? State {
get
;
set
; }
public
void
AddValidationErrors(List<ValidationResult> results)
{
if
(AssignedPersonId ==
null
&& State ==
null
)
{
results.Add(
new
ValidationResult(
"AssignedPersonId和State不能同時為空!"
,
new
[] {
"AssignedPersonId"
,
"State"
}));
}
}
}
|
你可以在AddValidationErrors方法中寫自定義驗證的代碼。
創建Web Api服務 ABP可以非常輕松地把Application Service的public方法發布成Web Api接口,可以供客戶端通過ajax調用。
1
2
3
|
DynamicApiControllerBuilder
.ForAll<IApplicationService>(Assembly.GetAssembly(
typeof
(SimpleTaskSystemApplicationModule)),
"tasksystem"
)
.Build();
|
SimpleTaskSystemApplicationModule這個程序集中所有繼承了IApplicationService接口的類,都會自動創建相應的ApiController,其中的公開方法,就會轉換成WebApi接口方法。
可以通過http://xxx/api/services/tasksystem/Task/GetTasks這樣的路由地址進行調用。
通過上面的案例,大致介紹了領域層、基礎設施層、應用服務層的用法。
現在,可以在ASP.NET MVC的Controller的Action方法中直接調用Application Service的方法了。
如果用SPA單頁編程,可以直接在客戶端通過ajax調用相應的Application Service的方法了(通過創建了動態Web Api)。