基于ASP.NET MVC的ABP框架入门学习教程
为什么使用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;" />
(在后面用到EF的Code first数据迁移时,会自动在SQL Server数据库中创建一个名为SimpleTaskSystemDb的数据库。)
就这样,项目已经准备好运行了!打开VS2013并且按F5:
下面将逐步实现这个简单的任务系统程序
创建实体
把实体类写在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)。