点这里进入ABP系列文章总目录
基于DDD的现代ASP.NET开发框架--ABP系列之2、ABP入门教程
ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称。
ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板。
ABP的官方网站 : http://www.aspnetboilerplate.com
ABP在Github上的开源项目: https://github.com/aspnetboilerplate
“DRY——避免重复代码”是一个优秀的开发者在开发软件时所具备的最重要的思想之一。我们在开发企业WEB应用程序时都有一些类似的需求,例如:都需要登录页面、用户/角色管理、权限验证、数据有效性验证、多语言/本地化等等。一个高品质的大型软件都会运用一些最佳实践,例如分层体系结构、领域驱动设计、依赖注入等。我们也可能会采用ORM、数据库迁移(Database Migrations)、日志记录(Logging)等工具。
从零开始创建一个企业应用程序是一件繁琐的事,因为需要重复做很多常见的基础工作。许多公司都在开发自己的应用程序框架来重用于不同的项目,然后在框架的基础上开发一些新的功能。但并不是每个公司都有这样的实力。假如我们可以分享的更多,也许可以避免每个公司或每个项目的重复编写类似的代码。作者之所以把项目命名为“ASP.NET Boilerplate”,就是希望它能成为开发一般企业WEB应用的新起点,直接把ABP作为项目模板。
ABP是为新的现代Web应用程序使用最佳实践和使用最流行工具的一个起点。可作为一般用途的应用程序的基础框架或项目模板。它的功能包括:
ABP 提供了一个应用程序开发模型用于最佳实践。它拥有基础类、接口和工具使我们容易建立起可维护的大规模的应用程序。
然而:
它不是RAD工具之一,RAD工具的目的是无需编码创建应用程序。相反,ABP提供了一种编码的最佳实践。
它不是一个代码生成工具。在运行时虽然它有一些特性构建动态代码,但它不能生成代码。
它不是一个一体化的框架。相反,它使用流行的工具/库来完成特定的任务(例如用EF做ORM,用Log4Net做日志记录,使得Castle Windsor作为赖注入容器, AngularJs 用于SPA 框架)。
就我使用了ABP几个月的经验来看,虽然ABP不是RAD,但是用它开发项目绝对很快。
虽然ABP不是代码生成工具,但因为有了它,使用我们项目的代码简洁规范,很有利于使用代码生成工具。
我自己使用VS2013的Scaffolder+T4开发的代码生成器,可根据领域对象的UML类图自动生成全部前后端代码和数据库,简单的CURD模块几乎不需要编写代码,有复杂业务逻辑的模块主要补充领域层代码即可。这样就能把时间多花在领域模型的设计上,减少写代码的时间。
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;" />
在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类型。
使用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) { } }
我们使用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 项目中定义应用服务。首先定义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 方法中写自定义验证的代码。
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)。
由于时间关系,展现层没有在本文中介绍,将放到以后的文章介绍。后续文章中也将会详细介绍每一层的具体知识要点。
如果想立即看到更多展示,可以查看我以前的文章 《新思想、新技术、新架构——更好更快的开发现代ASP.NET应用程序(续1)》 , 比较完整的演示了一个简单模块的开发,包括前后端各层的代码。(我自己项目用的ABP框架是在原作者的基础上做了一些修改,所以有些地方可能跟原作者的ABP不完全相同。)
由于演示一个完整的开发流程工作量巨大,写文章很难说得清楚,忙过这段时间我会准备用视频或YY在线的方式来分享,到时也可以分享我使用VS2013的Scaffolder+T4开发的代码生成器。
希望更多国内的架构师能关注到ABP这个项目,也许这其中有能帮助到您的地方,也许有您的参与,这个项目可以发展得更好。
点这里进入ABP系列文章总目录