ABP开发框架的技术点分析
- ABP框架全称为
“ASP.NET Boilerplate Project”
,中文翻译为“ ASP.NET样板项目”,诞生的主要目的就是为了让.NET程序员“秒变”架构师,将.NET企业级项目的主流开发技术、最先进的架构整合起来,让.NET工程师能够更快的开发出更好的项目。
- ASP.NET> Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板。
- ABP不仅仅是一个框架,ASP.NET Boilerplate 基于DDD的经典分层架构思想,实现了众多DDD的概念(但没有实现所有DDD的概念)。它提供了一个最徍实践的基于领域驱动设计(DDD)的体系结构模型。ABP框架可以说是.net core整合非常多技术点的一个很好的框架,整个涉及到很多非常多方面的知识。我们来大概了解下ABP框架涉及到的内容。
框架所包含技术如下:
1、 .NET MVC 5、Web API 2、C# 5.0。
2、领域驱动设计,如实体、仓储、领域服务、领域事件、应用服务、数据传输对象、工作单元等。
3、分层体系结构:基础设施层 -> 领域层 -> 应用层 -> 展现层。
4、提供一个基础架构来开发可重用可配置的模块。
5、集成现今主流流行的前端开发框架(Bootstrap、Less、AngularJs、jQuery、Modernizr、
jQuery.validate、jQuery.form、jQuery.blockUI、json2等)。
6、提供一个基础架构实现IOC(依赖注入,主要采用Castle Windsor)。
7、支持并实现数据迁移,这里主要采用Entity Framework。
8、模块化开发,每个模块可单独指定数据库,拥有独立的EF DbContext。
9、包含一个简单灵活的多语言/本地系统。
10、通过EventBus实现服务端全局领域事件。
11、统一异常处理,应用层不需要自己写异常处理代码。
12、提供针对Application层方法的参数有效性认证。
13、通过Application Service创建Web API层,无需编写ApiController。
14、提供基类帮助用户实现一些常见任务。
15、约定优于配置。
16、Zero模块提供身份验证、授权管理、用户&角色管理、系统设置存取管理、审计日志。
ASP.NET ZERO 是 利用ABP框架搭建的模板项目,它会提供预建的页面及强大的基础设施架构。利用它提供的基础框架代码能让你快速的开发你的应用层。
使用说明:
1、先编译成功,Nuget下载ABP的依赖dll
2、在建立一个名为AbpZeroTemplate的数据库,并修改web.config里的连接字符串
3、选择MyCompanyName.AbpZeroTemplate.Web为启动项,F5运行,此时会自动生成数据库表结构
4、VS菜单:工具->Nuget 程序包管理器->程序包管理器控制台
默认项目里选择:MyCompanyName.AbpZeroTemplate.EntityFramework
PM>update-database 回车
5、再次F5运行即可进入登录,初始帐号:admin 密码:123qwe
6、关于重命名查找:namespace MyCompanyName. 替换为:namespace ABC.
1、领域层(Domain)
领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。
实体(Entity)
实体代表业务领域的数据和操作,在实践中,通过用来映射成数据库表。
仓储(Repository)
仓储用来操作数据库进行数据存取。仓储接口在领域层定义,而仓储的实现类应该写在基础设施层。
领域服务(Domain service)
当处理的业务规则跨越两个(及以上)实体时,应该写在领域服务方法里面。
领域事件(Domain Event)
在领域层某些特定情况发生时可以触发领域事件,并且在相应地方捕获并处理它们。
工作单元(Unit of Work)
工作单元是一种设计模式
,用于维护一个由已经被修改(如增加、删除和更新等)的业务对象组成的列表。它负责协调这些业务对象的持久化工作及并发问题
。
2、应用层(Application)
1.应用层提供一些应用服务(Application Services)方法供展现层调用。一个应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个DTO。
2.在展现层到领域层之间,不应该接收或返回实体(Entity)对象,应该进行DTO映射。
- 一个应用服务方法通常被认为是一个工作单元(Unit of Work)。 用户输入参数的验证工作也应该在应用层实现。
4.ABP提供了一个基础架构让我们很容易地实现输入参数有效性验证。建议使用一种像AutoMapper这样的工具来进行实体与DTO之间的映射。
3、基础设施层(Infrastructure)
当在领域层中为定义了仓储接口,应该在基础设施层中实现这些接口。可以使用ORM工具,例如EntityFramework或NHibernate。ABP的基类已经提供了对这两种ORM工具的支持。数据库迁移也被用于这一层。
4、Web与展现层(Web&Presentation)
Web层使用ASP.NET MVC和Web API来实现。
可分别用于多页面应用程序(MPA)和单页面应用程序(SPA)。在SPA中,所有资源被一次加载到客户端浏览器中(或者先只加载核心资源,其他资源懒加载),然后通过AJAX调用服务端WebApi接口获取数据,再根据数据生成HTML代码。不会整个页面刷新。现在已经有很多SPA的JS框架,例如: AngularJs、 DurandalJs、BackboneJs、EmberJs。 ABP可以使用任何类似的前端框架,但是ABP提供了一些帮助类,让我们更方便地使用AngularJs和DurandalJs。
在经典的多页面应用(MPA)中,客户端向服务器端发出请求,服务器端代码(ASP.NET MVC控制器)从数据库获得数据,并且使用Razor视图生成HTML。这些被生成后的HTML页面被发送回客户端显示。每显示一个新的页面都会整页刷新。
SPA和MPA涉及到完全不同的体系结构,也有不同的应用场景。一个管理后台适合用SPA,博客就更适合用MPA,因为它更利于被搜索引擎抓取。
SignalR是一种从服务器到客户端发送推送通知的完美工具。它能给用户提供丰富的实时的体验。
已经有很多客户端的Javascript框架或库,JQuery是其中最流行的,并且它有成千上万免费的插件。使用Bootstrap可以让我们更轻松地完成写Html和CSS的工作。
ABP也实现了根据Web API接口自动创建 Javascript的代码函数,来简化JS对Web Api的调用。还有把服务器端的菜单、语言、设置等生成到JS端。(但是在我自己的项目中,我是把这些自动生成功能关闭的,因为必要性不是很大,而这些又会比较影响性能)。
ABP会自动处理服务器端返回的异常,并以友好的界面提示用户。
5、其他层:
ABP使用Castle Windsor为整个程序框架提供依赖注入的功能。使用Log4Net日志记录组件,提供给其他各层调用以进行日志记录。
- 依赖注入这个部分使用 Castle windsor (依赖注入容器)来实现依赖注入,这个也是我们经常使用IOC来处理的方式;
- Repository仓储模式,已实现了Entity Framework、NHibernate、MangoDB、内存数据库等,仓储模式可以快速实现对数据接口的调用;
- 身份验证与授权管理
可以使用声明特性的方式对用户是否登录,或者接口的权限进行验证,可以通过一个很细粒度的方式,对各个接口的调用权限进行设置; - 数据有效性验证
ABP自动对接口的输入参数对象进行非空判断,并且可以根据属性的申请信息对属性的有效性进行校验; - 审计日志记录
也就是记录我们对每个接口的调用记录,以及对记录的创建、修改、删除人员进行记录等处理; - Unit Of Work工作单元模式
为应用层和仓储层的方法自动实现数据库事务,默认所有应用服务层的接口,都是以工作单元方式运行,即使它们调用了不同的存储对象处理,都是处于一个事务的逻辑里面; - 异常处理
ABP框架提供了一整套比较完善的流程处理操作,可以很方便的对异常进行进行记录和传递; - 日志记录
我么可以利用Log4Net进行常规的日志记录,方便我们跟踪程序处理信息和错误信息; - 多语言/本地化支持
ABP框架对多语言的处理也是比较友好的,提供了对XML、JSON语言信息的配置处理; - Auto Mapping自动映射
这个是ABP的很重要的对象隔离概念,通过使用AutoMaper来实现域对象和DTO对象的属性映射,可以隔离两者的逻辑关系,但是又能轻松实现属性信息的赋值;
11.动态Web API层
利用这个动态处理,可以把Application Service 直接发布为Web API层,而不需要在累赘的为每个业务对象手工创建一个Web API的控制器,非常方便;
- 动态JavaScript的AJax代理处理
可以自动创建Javascript 的代理层来更方便使用Web Api,这个在Web层使用。
1、ABP应用框架项目结构
一般我们涉及到的ABP框架,可能解决方案上都会有这些项目。
而这些项目使用了ABP框架的底层框架模块,使用基础模块可以极大简化应用框架的规模,提高效率,并抽象常规的功能和约定处理,如下所示。
基础的ABP框架实现(地址https://github.com/aspnetboilerplate/aspnetboilerplate),这个是我们所说的ABP框架的核心实现;
上面提到的在基础ABP框架基础上实现处理的ABP应用框架,如下所示。
它主要是分为下面几个项目分层。
Core领域核心层
,领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。这个项目里面,除了定义所需的领域实体类外,其实可以定义我们自己的自定义的仓储对象(类似DAL/IDAL),以及定义自己的业务逻辑层(类似BLL/IBLL),以及基于AutoMapper映射规则等内容。
**EntityFrameworkCore 实体框架核心层**
,这个项目不需要修改太多内容,只需要在DbContext里面加入对应领域对象的仓储对象即可。
Application.Common和Application应用层
:应用层提供一些应用服务(Application Services)方法供展现层调用。一个应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个DTO。
**Web.Core Web核心层**
,基于Web或者Web API的核心层,提供了对身份登陆验证的基础处理,没有其他内容。
**Web.Core.Host Web API的宿主层**
,也是动态发布Web API的核心内容,另外在Web API里面整合了Swagger,使得我们可以方便对Web API的接口进行调试。
**Migrator数据迁移层**
,这个是一个辅助创建的控制台程序项目,如果基于DB First,我们可以利用它来创建我们项目的初始化数据库。
2、ABP框架的动态Web API
我们知道,**一般我们发布需要实现Web API的发布,需要创建对应的Web API控制器类,然后在Web API应用中注册对应的路由来处理**
。
由于ABP框架的Application Service层已经是无限接近Web API层的定义了,而且本身ABP框架已经抽象划分了几个不同的分层,再引入一个类似Application Service层的Web API层,显得多余且累赘,所以ABP框架约定了Application Service层继承接口和命名规则,也是为引入动态Web API做铺垫的,用一个通用的Host层,统一动态发布所有的Web API层,减轻了繁复且累赘的Web API 控制器的定义。
使用ABP自带的例子来说明,例如有如下的接口定义。
public interface ITaskAppService : IApplicationService
{
GetTasksOutput GetTasks(GetTasksInput input);
void UpdateTask(UpdateTaskInput input);
void CreateTask(CreateTaskInput input);
}
然后其使用动态发布Web API的方式类似如下逻辑所示。
Configuration.Modules
.AbpWebApi()
.DynamicApiControllerBuilder
.For<ITaskAppService>("tasksystem/task").Build();
当然这个是对于特定的接口的处理,通用的动态Web API发布处理,会通过反射获得对应的接口列表,然后是逐一进行处理的。
我们这里如果需要详细追究ABP的动态Web API发布的规则处理,可以参考ABP的基础框架部分,了解 DynamicApiControllerBuilder 的处理逻辑即可。
而ABP框架动态发布Web API,对应的控制器的方法,约定会根据命名规则进行处理,默认一般为Post,规则如下所示:
Get: 如果方法名以 'Get'开始
Put: 如果方法名以 'Put' 或 'Update' 开始
Delete: 如果方法名以 'Delete' 或 'Remove' 开始
Post: 如果方法名以 'Post' 、 'Create' 或 'Insert' 开始.
Patch: 如果方法名以 'Patch' 开始.
默认以 POST 方式作为 HTTP 动作.
有了动态发布Web API层,我们就不需要在Web API层中复制一份类似Application Service的定义和实现了,这样可以省却很多麻烦事情,减少维护的代码。
3、依赖注入的仓储模式
ABP使用并提供常规的依赖注入。可以简单地注入任何依赖项
(例如:IRepository <Authorization.Tasks.Task>)
依赖注入其实也是IOC,实现了接口的控制反转,可以在程序启动的时候,统一根据接口加载对应的实现,而使用的时候,我们只需要知道接口的使用方法即可。现在的EntityFramework 实体框架都是基于IOC实现的了。
我们这里假设您已经知道依赖注入带来的好处和大概的处理,
依赖注入一般分为构造函数的注入,和属性注入。让我们来看看一个具体的依赖注入实现的代码。
public class TaskAppService : ApplicationService, ITaskAppService
{
private readonly IRepository<Task> _taskRepository;
public TaskAppService(IRepository<Task> taskRepository)
{
_taskRepository = taskRepository;
}
public async Task UpdateTask(UpdateTaskInput input)
{
Logger.Info("Updating a task for input: " + input);
var task = await _taskRepository.FirstOrDefaultAsync(input.TaskId);
if (task == null)
{
throw new UserFriendlyException(L("CouldNotFindTheTaskMessage"));
}
ObjectMapper.MapTo(input, task);
}
}
这里的_taskRepository 就是仓储接口的依赖注入,这个具体的实现是在启动的时候,有IOC容器进行动态的加入。使用的时候我们在各个函数里面都是调用对应的仓储接口(而不是实现)来处理信息的。
属性注入的做法类似只是提供了一个Public的属性定义供动态设置属性接口。
public class PersonAppService
{
public ILogger Logger { get; set; }
private IPersonRepository _personRepository;
public PersonAppService(IPersonRepository personRepository)
{
_personRepository = personRepository;
Logger = NullLogger.Instance;
}
public void CreatePerson(string name, int age)
{
Logger.Debug("Inserting a new person to database with name = " + name);
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
Logger.Debug("Successfully inserted!");
}
}
ABP的依赖注入基于 Castle Windsor框架。Castle Windsor最成熟的DI框架之一。
还有很多这样的框架,如Unity,Ninject,StructureMap,Autofac等等。
接口的实例初始化,一般我们启动程序的时候,使用IoC容器进行统一的处理,如下所示。
var container = new WindsorContainer();
container.Register(
Component.For<IPersonRepository>().ImplementedBy<PersonRepository>().LifestyleTransient(),
Component.For<IPersonAppService>().ImplementedBy<PersonAppService>().LifestyleTransient()
);
var personService = container.Resolve<IPersonAppService>();
personService.CreatePerson("John Doe", 32);
而在ABP框架里面,一般可以通过 IocManager 来根据程序集统一进行接口的实例化处理,按照约定注册程序集。
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
Assembly.GetExecutingAssembly()得到一个对包括此代码的程序集的引用。
按照约定,ABP自动注册所有 Repositories, Domain Services,
Application Services, MVC 控制器和Web API控制器。
另外,ABP框架的数据处理采用了EF框架的仓储模式来处理数据的增删改查等处理,可以实现多种数据库的兼容,而且能够抽象实现常规数据操作接口,以及提供非常方便的LINQ处理方式。
在ABP中,仓储类要实现IRepository接口。在ABP基础模块中,它的接口定义如下所示。
对于仓储类,IRepository定义了许多泛型的方法。比如: Select,Insert,Update,Delete方法(CRUD操作)。在大多数的时候,这些方法已足已应付一般实体的需要。
一般我们定义一些对应的DTO以及领域对象,然后依据对应的接口来实现业务对象的仓储处理。
例如对于字典类型来说,定义的领域对象如下所示。
namespace MyProject.Dictionary
{
[Table("TB_DictType")]
public class DictType : FullAuditedEntity<string>
{
/// <summary>
/// 类型名称
/// </summary>
[Required]
public virtual string Name { get; set; }
/// <summary>
/// 字典代码
/// </summary>
public virtual string Code { get; set; }
/// <summary>
/// 父ID
/// </summary>
public virtual string PID { get; set; }
/// <summary>
/// 备注
/// </summary>
public virtual string Remark { get; set; }
/// <summary>
/// 排序
/// </summary>
public virtual string Seq { get; set; }
}
}
然后在应用层就直接使用通用的仓储对象接口即可。
/// <summary>
/// 字典类型应用服务层实现
/// </summary>
[AbpAuthorize]
public class DictTypeAppService : MyAsyncServiceBase<DictType, DictTypeDto, string, DictTypePagedDto, CreateDictTypeDto, DictTypeDto>, IDictTypeAppService
{
/// <summary>
/// 标准的仓储对象
/// </summary>
private readonly IRepository<DictType, string> _repository;
public DictTypeAppService(IRepository<DictType, string> repository) : base(repository)
{
_repository = repository;
}
............
}
因为这些通用的仓储对象接口已经很多,常规都是够用的,而不需要进行特定仓储对象的自定义封装处理,否则徒增烦恼。