点这里进入ABP系列文章总目录html
基于DDD的现代ASP.NET开发框架--ABP系列之二、ABP入门教程
git
ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称。github
ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板。web
ABP的官方网站:http://www.aspnetboilerplate.comajax
ABP在Github上的开源项目:https://github.com/aspnetboilerplate数据库
“DRY——避免重复代码”是一个优秀的开发者在开发软件时所具有的最重要的思想之一。咱们在开发企业WEB应用程序时都有一些相似的需求,例如:都须要登陆页面、用户/角色管理、权限验证、数据有效性验证、多语言/本地化等等。一个高品质的大型软件都会运用一些最佳实践,例如分层体系结构、领域驱动设计、依赖注入等。咱们也可能会采用ORM、数据库迁移(Database Migrations)、日志记录(Logging)等工具。编程
从零开始建立一个企业应用程序是一件繁琐的事,由于须要重复作不少常见的基础工做。许多公司都在开发本身的应用程序框架来重用于不一样的项目,而后在框架的基础上开发一些新的功能。但并非每一个公司都有这样的实力。假如咱们能够分享的更多,也许能够避免每一个公司或每一个项目的重复编写相似的代码。做者之因此把项目命名为“ASP.NET Boilerplate”,就是但愿它能成为开发通常企业WEB应用的新起点,直接把ABP做为项目模板。json
ABP是为新的现代Web应用程序使用最佳实践和使用最流行工具的一个起点。可做为通常用途的应用程序的基础框架或项目模板。它的功能包括:后端
ABP 提供了一个应用程序开发模型用于最佳实践。它拥有基础类、接口和工具使咱们容易创建起可维护的大规模的应用程序。api
然而:
它不是RAD工具之一,RAD工具的目的是无需编码建立应用程序。相反,ABP提供了一种编码的最佳实践。
它不是一个代码生成工具。在运行时虽然它有一些特性构建动态代码,但它不能生成代码。
它不是一个一体化的框架。相反,它使用流行的工具/库来完成特定的任务(例如用EF作ORM,用Log4Net作日志记录,使得Castle Windsor做为赖注入容器, AngularJs 用于SPA 框架)。
就我使用了ABP几个月的经验来看,虽然ABP不是RAD,可是用它开发项目绝对比传统三层架构要快不少。
虽然ABP不是代码生成工具,但由于有了它,使咱们项目的代码更简洁规范,这有利于使用代码生成工具。
我本身使用VS2013的Scaffolder+T4开发的代码生成器,可根据领域对象的UML类图自动生成所有先后端代码和数据库,简单的CURD模块几乎不须要编写代码,有复杂业务逻辑的模块主要补充领域层代码便可。这样就能把时间多花在领域模型的设计上,减小写代码的时间。
下面经过原做者的“简单任务系统”例子,演示如何运用ABP开发项目
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类型。
使用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架构设计交流QQ群:134710707