ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称。
[新思想、新技术、新架构——更好更快的开发现代ASP.NET应用程序]
ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板。
ABP的官方网站:http://www.aspnetboilerplate.com
ABP在Github上的开源项目:https://github.com/aspnetboilerplate
(https://www.cnblogs.com/mienreal/p/4340864.html)
https://aspnetboilerplate.com/Pages/Documents/Zero/User-Management
ABP是为新的现代Web应用程序使用最佳实践和使用最流行工具的一个起点。可作为一般用途的应用程序的基础框架或项目模板。它的功能包括:
服务器端:
- 基于最新的.NET技术 (目前是ASP.NET MVC 5、Web API 2、C# 5.0,在ASP.NET 5正式发布后会升级)
- 实现领域驱动设计(实体、仓储、领域服务、领域事件、应用服务、数据传输对象,工作单元等等)
- 实现分层体系结构(领域层,应用层,展现层和基础设施层)
- 提供了一个基础架构来开发可重用可配置的模块
- 集成一些最流行的开源框架/库,也许有些是你正在使用的。
- 提供了一个基础架构让我们很方便地使用依赖注入(使用Castle Windsor作为依赖注入的容器)
- 提供Repository仓储模式支持不同的ORM(已实现Entity Framework 、NHibernate、MangoDb和内存数据库)支持并实现数据库迁移(EF 的 Code first)
- 模块化开发(每个模块有独立的EF DbContext,可单独指定数据库)
- 包括一个简单的和灵活的多语言/本地化系统
- 包括一个 EventBus来实现服务器端全局的领域事件
- 统一的异常处理(应用层几乎不需要处理自己写异常处理代码)
- 数据有效性验证(Asp.NET MVC只能做到Action方法的参数验证,ABP实现了Application层方法的参数有效性验证)
- 通过Application Services自动创建Web Api层(不需要写ApiController层了)
- 提供基类和帮助类让我们方便地实现一些常见的任务
- 使用“约定优于配置原则”
客户端:
- Bootstrap、Less、AngularJs、jQuery、Modernizr和其他JS库: jQuery.validate、jQuery.form、jQuery.blockUI、json2等
- 为单页面应用程序(AngularJs、Durandaljs)和多页面应用程序(Bootstrap+Jquery)提供了项目模板。
- 自动创建Javascript 的代理层来更方便使用Web Api封装一些Javascript 函数,更方便地使用ajax、消息框、通知组件、忙状态的遮罩层等等
除ABP框架项目以外,还开发了名叫“Zero”的模块,实现了以下功能:
- 身份验证与授权管理(通过ASP.NET Identity实现的)
- 用户&角色管理
- 系统设置存取管理(系统级、租户级、用户级,作用范围自动管理)
- 审计日志(自动记录每一次接口的调用者和参数)
ABP不是什么?
ABP 提供了一个应用程序开发模型用于最佳实践。它拥有基础类、接口和工具使我们容易建立起可维护的大规模的应用程序。
然而:
它不是RAD工具之一,RAD工具的目的是无需编码创建应用程序。相反,ABP提供了一种编码的最佳实践。
它不是一个代码生成工具。在运行时虽然它有一些特性构建动态代码,但它不能生成代码。
它不是一个一体化的框架。相反,它使用流行的工具/库来完成特定的任务(例如用EF做ORM,用Log4Net做日志记录,使得Castle Windsor作为赖注入容器, AngularJs 用于SPA 框架)。
就我使用了ABP几个月的经验来看,虽然ABP不是RAD,但是用它开发项目绝对比传统三层架构要快很多。
虽然ABP不是代码生成工具,但因为有了它,使我们项目的代码更简洁规范,这有利于使用代码生成工具。
我自己使用VS2013的Scaffolder+T4开发的代码生成器,可根据领域对象的UML类图自动生成全部前后端代码和数据库,简单的CURD模块几乎不需要编写代码,有复杂业务逻辑的模块主要补充领域层代码即可。这样就能把时间多花在领域模型的设计上,减少写代码的时间。
3、ABP分层架构
前言
为了减少复杂性和提高代码的可重用性,采用分层架构是一种被广泛接受的技术。
为了实现分层的体系结构,ABP遵循DDD(领域驱动设计)的原则,将分为四个层次:
- 展现层(Presentation):提供一个用户界面,实现用户交互操作。
- 应用层(Application):进行展现层与领域层之间的协调,协调业务对象来执行特定的应用程序的任务。它不包含业务逻辑。
- 领域层(Domain):包括业务对象和业务规则,这是应用程序的核心层。
- 基础设施层(Infrastructure):提供通用技术来支持更高的层。例如基础设施层的仓储(Repository)可通过ORM来实现数据库交互。
根据实际需要,可能会有额外添加的层。例如: - 分布式服务层(Distributed Service):用于公开应用程序接口供远程客户端调用。比如通过ASP.NET Web API和WCF来实现。
这些都是常见的以领域为中心的分层体系结构。不同的项目在实现上可能会有细微的差别。
领域层(Domain)
领域层就是业务层,是一个项目的核心,所有业务规则都应该在领域层实现。
实体(Entity)
实体代表业务领域的数据和操作,在实践中,通过用来映射成数据库表。
仓储(Repository)
仓储用来操作数据库进行数据存取。仓储接口在领域层定义,而仓储的实现类应该写在基础设施层。
领域服务(Domain service)
当处理的业务规则跨越两个(及以上)实体时,应该写在领域服务方法里面。
领域事件(Domain Event)
在领域层某些特定情况发生时可以触发领域事件,并且在相应地方捕获并处理它们。
工作单元(Unit of Work)
工作单元是一种设计模式,用于维护一个由已经被修改(如增加、删除和更新等)的业务对象组成的列表。它负责协调这些业务对象的持久化工作及并发问题。
应用层(Application)
应用层提供一些应用服务(Application Services)方法供展现层调用。一个应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入参数执行特定的领域层操作,并根据需要可返回另一个DTO。在展现层到领域层之间,不应该接收或返回实体(Entity)对象,应该进行DTO映射。一个应用服务方法通常被认为是一个工作单元(Unit of Work)。用户输入参数的验证工作也应该在应用层实现。ABP提供了一个基础架构让我们很容易地实现输入参数有效性验证。建议使用一种像AutoMapper这样的工具来进行实体与DTO之间的映射。
基础设施层(Infrastructure)
当在领域层中为定义了仓储接口,应该在基础设施层中实现这些接口。可以使用ORM工具,例如EntityFramework或NHibernate。ABP的基类已经提供了对这两种ORM工具的支持。数据库迁移也被用于这一层。
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会自动处理服务器端返回的异常,并以友好的界面提示用户。
其它
ABP使用Castle Windsor为整个程序框架提供依赖注入的功能。使用Log4Net日志记录组件,提供给其他各层调用以进行日志记录。
ABP系列之4、ABP模块系统
ABP模块系统简介
ABP框架提供了创建和组装模块的基础,一个模块能够依赖于另一个模块。在通常情况下,一个程序集就可以看成是一个模块。在ABP框架中,一个模块通过一个类来定义,而这个类要继承自AbpModule
。
译者注:如果学习过Orchard的朋友,应该知道module模块的强大了。模块的本质就是可重用性,你可以在任意的地方去调用,而且通过实现模块,你写的模块也可以给别人用。
Assembly程序集:Assembly是一个包含来程序的名称,版本号,自我描述,文件关联关系和文件位置等信息的一个集合。最简单的理解就是:一个你自己写的类库生成的dll就可以看做是一个程序集,这个程序集可以包括很多类,类又包括很多方法等。
.net可以通过反射获取一个程序集中的类以及方法。
下面的例子,我们开发一个可以在多个不同应用中被调用MybolgApplication模块,代码如下:
public class MyBlogApplicationModule : AbpModule //定义
{
public override void Initialize() //初始化
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
//这行代码的写法基本上是不变的。它的作用是把当前程序集的特定类或接口注册到依赖注入容器中。
}
}
ABP框架会扫描所有的程序集,并且发现AbpModule类中所有已经导入的所有类,如果你已经创建了包含多个程序集的应用,对于ABP,我们的建议是为每一个程序集创建一个Module(模块)。
生命期事件
在一个应用中,abp框架调用了Module
模块的一些指定的方法来进行启动和关闭模块的操作
。我们可以重载这些方法来完成我们自己的任务。
ABP框架通过依赖关系的顺序来调用这些方法,假如:模块A依赖于模块B,那么模块B要在模块A之前初始化,模块启动的方法顺序如下:
PreInitialize-B
PreInitialize-A
Initialize-B
Initialize-A
PostInitialize-B
PostInitialize-A
下面是具体方法的说明:
PreInitialize
预初始化:当应用启动后,第一次会调用这个方法。在依赖注入注册之前,你可以在这个方法中指定自己的特别代码。举个例子吧:假如你创建了一个传统的登记类,那么你要先注册这个类(使用IocManager对登记类进行注册),你可以注册事件到IOC容器。等。
Initialize
初始化:在这个方法中一般是来进行依赖注入的注册,一般我们通过IocManager.RegisterAssemblyByConvention这个方法来实现。如果你想实现自定义的依赖注入,那么请参考依赖注入的相关文档。
PostInitialize
提交初始化:最后一个方法,这个方法用来解析依赖关系。
Shutdown
关闭:当应用关闭以后,这个方法被调用。
模块依赖(Module dependencies)
Abp框架会自动解析模块之间的依赖关系,但是我们还是建议你通过重载GetDependencies方法来明确的声明依赖关系。
[DependsOn(typeof(MyBlogCoreModule))]//通过注解来定义依赖关系
public class MyBlogApplicationModule : AbpModule
{
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
例如上面的代码,我们就声明了MyBlogApplicationModule和MyBlogCoreModule的依赖关系(通过属性attribute),MyBlogApplicationModule这个应用模块依赖于MyBlogCoreModule核心模块,并且,MyBlogCoreModule核心模块会在MyBlogApplicationModule模块之前进行初始化。
如何自定义的模块方法
我们自己定义的模块中可能有方法被其他依赖于当前模块的模块调用,下面的例子,假设模块2依赖于模块1,并且想在预初始化的时候调用模块1的方法。
我们自己定义的模块中可能有方法被其他依赖于当前模块的模块调用,下面的例子,假设模块2依赖于模块1,并且想在预初始化的时候调用模块1的方法。
public class MyModule1 : AbpModule
{
public override void Initialize() //初始化模块
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());//这里,进行依赖注入的注册。
}
public void MyModuleMethod1()
{
//这里写自定义的方法。
}
}
[DependsOn(typeof(MyModule1))]
public class MyModule2 : AbpModule
{
private readonly MyModule1 _myModule1;
public MyModule2(MyModule1 myModule1)
{
_myModule1 = myModule1;
}
public override void PreInitialize()
{
_myModule1.MyModuleMethod1(); //调用MyModuleMethod1的方法。
}
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
就这样,就把模块1注入到了模块2,因此,模块2就能调用模块1的方法了。
阳铭注:
ABP的模块系统与Orchard的模块有类似之处,但还是有比较大的差别。Orchard的框架修改了ASP.NET程序集的默认加载方式(模块的DLL没有放在Bin文件夹下,是放在WEB项目根文件夹下面的Modules文件夹下),实现了功能模块的热插拔,而ABP的模块程序集还是放在Bin文件夹下的,没有实现热插拔。
ABP系列之5、ABP启动配置
译者注:在看这一节的内容之前,建议大家先下载module-zero这个例子代码,这个例子就是一个用户和角色的模块,并且使用的实例。配置在每一个应用中都可能会有,比如你有一个网站,你要获取网站的一些自定义基本参数,比如logo位置,网站名称,上传文件大小等等。模块化的配置方式和我们之前的做法肯定是不同的,大家要注意。之前无非就是一个方法getconfig从对应的表取数据,然后使用。
在应用启动之前,abp框架提供了模块基本的配置和方法,大家参照下面这个例子就可以了。
配置ABP
配置是通过在自己模块的PreInitialize方法中来实现的(对于module的PreInitialize方法,在上一篇中已经向大家做了简单的说明)
代码示例如下:
public class SimpleTaskSystemModule : AbpModule
{
public override void PreInitialize()
{
//在你的应用中添加语言包,这个是英语和作者的土耳其语。
Configuration.Localization.Languages.Add(new LanguageInfo("en", "English", "famfamfam-flag-england", true));
Configuration.Localization.Languages.Add(new LanguageInfo("tr", "Türkçe", "famfamfam-flag-tr"));
Configuration.Localization.Sources.Add(
new XmlLocalizationSource(
"SimpleTaskSystem",
HttpContext.Current.Server.MapPath("~/Localization/SimpleTaskSystem")
)
);
//配置导航和菜单
Configuration.Navigation.Providers.Add<SimpleTaskSystemNavigationProvider>();
}
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
和orchard类似,abp框架一开始就被设计成模块化的,不同的模块可以通过abp框架来进行配置。举个例子吧,不同的模块都可以添加导航,通过导航添加菜单项到自己定义的主菜单,具体的细节大家可以参照:
本地化:http://www.aspnetboilerplate.com/Pages/Documents/Localization
导航:http://www.aspnetboilerplate.com/Pages/Documents/Navigation
配置模块
和.net框架原生的启动配置相比较,abp有哪些不一样呢?abp框架的模块可以通过IAbpModuleConfigurations
接口进行个性化的扩展,这样的话,模块配置更加简单、方便。
示例代码如下:
复制代码
...
using Abp.Web.Configuration;
...
public override void PreInitialize()
{
Configuration.Modules.AbpWeb().SendAllExceptionsToClients = true;
}
...
在上面这个例子中,我们通过配置AbpWeb模块,发送异常到客户端。当然了,不是每一个模块都需要这种配置,通常情况下我们需要,是当一个模块需要在多个不同的应用中重复使用,我们才进行这样的配置。
为一个模块创建配置
如下代码,假如我们有一个命名为MyModule的模块,并且这各模块有一些自己的配置。那么我们首先要创建一些类,这些类定义为属性(译者注:属性有自动的get和set访问器。),代表了不同的配置。
public class MyModuleConfig
{
public bool SampleConfig1 { get; set; }
public string SampleConfig2 { get; set; }
}
接下来,我们通过依赖注入,注册这个类。
IocManager.Register(); //译者注:在IocManager中注册了一个类,换句话说,我们通过IocManager可以得到这个类MyModuleConfig的实例。至于IOC的原理这里就不在详细说了,总之,就是可以得到一个类的实例。
最后,我们通过创建一个扩展的方法IModuleConfigurations来得到配置的引用。如下代码:
译者注:模块配置是一个静态类,因为我们需要重复使用它。静态方法Mymodule返回的是一个配置接口,参数是ImoduleConfigurations接口。
现在,在其他模块中也可以配置我们自定义的这个MyModule模块了。
Configuration.Modules.MyModule().SampleConfig1 = false;
Configuration.Modules.MyModule().SampleConfig2 = "test";
在某种意义上,MyModule需要这些配置,你能注射MyModuleConfig并且可以使用这些值。
public class MyService : ITransientDependency
{
private readonly MyModuleConfig _configuration;
public MyService(MyModuleConfig configuration)
{
_configuration = configuration;
}
public void DoIt()
{
if (_configuration.SampleConfig2 == "test")
{
//...
}
}
}
这意味着,在abp框架的系统中,所有的模块都可以集中配置。
ABP系列之6、ABP依赖注入
什么是依赖注入
如果你已经知道依赖注入的概念,构造函数和属性注入模式,你可以跳过这一节。
维基百科说:“依赖注入是一种软件设计模式的一个或多个依赖项注入(或服务),或通过引用传递,为依赖对象(或客户)和客户端状态的一部分。模式之间建立一个客户的依赖关系的行为,它允许程序设计是松散耦合的,依赖倒置和单一职责原则。它直接对比service locator模式,它允许客户了解他们所使用的系统找到依赖。”。
如果不使用依赖注入技术,很难进行依赖管理、模块化开发和应用程序模块化。
传统方式的问题
在一个应用程序中,类之间相互依赖。假设我们有一个应用程序服务,使用仓储(repository)类插入实体到数据库。在这种情况下,应用程序服务类依赖于仓储(repository)类。看下例子:
public class PersonAppService
{
private IPersonRepository _personRepository;
public PersonAppService()
{
_personRepository = new PersonRepository();
}
public void CreatePerson(string name, int age)
{
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
}
}
PersonAppService使用PersonRepository插入Person到数据库。这段代码的问题:
- PersonAppService通过IPersonRepository调用CreatePerson方法,所以这方法依赖于IPersonRepository,代替了PersonRepository具体类。但PersonAppService(的构造函数)仍然依赖于PersonRepository。组件应该依赖于接口而不是实现。这就是所谓的依赖性倒置原则。
- 如果PersonAppService创建PersonRepository本身,它成为依赖IPersonRepository接口的具体实现,不能使用另一个实现。因此,此方式的将接口与实现分离变得毫无意义。硬依赖(hard-dependency)使得代码紧密耦合和较低的可重用。
- 我们可能需要在未来改变创建PersonRepository的方式。即,我们可能想让它创建为单例(单一共享实例而不是为每个使用创建一个对象)。或者我们可能想要创建多个类实现IPersonRepository并根据条件创建对象。在这种情况下,我们需要修改所有依赖于IPersonRepository的类。
- 有了这样的依赖,很难(或不可能)对PersonAppService进行单元测试。
为了克服这些问题,可以使用工厂模式。因此,创建的仓储类是抽象的。看下面的代码:
public class PersonAppService
{
private IPersonRepository _personRepository;
public PersonAppService()
{
_personRepository = PersonRepositoryFactory.Create();
}
public void CreatePerson(string name, int age)
{
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
}
}
`PersonRepositoryFactory 是一个静态类,创建并返回一个IPersonRepository。这就是所谓的服务定位器模式。`
以上依赖问题得到解决,因为PersonAppService不需要创建一个IPersonRepository的实现的对象,这个对象取决于PersonRepositoryFactory的Create方法。但是,仍然存在一些问题:
此时,PersonAppService取决于PersonRepositoryFactory。这是更容易接受,但仍有一个硬依赖(hard-dependency)。
为每个库或每个依赖项乏味的写一个工厂类/方法。
测试性依然不好,由于很难使得PersonAppService使用mock实现IPersonRepository。
解决方案
有一些最佳实践(模式)用于类依赖。
构造函数注入
重写上面的例子,如下所示:
public class PersonAppService
{
private IPersonRepository _personRepository;
public PersonAppService(IPersonRepository personRepository)
{
_personRepository = personRepository;
}
public void CreatePerson(string name, int age)
{
var person = new Person { Name = name, Age = age };
_personRepository.Insert(person);
}
}
这被称为构造函数注入。现在,PersonAppService不知道哪些类实现IPersonRepository以及如何创建它。谁需要使用PersonAppService,首先创建一个IPersonRepository PersonAppService并将其传递给构造函数,如下所示:
var repository = new PersonRepository();
var personService = new PersonAppService(repository);
personService.CreatePerson("Yunus Emre", 19);
构造函数注入是一个完美的方法,使一个类独立创建依赖对象。但是,上面的代码有一些问题:
创建一个PersonAppService变得困难。想想如果它有4个依赖,我们必须创建这四个依赖对象,并将它们传递到构造函数PersonAppService。
从属类可能有其他依赖项(在这里,PersonRepository可能有依赖关系)。所以,我们必须创建PersonAppService的所有依赖项,所有依赖项的依赖关系等等. .如此,依赖关系使得我们创建一个对象变得过于复杂了。
幸运的是,依赖注入框架自动化管理依赖关系。
属性注入
构造函数注入模式是一个完美的提供类的依赖关系的方式。通过这种方式,您不能创建类的实例,而不提供依赖项。它也是一个强大的方式显式地声明是什么类的需求正确地工作。
但是,在某些情况下,该类依赖于另一个类,但也可以没有它。这通常是适用于横切关注点(如日志记录)。一个类可以没有工作日志,但它可以写日志如果你提供一个日志对象。在这种情况下,您可以定义依赖为公共属性,而不是让他们放在构造函数。想想,如果我们想在PersonAppService写日志。我们可以重写类如下:
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!");
}
}
NullLogger.Instance 是一个单例对象,实现了ILogger接口,但实际上什么都没做(不写日志。它实现了ILogger实例,且方法体为空)。现在,PersonAppService可以写日志了,如果你为PersonAppService实例设置了Logger,如下面:
var personService = new PersonAppService(new PersonRepository());
personService.Logger = new Log4NetLogger();
personService.CreatePerson("Yunus Emre", 19);
假设Log4NetLogger实现ILogger实例,使得我们可以使用Log4Net库写日志。因此,PersonAppService可以写日志。如果我们不设置Logger,PersonAppService就不写日志。因此,我们可以说PersonAppService ILogger实例是一个可选的依赖。
几乎所有的依赖注入框架都支持属性注入模式
依赖注入框架
有许多依赖注入框架,都可以自动解决依赖关系。他们可以创建所有依赖项(递归地依赖和依赖关系)。所以你只需要根据注入模式写类和类构造函数&属性,其他的交给DI框架处理!在良好的应用程序中,类甚至独立于DI框架。整个应用程序只会有几行代码或类,显示的与DI框架交互。
ABP的依赖注入基于 Castle Windsor框架。Castle Windsor最成熟的DI框架之一。还有很多这样的框架,如Unity,Ninject,StructureMap,Autofac等等。
在使用一个依赖注入框架时,首先注册您的接口/类到依赖注入框架中,然后你就可以resolve一个对象。在Castle Windsor,它是这样的:
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("Yunus Emre", 19);
我们首先创建了WindsorContainer。然后注册PersonRepository 和 PersonAppService及它们的接口。然后我们要求容器创建一个IPersonAppService实例。它创建PersonAppService对象及其依赖项并返回。在这个简单的示例中,使用DI框架也许不是那么简洁,但想象下,在实际的企业应用程序中你会有很多类和依赖关系。当然,注册的依赖项只在程序启动的某个地方创建一次。
请注意,我们只是讲对象声明为临时对象(transient)。这意味着每当我们创建这些类型的一个对象时,就会创建一个新的实例。有许多不同的生命周期(如Singletion)。
ABP依赖注入的基础结构
在编写应用程序时遵循最佳实践和一些约定,ABP几乎让依赖注入框架使用变得无形。
- 注册
在ABP中,有很多种不同的方法来注册你的类到依赖注入系统。大部分时间,常规方法就足够了。 - 常规注册
按照约定,ABP自动注册所有 Repositories, Domain Services, Application Services, MVC 控制器和Web API控制器。例如,您可能有一个IPersonAppService 接口和实现类PersonAppService:
public interface IPersonAppService : IApplicationService
{
//...
}
public class PersonAppService : IPersonAppService
{
//...
}
ABP会自动注册它,因为它实现IApplicationService接口(它只是一个空的接口)。它会被注册为transient (每次使用都创建实例)。当你注入(使用构造函数注入)IPersonAppService接口成一个类,PersonAppService对象会被自动创建并传递给构造函数。
命名约定在这里非常重要。例如你可以将名字PersonAppService改为 MyPersonAppService或另一个包含“PersonAppService”后缀的名称,由于IPersonAppService包含这个后缀。但是你可以不遵循PeopleService命名您的服务类。如果你这样做,它将不会为IPersonAppService自动注册(它需要自注册(self-registration)到DI框架,而不是接口),所以,如果你想要你应该手动注册它。
ABP按照约定注册程序集。所以,你应该告诉ABP按照约定注册您的程序集。这很容易:
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
Assembly.GetExecutingAssembly()得到一个对包括此代码的程序集的引用。你可以通过RegisterAssemblyByConvention方法注册其他程序集。这同在你的模块初始化(AbpModule.Initialize())时完成。请查看ABP的模块系统获得更多信息。
您可以通过实现IConventionalRegisterer接口和调用IocManager。AddConventionalRegisterer方法编写自己的约定注册类。你应该将它添加到模块的pre-initialize方法中。
帮助接口
你可以注册一个特定的类,不遵循传统的约定制度规则。ABP提供了ITransientDependency和ISingletonDependency接口的快捷方法。例如:
public interface IPersonManager
{
//...
}
public class MyPersonManager : IPersonManager, ISingletonDependency
{
//...
}
以这种方式,您可以很容易地注册MyPersonManager为transient。当需要注入IPersonManager时,MyPersonManager会被使用。注意,依赖被声明为单例。因此,创建的MyPersonManager同一个对象被传递给所有需要的类。只是在第一次使用时创建,那么应用程序的整生命周期使用的是同一实例。
自定义/直接 注册
如果之前描述的方法还是不足以应对你的情况,你可以使用Castle Windsor注册类和及依赖项。因此,您将拥有Castle Windsor注册的所有能力。
可以实现IWindsorInstaller接口进行注册。您可以在应用程序中创建一个实现IWindsorInstaller接口的类:
public class MyInstaller : IWindsorInstaller
{
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(Classes.FromThisAssembly().BasedOn<IMySpecialInterface>().LifestylePerThread().WithServiceSelf());
}
}
Abp自动发现和执行这个类。最后,你可以通过使用IIocManager.IocContainer属性得到WindsorContainer。有关更多信息,阅读Windsor的文档。
解析(Resolving)
注册通知IOC(控制反转)容器关于你的类,它们的依赖项和生命周期。在您的应用程序需要使用IOC容器创建对象时,ASP.NET提供了一些方法解决依赖关系。
构造函数 & 属性注入
作为最佳实践,你可以使用构造函数和属性注入去获取你的类的依赖。任何可能的地方,你都应该这样做。例子:
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!");
}
}
IPersonRepository从构造函数注入,ILogger实例从公共属性注入。这样,您的代码不会体现依赖注入系统。这是使用DI系统最适当的方式。
IIocResolver 和 IIocManager
有时你可能需要直接创建你的依赖项,而不是构造函数和属性注入。应该尽可能避免这种情况,但它可能无法避免。Abp提供一些服务使得这样的注入很容易实现。例子:
public class MySampleClass : ITransientDependency
{
private readonly IIocResolver _iocResolver;
public MySampleClass(IIocResolver iocResolver)
{
_iocResolver = iocResolver;
}
public void DoIt()
{
//Resolving, using and releasing manually
var personService1 = _iocResolver.Resolve<PersonAppService>();
personService1.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" });
_iocResolver.Release(personService1);
//Resolving and using in a safe way
using (var personService2 = _iocResolver.ResolveAsDisposable<PersonAppService>())
{
personService2.Object.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" });
}
}
}
MySampleClass是一个应用程序的示例类。IIcResolver通过构造函数注入,然后用它来创建和释放对象。有几个解决方法的重载可以根据需要使用。Release方法用于释放组件(对象)。如果你是手动创建一个对象,调用Release方法释放对象非常重要。否则,您的应用程序会有内存泄漏问题。为了保证对象被释放,尽可能使用ResolveAsDisposable(就像上面的例子所示)。它会在using代码块结束的时候自动调用Release方法。
如果你想直接使用IOC容器(Castle Windsor)来处理依赖关系项,可以通过构造函数注入 IIocManager并使用它IIocManager.IocContainer 属性。如果你是在一个静态上下文或不能注入IIocManager,还有最后一个方法,你可以使用单例对象IocManager.Instance,你可以在任何地方获取到,它无处不在。但是,在这种情况下你的代码将变得不易容测试。
附加
IShouldInitialize 接口
有些类在第一次使用前需要初始化。IShouldInitialize有Initialize()方法。如果你实现它,那么你的Initialize()方法自动会被自动调用在创建对象之后(在使用之前)。当然,为了使用这个特性,你应该注入/创建此对象。
ASP.NET MVC & ASP.NET Web API 集成
当然,我们必须调用依赖注入系统处理依赖关系图的根对象。在一个ASP.NET MVC应用程序,通常是一个控制器类。我们可以使用构造函数注入模式注入控制器。当一个请求来到我们的应用程序中,控制器和所有依赖项被IOC容器递归创建。所以,谁做了这些?这是被Abp扩展的ASP.NET MVC默认控制器工厂自动完成的。ASP.NET Web API 也是相似的。你不用关心对象的创建和释放。
最后说明
Abp简化并自动使用依赖注入,只要你遵守规则和使用上面的结构。大多数时候这样就够了。但是如果不能满足你的需求,你可以直接使用Castle Windsor的所有能力来执行任何任务(如自定义注册,注入钩子,拦截器等等)。
ABP系列之7、ABP Session管理
简介
如果一个应用程序需要登录,则它必须知道当前用户执行了什么操作。因此ASP.NET在展示层提供了一套自己的SESSION会话对象,而ABP则提供了一个可以在任何地方
获取当前用户和租户的IAbpSession接口。
关于IAbpSession
需要获取会话信息则必须实现IAbpSession接口。虽然你可以用自己的方式去实现它(IAbpSession),但是它在module-zero项目中已经有了完整的实现。
注入Session
IAbpSession通常是以属性注入的方式存在于需要它的类中,不需要获取会话信息的类中则不需要它。如果我们使用属性注入方式,我们可以用
NullAbpSession.Instance作为默认值来初始化它(IAbpSession),如下所示:
复制代码
public class MyClass : ITransientDependency
{
public IAbpSession AbpSession { get; set; }
public MyClass()
{
AbpSession = NullAbpSession.Instance;
}
public void MyMethod()
{
var currentUserId = AbpSession.UserId;
//...
}
}
复制代码
由于授权是应用层的任务,因此我们应该在应用层和应用层的上一层使用IAbpSession(我们不在领域层使用IAbpSession是很正常的)。
ApplicationService, AbpController 和 AbpApiController 这3个基类已经注入了AbpSession属性,因此在Application Service的实例方法中,能直接使用AbpSession属性。
使用Session属性
AbpSession定义的一些关键属性:
UserId: 当前用户的标识ID,如果没有当前用户则为null.如果需要授权访问则它不可能为空。
TenantId: 当前租户的标识ID,如果没有当前租户则为null。
MultiTenancySide: 可能是Host或Tenant。
UserId和TenantId是可以为null的。当然也提供了不为空时获取数据的 GetUserId()和GetTenantId() 方法 。当你确定有当前用户时,你可以使用GetUserId()方法。
如果当前用户为空,使用该方法则会抛出一个异常。GetTenantId()的使用方式和GetUserId()类似。
阳铭注:
ABP框架中的AbpSession, 并没有使用到System.Web.HttpSessionStateBase, 而是自己定义了一个Abp.Runtime.Session.IAbpSession接口, 并在Zero模块中通过AspNet.Identity组件实现了AbpSession对象的存值、取值。 所以即使Web服务重启,也不会丢失Session状态。
在我自己的项目中, Session对象只有UserId、TenantId、MultiTenancySide这几个属性是不够用的,我扩充了几个属性和方法,使用起来非常方便。有这个需求的朋友,欢迎加入QQ群与我交流。
ABP系列之9、ABP设置管理
介绍
每个应用程序需要存储一些设置并在应用程序的某个地方使用这些设置。ABP框架提供强大的基础架构,我们可以在服务端或者客户端设置,来存储/获取应用程序、 租户和用户级别的配置。
设置通常是存储在数据库(或另一个来源)中,用名称-值(name-value)字符串对应的结构来表示。我们可以把非字符串值转换成字符串值来存储。
注意:关于ISettingStore接口
为了使用设置管理必须实现 ISettingStore 接口。你可以用自己的方式实现它,在module-zero项目中有完整的实现可以参考。
定义设置
使用设置之前必须要先定义。ABP框架是模块化设计,所以不同的模块可以有不同的设置。为了定义模块自己的设置,每个模块都应该创建继承自SettingProvider 的派生类。设置提供程序示例如下所示:
复制代码
public class MySettingProvider : SettingProvider
{
public override IEnumerable GetSettingDefinitions(SettingDefinitionProviderContext context)
{
return new[]
{
new SettingDefinition(
“SmtpServerAddress”,
“127.0.0.1”
),
new SettingDefinition(
"PassiveUsersCanNotLogin",
"true",
scopes: SettingScopes.Application | SettingScopes.Tenant
),
new SettingDefinition(
"SiteColorPreference",
"red",
scopes: SettingScopes.User,
isVisibleToClients: true
)
};
}
}
复制代码
GetSettingDefinitions 方法返回 SettingDefinition 对象。SettingDefinition 类的构造函数中有如下参数:
Name (必填):必须具有全系统唯一的名称。比较好的办法是定义字符串常量来设置Name。
Default value: 设置一个默认值。此值可以是null 或空字符串。
Scopes: 定义设置的范围 (见下文)。
Display name: 一个可本地化的字符串,用于以后在UI中显示设置的名称。
Description: 一个可本地化的字符串,用于以后在UI中显示设置的描述。
Group: 可用于设置组。这仅仅是UI使用,不用于设置管理。
IsVisibleToClients: 设置为 true 将使设置在客户端可用。
在创建设置提供程序(SettingProvider)之后,我们应该在预初始化(PreIntialize)方法中注册我们的模块:
Configuration.Settings.Providers.Add();
设置提供程序会自动注册依赖注入。所以,设置提供程序可以注入任何依赖项 (如存储库) 来生成设置定义的一些其它来源。
设置范围
有三个设置范围 (或级别) 在 SettingScopes 枚举中定义:
Application:应用程序范围设置用于用户/租户独立的设置。例如,我们可以定义一个名为"SmtpServerAddress"的设置,当发送电子邮件时,获取服务器的 IP 地址。如果此设置有一个单一的值 (不基于用户改变),那么我们可以定义它为应用程序范围。
Tenant:如果应用程序是多租户的,我们可以定义特定于租户的设置。
User:我们可以使用的用户范围的设置来为每个用户存储/获取设置的值。
SettingScopes 枚举具有Flags属性,所以我们可以定义一个具有多个作用域的设置。
设置范围是分层的。例如,如果我们定义设置范围为"Application | Tenant | User"并尝试获取当前设置的值;
我们获取特定用户的值,如果它定义 (重写) User。
如果没有,我们获取特定的租户值,如果它定义 (重写) Tenant。
如果没有,我们获取应用的值,如果它定义Application。
如果没有,我们得到的默认值。
默认值可以是 null 或空字符串。如果可以,建议为设置提供一个默认值。
获取设置值
定义设置后,我们可以在服务器和客户端获取到它的当前值。
(1)服务器端(Server side)
ISettingManager 用于执行设置操作。我们可以在应用程序中任何地方注入和使用它。ISettingManager 定义了很多获取设置值方法。
最常用的方法是 GetSettingValue (或GetSettingValueAsync 为异步调用)。它将返回当前设置的基于默认值、 应用程序、 租户和用户设置范围的值(如设置范围之前的一段中所述)。例子:
//Getting a boolean value (async call)
var value1 = await SettingManager.GetSettingValueAsync(“PassiveUsersCanNotLogin”);
//Getting a string value (sync call)
var value2 = SettingManager.GetSettingValue(“SmtpServerAddress”);
GetSettingValue 有泛型和异步版本,如上所示。也有方法来获取特定的租户或用户的设置值或所有设置值的列表。
由于ISettingManager使用广泛,一些特定的基类 (如 ApplicationService、 DomainService 和 AbpController) 有一个名为 SettingManager的属性。如果我们从这些类继承,就无需显式地注入它。
(2)客户端
如果定义设置时将 IsVisibleToClients 设置为 true,就可以在客户端使用 javascript得到它的当前值。abp.setting 命名空间定义所需的函数和对象。示例:
var currentColor = abp.setting.get(“SiteColorPreference”);
也有 getInt 和 getBoolean 这样的方法。你可以使用 abp.setting.values 对象获取所有值。请注意,如果你在服务器端更改设置,客户端不会知道这种变化,除非刷新页面或者以某种方式重新加载页面或者通过代码手动更新。
更改设置
ISettingManager 定义了 ChangeSettingForApplicationAsync,ChangeSettingForTenantAsync 和 ChangeSettingForUserAsync 方法(以及同步版本)来更改应用程序,租户和用户分别的设置。
关于缓存
缓存在服务器端设置管理,所以,我们不应直接使用存储库或数据库更新语句改变设置的值。
ABP系列之10、ABP领域层——实体
实体是DDD(领域驱动设计)的核心概念之一。Eric Evans是这样描述的“很多对象不是通过它们的属性定义的,而是通过一连串的连续性事件和标识定义的”(引用领域驱动设计一书)。
译者注:对象不是通过它们的属性来下根本性的定义,而应该是通过它的线性连续性和标识性定义的。。所以,实体是具有唯一标识的ID且存储在数据库中。实体通常被映射成数据库中的一个表。
实体类(Entity classes)
在ABP中,实体继承自Entity类,请看下面示例:
public class Person : Entity
{
public virtual string Name { get; set; }
public virtual DateTime CreationTime { get; set; }
public Task()
{
CreationTime = DateTime.Now;
}
}
Person 类被定义为一个实体。它具有两个属性,它的父类中有Id属性。Id是该实体的主键。所以,Id是所有继承自Entity类的实体的主键(所有实体的主键都是Id字段)。
Id(主键)数据类型可以被更改。默认是int(int32)类型。如果你想给Id定义其它类型,你应该像下面示例一样来声明Id的类型。
public class Person : Entity<long>
{
public virtual string Name { get; set; }
public virtual DateTime CreationTime { get; set; }
public Task()
{
CreationTime = DateTime.Now;
}
}
你可以设置为string,Guid或者其它数据类型。
实体类重写了 equality (==) 操作符用来判断两个实体对象是否相等(两个实体的Id是否相等)。还定义了一个IsTransient()方法来检测实体是否有Id属性。
接口约定
在很多应用程序中,很多实体具有像CreationTime的属性(数据库表也有该字段)用来指示该实体是什么时候被创建的。APB提供了一些有用的接口来实现这些类似的功能。也就是说,为这些实现了这些接口的实体,提供了一个通用的编码方式(通俗的说只要实现指定的接口就能实现指定的功能)。
(1)审计(Auditing)
实体类实现 IHasCreationTime 接口就可以具有CreationTime的属性。当该实体被插入到数据库时, ABP会自动设置该属性的值为当前时间。
public interface IHasCreationTime
{
DateTime CreationTime { get; set; }
}
Person类可以被重写像下面示例一样实现IHasCreationTime 接口:
复制代码
public class Person : Entity<long>, IHasCreationTime
{
public virtual string Name { get; set; }
public virtual DateTime CreationTime { get; set; }
public Task()
{
CreationTime = DateTime.Now;
}
}
ICreationAudited 扩展自 IHasCreationTime 并且该接口具有属性 CreatorUserId :
public interface ICreationAudited : IHasCreationTime
{
long? CreatorUserId { get; set; }
}
当保存一个新的实体时,ABP会自动设置CreatorUserId 的属性值为当前用户的Id
你可以轻松的实现ICreationAudited接口,通过派生自实体类 CreationAuditedEntity (因为该类已经实现了ICreationAudited接口,我们可以直接继承CreationAuditedEntity 类就实现了上述功能)。它有一个实现不同ID数据类型的泛型版本(默认是int),可以为ID(Entity类中的ID)赋予不同的数据类型。
下面是一个为实现类似修改功能的接口
public interface IModificationAudited
{
DateTime? LastModificationTime { get; set; }
long? LastModifierUserId { get; set; }
}
当更新一个实体时,APB会自动设置这些属性的值。你只需要在你的实体类里面实现这些属性。
如果你想实现所有的审计属性,你可以直接扩展 IAudited 接口;示例如下:
public interface IAudited : ICreationAudited, IModificationAudited
{
}
作为一个快速开发方式,你可以直接派生自AuditedEntity 类,不需要再去实现IAudited接口(AuditedEntity 类已经实现了该功能,直接继承该类就可以实现上述功能),AuditedEntity 类有一个实现不同ID数据类型的泛型版本(默认是int),可以为ID(Entity类中的ID)赋予不同的数据类型。
(2)软删除(Soft delete)
软删除是一个通用的模式被用来标记一个已经被删除的实体,而不是实际从数据库中删除记录。例如:你可能不想从数据库中硬删除一条用户记录,因为它被许多其它的表所关联。为了实现软删除的目的我们可以实现该接口 ISoftDelete:
public interface ISoftDelete{
bool IsDeleted { get; set; }
}
ABP实现了开箱即用的软删除模式。当一个实现了软删除的实体正在被被删除,ABP会察觉到这个动作,并且阻止其删除,设置IsDeleted 属性值为true并且更新数据库中的实体。也就是说,被软删除的记录不可以从数据库中检索出,ABP会为我们自动过滤软删除的记录。(例如:Select查询,这里指通过ABP查询,不是通过数据库中的查询分析器查询。)
如果你用了软删除,你有可能也想实现这个功能,就是记录谁删除了这个实体。要实现该功能你可以实现IDeletionAudited 接口,请看下面示例:
public interface IDeletionAudited : ISoftDelete
{
long? DeleterUserId { get; set; }
DateTime? DeletionTime { get; set; }
}
正如你所看到的IDeletionAudited 扩展自 ISoftDelete接口。当一个实体被删除的时候ABP会自动的为这些属性设置值。
如果你想为实体类扩展所有的审计接口(例如:创建(creation),修改(modification)和删除(deletion)),你可以直接实现IFullAudited接口,因为该接口已经继承了这些接口,请看下面示例:
public interface IFullAudited : IAudited, IDeletionAudited
{
}
作为一个快捷方式,你可以直接从FullAuditedEntity 类派生你的实体类,因为该类已经实现了IFullAudited接口。
注意:所有的审计接口和类都有一个泛型模板为了导航定义属性到你的User 实体(例如:ICreationAudited和FullAuditedEntity<TPrimaryKey, TUser>),这里的TUser指的进行创建,修改和删除的用户的实体类的类型,详细请看源代码(Abp.Domain.Entities.Auditing空间下的FullAuditedEntity<TPrimaryKey, TUser>类),TprimaryKey 只的是Entity基类Id类型,默认是int。
(3)激活状态/闲置状态(Active/Passive)
有些实体需要被标记为激活状态或者闲置状态。那么你可以为实体采取active/passive状态的行动。基于这个原因而创建的实体,你可以扩展IPassivable 接口来实现该功能。该接口定义了IsActive 的属性。
如果你首次创建的实体被标记为激活状态,你可以在构造函数设置IsActive属性值为true。
这是不同于软删除(IsDeleted)。如果实体被软删除,它不能从数据库中被检索到(ABP已经过滤了软删除记录)。但是对于激活状态/闲置状态的实体,你完全取决于你怎样去获取这些被标记了的实体。
IEntity接口
事实上Entity 实现了IEntity 接口(和Entity 实现了 IEntity接口)。如果你不想从Entity 类派生,你能直接的实现这些接口。其他实体类也可以实现相应的接口。但是不建议你用这种方式。除非你有一个很好的理由不从Entity 类派生。
ABP系列之11、ABP领域层——仓储(Repositories)
仓储定义:“在领域层和数据映射层的中介,使用类似集合的接口来存取领域对象”(Martin Fowler)。
实际上,仓储被用于领域对象在数据库上的操作(实体Entity和值对象Value types)。
一般来说,我们针对不同的实体(或聚合根Aggregate Root)会创建相对应的仓储。
IRepository接口
在ABP中,仓储类要实现IRepository接口。最好的方式是针对不同仓储对象定义各自不同的接口。
针对Person实体的仓储接口声明的示例如下所示:
public interface IPersonRepository : IRepository<Person>
{
}
- IPersonRepository继承自IRepository,用来定义Id的类型为int(Int32)的实体。
- 如果你的实体Id数据类型不是int,你可以继承IRepository<TEntity, TPrimaryKey>接口,
如下所示:
public interface IPersonRepository : IRepository<Person, long>
{
}
对于仓储类,IRepository定义了许多泛型的方法。比如: Select,Insert,Update,Delete方法(CRUD操作)。在大多数的时候,这些方法已足已应付一般实体的需要。如果这些方对于实体来说已足够,我们便不需要再去创建这个实体所需的仓储接口/类。在Implementation章节有更多细节。
(1)查询(Query)
IRepository定义了从数据库中检索实体的常用方法。
- 取得单一实体(Getting single entity)
TEntity Get(TPrimaryKey id);
Task<TEntity> GetAsync(TPrimaryKey id);
TEntity Single(Expression<Func<TEntity, bool>> predicate);
TEntity FirstOrDefault(TPrimaryKey id);
Task<TEntity> FirstOrDefaultAsync(TPrimaryKey id);
TEntity FirstOrDefault(Expression<Func<TEntity, bool>> predicate);
Task<TEntity> FirstOrDefaultAsync(Expression<Func<TEntity, bool>> predicate);
TEntity Load(TPrimaryKey id);
Get方法被用于根据主键值(Id)取得对应的实体。当数据库中根据主键值找不到相符合的实体时,它会抛出例外。Single方法类似Get方法,但是它的输入参数是一个表达式而不是主键值(Id)。因此,我们可以写Lambda表达式来取得实体。示例如下:
var person = _personRepository.Get(42);
var person = _personRepository.Single(p => o.Name == "Halil ibrahim Kalkan");
注意,Single方法会在给出的条件找不到实体或符合的实体超过一个以上时,都会抛出例外。
FirstOrDefault也一样,但是当没有符合Lambda表达式或Id的实体时,会回传null(取代抛出异常)。当有超过一个以上的实体符合条件,它只会返回第一个实体。
Load并不会从数据库中检索实体,但它会创建延迟执行所需的代理对象。如果你只使用Id属性,实际上并不会检索实体,它只有在你存取想要查询实体的某个属性时才会从数据库中查询实体。当有性能需求的时候,这个方法可以用来替代Get方法。Load方法在NHibernate与ABP的整合中也有实现。如果ORM提供者(Provider)没有实现这个方法,Load方法运行的会和Get方法一样。
ABP有些方法具有异步(Async)版本,可以应用在异步开发模型上(见Async方法相关章节)。
- 取得实体列表(Getting list of entities)
List<TEntity> GetAllList();
Task<List<TEntity>> GetAllListAsync();
List<TEntity> GetAllList(Expression<Func<TEntity, bool>> predicate);
Task<List<TEntity>> GetAllListAsync(Expression<Func<TEntity, bool>> predicate);
IQueryable<TEntity> GetAll();
GetAllList被用于从数据库中检索所有实体。重载并且提供过滤实体的功能,如下:
var allPeople = _personRespository.GetAllList();
var somePeople = _personRepository.GetAllList(person => person.IsActive && person.Age > 42);
GetAll返回IQueryable类型的对象。因此我们可以在调用完这个方法之后进行Linq操作。示例:
//例子一
var query = from person in _personRepository.GetAll()
where person.IsActive
orderby person.Name
select person;
var people = query.ToList();
//例子二
List<Person> personList2 = _personRepository.GetAll()
.Where(p => p.Name.Contains("H"))
.OrderBy(p => p.Name).Skip(40)
.Take(20).ToList();
如果调用GetAll方法,那么几乎所有查询都可以使用Linq完成。甚至可以用它来编写Join表达式。
说明:关于IQueryable
当你调用GetAll这个方法在Repository对象以外的地方,必定会开启数据库连接。这是因为IQueryable允许延迟执行。它会直到你调用ToList方法或在forEach循环上(或是一些存取已查询的对象方法)使用IQueryable时,才会实际执行数据库的查询。因此,当你调用ToList方法时,数据库连接必需是启用状态。我们可以使用ABP所提供的UnitOfWork特性在调用的方法上来实现。
注意,Applicationervice方法预设都已经是UnitOfWork。因此,使用了GetAll方法就不需要如同ApplicationService的方法上添加UnitOfWork特性。
有些方法拥有异步版本,可应用在异步开发模型(见关于async方法章节)。
自定义返回值(Custom return value)
ABP也有一个额外的方法来实现IQueryable的延迟加载效果,而不需要在调用的方法上添加UnitOfWork这个属性卷标。
T Query<T>(Func<IQueryable<Tentity>,T> queryMethod);
查询方法接受Lambda(或一个方法)来接收IQueryable并且返回任何对象类型。示例如下:
var people = _personRepository.Query(q => q.Where(p => p.Name.Contains("H"))
.OrderBy(p => p.Name).ToList());
因为是采用Lambda(或方法)在仓储对象的方法中执行,它会在数据库连接开启之后才被执行。你可以返回实体集合,或一个实体,或一个具部份字段(注: 非Select *)或其它执行查询后的查询结果集。
(2)新增(insert)
IRepository接口定义了简单的方法来提供新增一个实体到数据库:
TEntity Insert(TEntity entity);
Task<TEntity> InsertAsync(TEntity entity);
TPrimaryKey InsertAndGetId(TEntity entity);
Task<TPrimaryKey> InsertAndGetIdAsync(TEntity entity);
TEntity InsertOrUpdate(TEntity entity);
Task<TEntity> InsertOrUpdateAsync(TEntity entity);
TPrimaryKey InsertOrUpdateAndGetId(TEntity entity);
Task<TPrimaryKey> InsertOrUpdateAndGetIdAsync(TEntity entity);
新增方法会新增实体到数据库并且返回相同的已新增实体。InsertAndGetId方法返回新增实体的标识符(Id)。当我们采用自动递增标识符值且需要取得实体的新产生标识符值时非常好用。InsertOfUpdate会新增或更新实体,选择那一种是根据Id是否有值来决定。最后,InsertOrUpdatedAndGetId会在实体被新增或更新后返回Id值。
所有的方法都拥有异步版本可应用在异步开发模型(见关于异步方法章节)
(3)更新(UPDATE)
IRepository定义一个方法来实现更新一个已存在于数据库中的实体。它更新实体并返回相同的实体对象。
TEntity Update(TEntity entity);
Task<TEntity> UpdateAsync(TEntity entity);
(4)删除(Delete)
IRepository定了一些方法来删除已存在数据库中实体。
void Delete(TEntity entity);
Task DeleteAsync(TEntity entity);
void Delete(TPrimaryKey id);
Task DeleteAsync(TPrimaryKey id);
void Delete(Expression<Func<TEntity, bool>> predicate);
Task DeleteAsync(Expression<Func<TEntity, bool>> predicate);
第一个方法接受一个现存的实体,第二个方法接受现存实体的Id。
最后一个方法接受一个条件来删除符合条件的实体。要注意,所有符合predicate表达式的实体会先被检索而后删除。因此,使用上要很小心,这是有可能造成许多问题,假如果有太多实体符合条件。
所有的方法都拥有async版本来应用在异步开发模型(见关于异步方法章节)。
(5)其它方法(others)
IRepository也提供一些方法来取得数据表中实体的数量。
int Count();
Task<int> CountAsync();
int Count(Expression<Func<TEntity, bool>> predicate);
Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate);
Long LongCount();
Task<long> LongCountAsync();
Long LongCount(Expression<Func<TEntity, bool>> predicate);
Task<long> LongCountAsync(Expression<TEntity, bool>> predicate);
所有的方法都拥有async版本被应用在异步开发模型(见关于异步方法章节)。
(6)关于异步方法(About Async methods)
ABP支持异步开发模型。因此,仓储方法拥有Async版本。在这里有一个使用异步模型的application service方法的示例:
public class PersonAppService : AbpWpfDemoAppServiceBase, IPersonAppService
{
private readonly IRepository<Person> _personRepository;
public PersonAppService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public async Task<GetPeopleOutput> GetAllPeople()
{
var people = await _personRepository.GetAllListAsync();
return new GetPeopleOutput
{
People = Mapper.Map<List<PersonDto>>(people)
};
}
}
GetAllPeople方法是异步的并且使用GetAllListAsync与await保留关键字。
Async不是在每个ORM框架都有提供。
上例是从EF所提供的异步能力。如果ORM框架没有提供Async的仓储方法则它会以同步的方式操作。同样地,举例来说,InsertAsync操作起来和EF的新增是一样的,因为EF会直到单元作业(unit of work)完成之后才会写入新实体到数据库中(DbContext.SaveChanges)。
仓储的实现
ABP在设计上是采取不指定特定ORM框架或其它存取数据库技术的方式。只要实现IRepository接口,任何框架都可以使用。
仓储要使用NHibernate或EF来实现都很简单。见实现这些框架在ABP仓储对象上一文:
- NHibernate
- EntityFramework
当你使用NHibernate或EntityFramework,如果提供的方法已足够使用,你就不需要为你的实体创建仓储对象了。我们可以直接注入IRepository(或IRepository<TEntity, TPrimaryKey>)。下面的示例为application service使用仓储对象来新增实体到数据库:
public class PersonAppService : IPersonAppService
{
private readonly IRepository<Person> _personRepository;
public PersonAppService(IRepository<Person> personRepository)
{
_personRepository = personRepository;
}
public void CreatePerson(CreatePersonInput input)
{
person = new Person { Name = input.Name, EmailAddress = input.EmailAddress };
_personRepository.Insert(person);
}
}
PersonAppService的建构子注入了IRepository并且使用其Insert方法。当你有需要为实体创建一个客制的仓储方法,那么你就应该创建一个仓储类给指定的实体。
管理数据库连接
数据库连接的开启和关闭,在仓储方法中,ABP会自动化的进行连接管理。
当仓储方法被调用后,数据库连接会自动开启且启动事务。当仓储方法执行结束并且返回以后,所有的实体变化都会被储存, 事务被提交并且数据库连接被关闭,一切都由ABP自动化的控制。如果仓储方法抛出任何类型的异常,事务会自动地回滚并且数据连接会被关闭。上述所有操作在实现了IRepository接口的仓储类所有公开的方法中都可以被调用。
如果仓储方法调用其它仓储方法(即便是不同仓储的方法),它们共享同一个连接和事务。连接会由仓储方法调用链最上层的那个仓储方法所管理。更多关于数据库管理,详见UnitOfWork文件。
储的生命周期
所有的仓储对象都是暂时性的。这就是说,它们是在有需要的时候才会被创建。ABP大量的使用依赖注入,当仓储类需要被注入的时候,新的类实体会由注入容器会自动地创建。见相根据注入文件有更多信息。
仓储的最佳实践
对于一个T类型的实体,是可以使用IRepository。但别任何情况下都创建定制化的仓储,除非我们真的很需要。预定义仓储方法已经足够应付各种案例。
假如你正创建定制的仓储(可以实现IRepository)
仓储类应该是无状态的。这意味着, 你不该定义仓储等级的状态对象并且仓储方法的调用也不应该影响到其它调用。
当仓储可以使用相根据注入,尽可较少或是不相根据于其它服务。
ABP系列之12、ABP领域层——工作单元(Unit Of work)
https://www.cnblogs.com/mienreal/p/4687781.html