為什麼使用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。
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; } }Person實體更簡單,只定義了一個Name屬性:
public class Person : Entity { public virtual string Name { get; set; } }在ABP框架中,有一個Entity基類,它有一個Id屬性。因為Task類繼承自Entity<long>,所以它有一個long類型的Id。Person類有一個int類型的Id,因為int類型是Entity基類Id的默認類型,沒有特別指定類型時,實體的Id就是int類型。
創建DbContext
使用EntityFramework需要先定義DbContext類,ABP的模板已經創建了DbContext文件,我們只需要把Task和Person類添加到IDbSet,請看代碼:
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 :
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文件,內容如下:
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"); } }然後繼續在“程序包管理器控制台”執行“Update-Database”,會自動在數據庫創建相應的數據表:
PM> Update-Database
數據庫顯示如下:
(以後修改了實體,可以再次執行Add-Migration和Update-Database,就能很輕松的讓數據庫結構與實體類的同步)
定義倉儲接口
通過倉儲模式,可以更好把業務代碼與數據庫操作代碼更好的分離,可以針對不同的數據庫有不同的實現類,而業務代碼不需要修改。
定義倉儲接口的代碼寫到Core項目中,因為倉儲接口是領域層的一部分。
我們先定義Task的倉儲接口:
public interface ITaskRepository : IRepository<Task, long> { List<Task> GetAllWithPeople(int? assignedPersonId, TaskState? state); }
它繼承自ABP框架中的IRepository泛型接口。
在IRepository中已經定義了常用的增刪改查方法:
所以ITaskRepository默認就有了上面那些方法。可以再加上它獨有的方法GetAllWithPeople(...)。
不需要為Person類創建一個倉儲類,因為默認的方法已經夠用了。ABP提供了一種注入通用倉儲的方式,將在後面“創建應用服務”一節的TaskAppService類中看到。
實現倉儲類
我們將在EntityFramework項目中實現上面定義的ITaskRepository倉儲接口。
通過模板建立的項目已經定義了一個倉儲基類:SimpleTaskSystemRepositoryBase(這是一種比較好的實踐,因為以後可以在這個基類中添加通用的方法)。
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的應用服務層的接口:
public interface ITaskAppService : IApplicationService { GetTasksOutput GetTasks(GetTasksInput input); void UpdateTask(UpdateTaskInput input); void CreateTask(CreateTaskInput input); }
ITaskAppService繼承自IApplicationService,ABP自動為這個類提供一些功能特性(比如依賴注入和參數有效性驗證)。
然後,我們寫TaskAppService類來實現ITaskAppService接口:
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參數,定義如下:
public class CreateTaskInput : IInputDto { public int? AssignedPersonId { get; set; } [Required] public string Description { get; set; } }
Description屬性通過注解指定它是必填項。也可以使用其他 Data Annotation 特性。
如果你想使用自定義驗證,你可以實現ICustomValidate 接口:
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調用。
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)。