ABP理论学习之开篇介绍
介绍
我们总是基于不同的需求创建不同的应用,但是在一定程度上,总在反复地实现通用而相似的结构。这些通用的结构包括授权,验证,异常处理,日志,本地化,数据库连接管理,设置管理,审计日志等。而且,我们总是在构建体系结构和最佳实践,比如分层和模块化架构,领域驱动设计(DDD),依赖注入等等。同时也在尝试基于惯例开发应用。
因为这些都是非常耗时的,并且对于每个项目单独创建是很困难的,所以很多公司都会创建自己私有的框架。通过使用私有的框架,他们总是可以快速地开发新的应用,同时应用的bug又会更少。当然了,不是所有的公司都是那么幸运了,你以为中国所有的公司都是BAT啊?!大多数公司还是没有时间,预算和团队来开发他们自己的私人框架。即使他们有可能构建这么一个框架,写文档,培训开发者以及维护也是很难的。
ABP是一个开源的且文档友好的应用框架,起始的想法是,“开发一款为所有公司和开发者通用的框架!”。它不仅仅是一个框架,更提供了一个基于DDD和最佳实践的健壮的体系模型。
下面让我们总结一下ABP的一些优点:
- 依赖注入:ABP使用并提供了一个健壮而又传统的DI基础设施。因为上面的类是在一个应用服务中定义的,所以它会按照惯例约定短暂地(每个请求创建一次)注册到DI容器中。它也简单地注入了所有依赖(本例中注入了IRepository)。
- 仓储:ABP可以为每一个实体创建一个默认的仓储(本例中是IRepository)。默认的仓储有许多有用的方法,如本例中的 FirstOrDefault。我们也可以根据我们的需求轻易地扩展默认仓储。仓储抽象了DBMS和ORM,并简化了数据的访问逻辑。
- 授权:ABP可以检测权限。它使用声明式的特性简化了授权,而且还有其他的授权方法。
- 验证:ABP会自动检测输入是否为null。它也基于标准的数据注解特性和自定义的验证规则验证输入对象的所有属性。如果请求不合法,那么它会抛出一个合适的验证异常。
- 审计日志:用户,浏览器,IP地址,调用服务,方法,参数,调用时间,执行时长和其他的一些信息也会基于惯例和配置为每个请求自动地保存。
- 工作单元(Unit of Work):在ABP中,每个应用服务方法默认视为一个工作单元。它会自动创建一个连接并在方法的开始位置开启一个事务。如果方法不报异常地成功完成了,那么事务会提交并且连接被释放。即使该方法使用了不同的仓储或者方法,它们全部也都是原子的(事务的)。当事务提交时,实体的所有改变都会自动保存。
- 异常处理:在一个使用了ABP框架的Web应用中,我们基本上不用处理异常。所有的异常都会默认自动处理。如果一个异常发生了,那么ABP会自动地记录它,然后返回给客户端一个合适的结果。比如,如果这是一个Ajax请求,那么它会返回一个JSON到客户端,指明发生了一个错误。本例中使用了一个UserFriendlyException,这样就隐藏了客户端实际的异常信息。它也理解并处理客户端的错误,最后将合适的信息呈现给用户。
- 日志:我们可以使用在基类中定义的Logger来写日志。ABP默认使用了Log4Net,但是它是可改变的或可配置的。
- 本地化(Localization):注意当抛出异常的时候我们使用了L方法。因此,它会基于当前用户的文化自动进行本地化。当然,我们可以在某些地方定义CouldNotFoundTheTaskMessage。
- 自动映射:上面的最后一行代码,我们使用了ABP的MapTo扩展方法将输入对象的属性映射到实体属性。它使用了AutoMapper库来执行映射。因此,我们可以基于命名惯例轻易地将属性从一个对象上映射到另一个对象上。
- 动态Web API层:实际上,TaskAppService 是一个简单的类(甚至不需要从ApplicationService 继承)。我们一般会写一个Web API Controller包装器来将方法暴露给javascript客户端。ABP在运行时会自动完成。这样,我们可以从客户端直接使用应用服务方法。
- 动态Ajax代理:ABP创建了javascript代理方法,它们可以调用应用服务方法就像调用客户端的javascript方法一样简单。
其他
ABP也提供了一个健壮的基础设施和应用模型。下面是ABP的一些其他特征:
- 模块化:提供了一个健壮的基础设施来生成可复用的模块。
- 数据过滤器:提供了自动的数据过滤来实现一些模式,比如软删除和多租户。
- 多租户:支持单数据库、多客户形式的多租户。
- 设置管理:提供了健壮的基础设施类获得或者更改应用,租户和用户级别的设置。
- 单元测试和集成测试:基于可测试性构建,也提供了一些基类来简化单元测试和集成测试。
启动模板
开始一个新的解决方案,创建层,安装nuget包,创建一个简单的布局和菜单...所有的这些都是非常耗时的。
ABP提供了一个预生成的启动模板,有了它,创建一个新的解决方案更容易了。模板支持SPA(单页应用)和MPA(多页应用)。而且,我们可以选择不同的ORM。
如何使用
ABP的源码已经推送到了Github上,Nuget包也已经发布到了Nuget上。开始使用ABP最简单的方式就是使用ABP官网的模板创建项目,然后跟着文档来学习。
ABP理论学习之N层架构
介绍
对应用代码基进行分层可以帮助降低复杂度和提高代码复用性,这已经成为广为接受的技巧。为了实现分层的架构,ABP遵循以下DDD(领域驱动模型)的原则。在DDD中,有四个基础层:
- 表现层:用户访问的接口,使用应用层实现用户交互。
- 应用层:表现层和领域层之间的媒介。负责组织业务对象,以执行特定的应用任务。
- 领域层:包括业务对象和原则。这是应用的核心。
- 基础设施层:为支持更高层提供了广泛的技术能力。基础设施层的一个例子是仓储,它可以通过ORM框架和数据库进行交互。
也可能会加入额外必要的层。以下就是一个例子:
- 分布式服务层:将应用的功能暴露给远程客户端。可以提供这个层的工具包括Asp.Net Web API和WCF。
这些都是以领域为中心的架构的通用层次。基于实现的话,可能还会稍有不同。
ABP架构
层次和结构的概览如下图所示:
实际解决方案中的项目分层如下:
一层可以为一个或多个程序集。对于第三方依赖创建不止一个程序集可能会特别好(比如NHibernate)。
领域层是所有的业务规则实现的地方。
实体代表了业务领域的数据和操作。在实践中,它们一般会映射到数据库的表中。
仓储是跟集合很像的对象,使用仓储可以检索数据源(数据库)上的实体,并将实体持久化到数据源上。领域层只是定义了仓储,但是并没有实现它们,它们是在基础设施层实现的。
领域事件定义了领域特定的事件,也包括触发和处理这些事件。领域服务借助实体运行,并实现了不属于单个实体的业务规则。
工作单元是一种管理实体的设计模式,这些实体受业务逻辑影响,并将状态持久化到数据存储中。
领域层应该尽可能地独立于第三方的库。
应用层包含了展现层使用的应用服务。应用服务方法接收一个DTO(数据传输对象)作为输入参数,使用这个输入对象执行一些特定的领域层操作,然后,如果需要的话,可能返回另一个DTO。一般而言,应用服务方法不接收或者返回一个实体对象,这样做的好处就是可以允许展现层可以从领域层中将实体抽象出来,而不受实体的约束。一个应用服务方法一般被看作是一个工作单元。用户输入验证也是在这一层实现的。ABP提供了一个基础设施,因此可以很容易地实现验证。建议使用一个将实体映射为DTO的工具,比如AutoMapper,TinyMapper等。
虽然领域层定义了仓储接口,但是基础设施层使用诸如NHibernate或者EntityFramework的ORM工具实现了那些仓储接口。ABP提供了使用这两种ORM框架的基类。基础设施层用于抽象来自其他层的第三方类库,数据库迁移(Database Migration)也可以用在这一层。
Web层使用ASP.NET MVC和Web API实现的。这里可以使用两种不同的方式来实现:单页面应用和多页面应用。
在单页面应用中(SPA),所有的资源都会一次性加载到客户端(或者只加载核心资源,懒加载其他资源),所有的后续和服务器的交互都是通过Ajax调用。Html代码是使用从服务端接收到的数据在客户端生成的。整个页面不会刷新,视图只是在必要时换入换出。有许多的Javascript SPA框架,比如AngularJs,DurandalJs,BackboneJs和EmberJs。ABP可以使用它们中的任何一个,但是提供了使用 AngularJs和DurandalJs的样例。
在多页面(经典)应用中(MPA),客户端向服务端发送请求,服务端代码(ASP.NET MVC 控制器)从数据库中获取数据,然后Razor视图引擎生成html 代码。这些编译后的页面发回给客户端显示。每个新的页面都会导致完整页面的刷新。
SPA和MPA涉及了完全不同的架构。对于后台管理系统来说,SPA是最好的候选者,另一方面,博客更适合MPA模型,因为博客渴望被搜索引擎抓取数据。虽然有很多工具可以使SPA对于搜索引擎可见,但是目前的一般做法就是使用MPA。
SignalR是发送服务端到客户端的推送通知的最好工具,也可以给用户提供一个丰富而且实时的体验。
在客户端还有很多javascript库和框架。jQuery是这个领域最流行的,并伴有成千上万的插件。也有很多很容易就能使用Html和CSS的框架或工具。比如,Twitter Bootstrap是非常流行的HTML/CSS框架。
ABP提供了使用应用服务层自动创建Web API层的基础设施,使用Javascript可以轻松地调用Web API。而且,还提供了管理管理应用菜单,本地化以及语言切换的基础设施,还包含了统一的Javascript API来简化显示系统的信息和通知。
ABP自动处理服务端的异常,并给客户端返回一个合适的响应。
ABP通过Castle Windsor使用并支持依赖注入,也使用了Log4Net来记录服务端的日志,然而,通过使用Castle的日志设备无需改变代码就可以切换到其他的日志库。
总结
ABP平衡了一些最好的框架或者类库,除此之外,ABP自己的类和系统也提供了一个很好的用于N层架构Web应用构建的基础设施,也提供了很轻松地创建分层的解决方案的模板,用作应用的起点。
ABP理论学习之模块系统
模块介绍
ABP提供了构建模块并将这些模块组合起来创建应用的基础设施。一个模块可以依赖另一个模块。一般来说,一个程序集可以认为是一个模块。一个模块是由一个派生了AbpModule的类定义的。比如说我们在开发一个可以用在不同的应用中的博客模块。最简单的模块定义如下:
public class MyBlogApplicationModule : AbpModule
{
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
ABP扫描所有的程序集,并找出所有的派生自AbpModule基类的类。如果你创建了不止一个程序集的应用,那么建议为每个程序集创建一个模块定义。
生命周期事件
ABP在应用启动和关闭的时候会调用一些特定的模块方法。你可以重写这些方法来执行特定的任务。
ABP按照依赖的顺序调用这些方法。如果模块A依赖于模块B,那么模块B在模块A之前初始化。方法执行的正确顺序是:PreInitialize-B, PreInitialize-A, Initialize-B, Initialize-A, PostInitialize-B and PostInitialize-A。这对于所有的依赖图都是成立的。Shutdown方法也是类似的,但顺序相反。
PreInitialize
该方法会在应用启动时首先调用。你可以在该方法里面写一些特定的代码,这些代码会在依赖注入注册之前执行。比如,如果你创建了一个传统的注册类,那么你应该把它在这里注册(使用locManager.AddConventionalRegister方法)。你也可以注册到IOC容器的事件...等等。
Initialize
该方法通常是依赖注入注册的地方。一般使用IocManager.RegisterAssemblyByConvention方法完成。如果你想要定义自定义的依赖注册,请看后面的依赖注入文档。
PostInitialize
该方法在应用启动的最后调用。在这里可以安全地解析一个依赖。
Shutdown
该方法在应用关闭的时候调用。
模块依赖
一个模块可以独立于另一个模块。ABP会自动地尝试解析这些依赖,但是建议通过重写GetDependencies方法来显式声明依赖,正如下面那样:
[DependsOn(typeof(MyBlogCoreModule))]
public class MyBlogApplicationModule : AbpModule
{
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
这样,我们声明ABP:MyBlogApplicationModule依赖于MyBlogCoreModule,因而该核心模块MyBlogCoreModule会在应用模块MyBlogApplicationModule之前进行初始化。
自定义模块方法
你的模块也可以有一些自定义的方法,这些方法可以被依赖于该模块的其他模块所使用。假设MyModule2依赖于MyModule1,且MyModule2想要在PreInitialize事件中调用一个MyModule1的方法。
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(); //Call MyModule1's method
}
public override void Initialize()
{
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
}
}
这里,我们通过构造函数将MyModule1z注入到了MyModule2,因此MyModule2可以调用MyModule1的自定义方法。
ABP理论学习之启动配置
为了在应用启动时配置ABP和模块,ABP提供了一个基础设施。
配置ABP
配置ABP是在模块的PreInitialize事件中完成的。下面的代码摘自Github上的Taskever(一个任务系统Demo):
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());
}
}
ABP是基于模块化设计的。不同的模块都可以配置ABP。比如,不同的模块可以通过添加导航提供者来给主菜单添加自己的菜单选项。
配置模块
除了ABP框架自身的启动配置之外,模块也可以实现IAbpModuleConfigurations接口来为模块提供配置点。比如:
...
using Abp.Web.Configuration;
...
public override void PreInitialize()
{
Configuration.Modules.AbpWeb().SendAllExceptionsToClients = true;
}
...
在本例中,我们配置了AbpWeb模块,目的是将所有的异常发送到客户端。
并不是每一个模块都应该定义这种类型的配置。一般是当一个模块需要在不同的应用中重用的时候,才需要在启动时进行配置。
为模块创建配置
假设我们有一个叫做“MyModule”的模块,它有一些配置属性。首先,我们为这些配置属性创建一个类MyModuleConfig:
public class MyModuleConfig
{
public bool SampleConfig1 { get; set; }
public string SampleConfig2 { get; set; }
}
然后,我们在MyModule的PreInitialize事件中将这个类MyModuleConfig注册到DI中(这样,MyModuleConfig就成为了可注册的类):
IocManager.Register<MyModuleConfig>();
最后,我们创建IModuleConfiguration的扩展方法来获得MyModuleConfig的引用。
public static class MyModuleConfigurationExtensions
{
public static MyModuleConfig MyModule(this IModuleConfigurations moduleConfigurations)
{
return moduleConfigurations.AbpConfiguration
.GetOrCreate("MyModuleConfig",
() => moduleConfigurations.AbpConfiguration.IocManager.Resolve<MyModuleConfig>()
);
}
}
现在,在应用启动时,其他的模块就可以配置这个模块了(在需要配置MyModule的模块的PreInitialize事件中):
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理论学习之多租户
什么是多租户
维基百科:“软件多租户是指一种软件架构,在这种软件架构中,软件的一个实例运行在服务器上并且为多个租户服务”。一个租户是一组共享该软件实例特定权限的用户。有了多租户架构,软件应用被设计成为每个租户提供一个 专用的实例包括该实例的数据的共享,还可以共享配置,用户管理,租户自己的功能和非功能属性。多租户和多实例架构相比,多租户分离了代表不同的租户操作的多个实例。
多租户用于创建Saas(Software as-a service)应用(云处理)。有几种类型的多租户:
多部署-多数据库
这实际上不是多租户。但是,如果我们为每个具有分开数据库的客户(租户)运行该应用的一个实例,那么我们可以在单个服务器上为多个租户提供服务。我们可以确定该应用的多个实例在相同的服务器环境不会相互冲突。
这个对于一个不是为多租户设计的已存在应用也是可能的。创建这么一个应用更容易,因为该应用不需要了解多租户。但这种方式存在安装,使用和维护问题。
单部署-多数据库
在这种情况下,我们可以在一个服务器上运行应用的单个实例。对于每个登录用户,我们从master database中检测该用户的租户,并获得该租户的数据库信息(连接字符串)。然后我们可以将连接字符串存储到像session一样的变量中,同时,使用这个租户特定的连接字符串执行所有的数据库操作。
某种程度上,这样的应用应该设计成多租户。但是大多数的应用都独立于多租户。这种方式也存在一些安装,使用和维护问题。我们应该为每个租户创建并维护一个分离的数据库。
单部署-单数据库
这是最真实的多租户架构:我们只将具有单个数据库应用的单个实例部署到单个服务器上。在(RDBMS)每个表中,都存在一个TenantId(或相似)字段,该字段用于分离每个租户之间的数据。
这种方法安装和维护都很简单,但唯独创建这么一个应用很难,因为我们必须要阻止一个租户读取或写入其他租户的数据。我们可以为每个数据库的读取(select)操作添加一个TenantId过滤器。而且,我们可以在每次写入的时候检查一下该实体是否和当前的租户相关。这是乏味而易于出错的,但ABP通过使用自动的数据过滤帮助我们处理这个事情。
如果我们有很多具有大量数据的租户,那么这种方法可能会有性能问题。我们可以使用关系型数据库的表分割特征或者将租户按组分到不同的服务器上。
ABP中的多租户
ABP提供了创建单部署,单数据库,多租户架构的基础设施。
开启多租户
多租户默认是关闭的。我们可以在模块的PreInitialize方法中开启,如下所示:
Configuration.MultiTenancy.IsEnabled = true;
租主vs租户
首先,我们应该定义多租户系统中的两个条目:
- 租主(Host):租主是单例的(只有一个租主)。租主会对创建和管理租户负责。因此,一个“租主用户”比所有的租户等级更高,并独立于所有租户,同时还能控制他们。
- 租户(Tenant):租主的一个客户,具有自己的用户角色,权限,设置等。每个租户都可以完全独立于其他租户使用应用。一个多租户应用会有一个或多个租户。如果是一个CRM应用,那么不同的租户也有它们自己的账户,契约,产品和订单。因此,当我们说“**租户用户”的时候,意思就是一个租户拥有的用户。
Session
ABP定义了一个获取当前用户和租户id的IAbpSession接口。该接口用于多租户获取当前的租户id。因此,它可以基于当前的租户id过滤数据。ABP中有以下规则:
- 如果UserId和TenantId都是null,那么当前的用户没有登录到系统。因此,我们可以不知道当前用户是否是一个租主用户还是一个租户用户。在这种情况下,用户不能访问授权的内容。
- 如果UserId不是null,TenantId是null,那么当前用户是一个租主用户。
- 如果UserId不是null,TenantId也不是null,那么当前用户是租户用户。
更多关于session的信息请看后面的Session一节。
数据过滤器
当从数据库中检索实体时,我们必须添加一个TenantId过滤器来只获得当前的租户实体。当你为实体实现了IMustHaveTenant和IMayHaveTenant两个接口之一时,ABP会自动地完成数据过滤。
IMustHaveTenant接口
该接口通过定义TenantId属性来区分不同租户的实体。一个实现了IMustHaveTenant的实体例子如下:
public class Product : Entity, IMustHaveTenant
{
public int TenantId { get; set; }
public string Name { get; set; }
//...其他属性
}
这样,ABP知道这是一个特定租户的实体,并且会自动地将一个租户的实体从其他实体中分离出来。
IMayHaveTenant接口
我们可能需要在租户和租户之间共享一个实体类型。因此,一个实体可能会被一个租户或租主拥有。IMayHaveTenant接口也定义了TenantId(类似于IMustHaveTenant),但在这种情况下是nullable。实现了IMayHaveTenant的一个实体例子:
public class Role : Entity, IMayHaveTenant
{
public int? TenantId { get; set; }
public string RoleName { get; set; }
//...其他属性
}
我们可能会使用相同的Role类来存储租主角色和租户角色。这种情况下,TenantId表明这是一个租户实体还是一个租主实体。null值表示这是一个租主实体,非null值表示这被一个租户拥有,该租户的Id是TenantId。
IMayHaveTenant不像IMustHaveTenant一样常用。比如,一个Product类可以不实现IMayHaveTenant接口,因为Product和实际的应用功能相关,和管理租户不相干。因此,要小心使用IMayHaveTenant接口,因为它更难维护租户和租主共享的代码。
保存实体
一个租户用户不应该创建或编辑其他租户的实体。如果相关的数据过滤器开启了,那么ABP会检查该实体相对于数据库的改变。
想要获得更多关于数据过滤器的信息,请看后面关于数据过滤器的博客。
ABP理论学习之OWIN集成
如果你的应用中使用了OWIN,那么需要在主项目(一般来说是指Web项目)中添加Abp.Owin的nuget包,然后像下面那样在OWIN的 Startup文件中调用 UseAbp()扩展方法:
[assembly: OwinStartup(typeof(Startup))]
public class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseAbp();
//your other configuration...
}
}
ABP框架理论学习之Debugging
所有的官方ABP nuget包都是支持GitLink的,这意味着你可以在项目中轻松地调试所有的以Abp为前缀的Nuget包。
要开启这项支持,“启用源服务器支持”选项应该勾选,“启用仅我的代码”选项应该取消勾选,中文截图如下:
英文版截图如下:
如上设置之后,就可以通过F11步入到ABP的源代码了,快去试试吧。
ABP理论学习之依赖注入
什么是依赖注入
维基百科说:“依赖注入是一种软件设计模式,在这种模式下,一个或更多的依赖(或服务)被注入(或者通过引用传递)到一个独立的对象(或客户端)中,然后成为了该客户端状态的一部分。该模式分离了客户端依赖本身行为的创建,这使得程序设计变得松耦合,并遵循了依赖反转和单一职责原则。与服务定位器模式形成直接对比的是,它允许客户端了解客户端如何使用该系统找到依赖”。
不使用依赖注入技巧来管理依赖,并开发一个模块化的,结构友好的应用是非常困难的。
在一个应用中,类相互依赖。假设我们有个应用服务,该应用服务使用了仓储将实体插入数据库。在这种情况下,此应用服务类依赖于仓储类。看下面这个例子:
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在CreatePerson方法中使用了IPersonRepository的引用,因此该方法依赖于IPersonRepository,而不是具体的PersonRepository类。但是在PersonAppService的构造函数中仍旧依赖于PersonRepository。而组件应该依赖于接口而不是实现,这就是依赖反转原则。
- 如果PersonAppService创建了PersonRepository本身,那么它会依赖于IPersonRepository接口的一个具体实现,这样就造成可能不会和其他实现一起工作。因此,从实现中分离接口就会变得毫无意义。硬依赖使得代码基变得紧耦合,可复用性降低。
- 在未来我们可能需要改变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的实现,而且它独立于PersonRepository的实现。但是,仍然有下面这些问题:
- 这次,PersonAppService依赖于PersonRepositoryFactory。这个较为可接受,但是仍然有硬依赖。
- 为每个仓储或者依赖写一个工厂类或方法太繁琐了。
- 还是不太好测试,因为让PersonAppService使用一些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个依赖,那么我们必须创建这4个依赖的对象,然后把它们传入PersonAppService的构造函数中。
- 依赖的类可能有其它的依赖(这里,PersonRepository可能有依赖)。因此,我们必须创建PersonAppService的所有依赖,依赖的所有依赖等等。这样的话,我们甚至可能不再创建单一对象,因为依赖图太复杂了。
幸运的是,ABP有依赖注入框架自动管理依赖。
属性注入模式
构造函数注入是提供一个类的依赖的完美模式。用这种方式,你可以不需要提供依赖就能创建一个类的实例,它也是显示声明该类需要满足什么要求才能正确工作的强大方式。
但在某些情况下,该类依赖于其他的类而且其他的类没有它也能工作。这对于关注度分离(比如日志记录)来说经常是成立的。一个类可以离开logging工作,但如果提供了logger,那它就能记录日志。这种情况下,你可以定义将依赖定义为公共的属性而不是在构造函数中获得这些依赖。试想如果我们要在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对象之后,并像下面那样设置了Logger,PersonAppService就可以记录日志了:
var personService = new PersonAppService(new PersonRepository());
personService.Logger = new Log4NetLogger();
personService.CreatePerson("Yunus Emre", 19);
假设Log4NetLogger实现了ILogger并使用Log4Net类库记录日志。这样,PersonAppService实际上就可以记录日志了。如果没有设置Logger,那么它就不会记录日志。因此,我们可以说ILogger是PersonAppService的一个可选依赖。
几乎所有的依赖注入框架都支持属性注入模式。
有很多自动解析依赖的依赖注入框架。它们能够使用所有的依赖(包括依赖的依赖)创建对象。因此,你只需要使用构造和属性注入模式编写你的类,DI框架会处理剩下的事情。在一个优秀的应用中,你的类甚至独立于DI框架。在整个应用中,有许多显式和DI框架交互的代码行或者类。
ABP使用Castle Windsor框架处理依赖注入。它是最成熟的DI框架之一。还有很多其他的框架,如Unity,Ninject,StructureMap,Autofac等等。
在依赖注入框架中,你首先要将你的接口或者类注册到其中,然后才可以解析(创建)一个对象。在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。这意味着,无论何时解析这些类型的一个对象,都会创建一个新的实例。当然还有很多不同的生命周期(像singleton)。
ABP中的依赖注入基础设施
当你通过下面的最佳实践和一些惯例编写你的应用时,ABP几乎让使用DI框架变得不可见了。
在ABP中,将你的类注册到DI系统有几种不同的方式。大多数情况下,按照惯例注册已经足够了。
惯例注册
ABP会按照惯例自动注册所有的仓储,领域服务,应用服务,MVC控制器和Web API控制器。比如,你可能有一个IPersonAppService接口和一个实现了该接口的PersonAppService类:
public interface IPersonAppService : IApplicationService
{
//...
}
public class PersonAppService : IPersonAppService
{
//...
}
因为它实现了IApplicationService接口(只是一个空接口),所以ABP会自动注册它,并注册为transient(每次使用创建一个实例)。当你使用构造函数注入IPersonAppService接口到一个类中时,一个PersonAppService对象会自动地创建并传入该类的构造函数中。
命名规范在ABP中非常重要。比如,你可以将PersonAppService更名为MyPersonAppService或是其他包含了“PersonAppService”后缀的名字,因为IPersonAppService接口有这个后缀。但你不能将它命名为PeopleService。如果你没有按照这种命名规范来操作的话,那么IPersonAppService不会自动地注册(但是它已经以自注册的方式注入到DI框架,而不是接口方式),因此如果你想要以接口方式注册的话,那么你应该手动注册。
ABP按照惯例注册程序集。因此,你应该按照惯例告诉ABP注册你的程序集。这个相当简单:
IocManager.RegisterAssemblyByConvention(Assembly.GetExecutingAssembly());
Assembly.GetExecutingAssembly()会获得包含这句代码的程序集的引用。你也可以将其他的程序集传入RegisterAssemblyByConvention 方法中。这个操作通常在你的模块初始化的时候完成的。查看《模块系统》博文获得更多信息。
通过实现IConventionalRegister接口和调用IocManager.AddConventionalRegister方法,你可以用你的类编写你自己的惯例注册类。你要做的就是在模块的PreInitialize方法中加入它。
帮助接口
你可能想要注册一个特殊的类,但是它不符合惯例注册的原则。为此,ABP提供了ITransientDependency 和 ISingletonDependency接口。比如:
public interface IPersonManager
{
//...
}
public class MyPersonManager : IPersonManager, ISingletonDependency
{
//...
}
用这种方式,你可以轻松地注册MyPersonManager。当需要注入IPersonManager的时候,就会使用MyPersonManager。注意依赖声明为Singleton。这样,MyPersonManager的单例就被创建了,并且相同的对象也被传入到所有的类中。只有在第一次使用时才会创建,以后再整个应用的生命周期都会使用相同的实例。
自定义/直接注册
如果之前描述的方法还不能满足你,那么你可以直接使用Castle Windsor来注册你的类和依赖。这样,你就在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。
注册会将你的类,类的依赖和生命周期通知给IOC(控制反转)容器。接下来,你需要在应用中的某些地方使用IOC容器创建对象。ABP针对依赖的解析提供了很多选项。
构造函数&属性注入
你可以将使用构造函数和属性注入获得类的依赖作为最佳实践。无论在哪里,你都应该这样做。例如:
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()
{
//手动解析
var personService1 = _iocResolver.Resolve<PersonAppService>();
personService1.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" });
_iocResolver.Release(personService1);
//安全地解析并使用
using (var personService2 = _iocResolver.ResolveAsDisposable<PersonAppService>())
{
personService2.Object.CreatePerson(new CreatePersonInput { Name = "Yunus", Surname = "Emre" });
}
}
}
在以上例子中的MySampleClass通过构造函数注入IIocResolver并用它来解析和释放对象。Resolve方法有许多重载可供使用。Release方法用来释放组件(对象)。调用Release来手动解析一个对象是很关键的,否则,应用会有内存泄漏问题。为了确保释放对象,要尽可能使用ResolveAsDisPosable(如例子中演示的那样)。在using块的末尾会自动地调用Release。
如果你想要直接使用IOC容器(Castle Windor)来解析依赖,那么你可以构造函数注入IIocManager并使用IIocManager.IocContainer属性。如果你处于静态上下文或者不能注入IIocManager,那么最后的机会就是,你可以使用单例对象IocManager.Instance。但是,这种情况不容易测试。
IShouldInitialize接口
某些类在第一次使用前就要初始化。IShouldInitialize接口有一个Initialize方法。如果实现了该接口,那么在创建对象之后(使用前)就会自动地调用Initialize方法。当然,为了使该功能有效,你应该注入/解析该对象。
当然,为了解析依赖图中的根对象,我们必须调用依赖注入系统。在ASP.NET MVC应用中,根对象一般是一个Controller类。我们也可以在控制器中使用构造函数注入和属性注入模式。当一个请求到达应用时,IOC容器创建了控制器对象,然后所有的依赖递归地解析出来。那么,谁处理的这个呢?这是ABP通过扩展了ASP.NET MVC默认的控制器工厂自动完成的。相似地,对于ASP.Net Web API也是如此。你不必关心创建和释放对象的事情。
只要你遵循规则并使用上面的结构,ABP就能简化并自动化依赖注入的使用。大多数情况下,这些已经够用了。但是,如果你需要的话,你可以直接使用所有Castle Windsor的能力来执行任何任务(如自定义注册,注入钩子,拦截器等等)。
ABP理论学习之Abp Session
介绍
当应用程序要求用户登录时,那么应用程序也需要知道当前用户正在执行的操作。虽然ASP.NET本身在展现层提供了Session对象,但ABP也提供了在任何需要获得当前用户和租户的地方都可以使用的IAbpSession接口。
关于IAbpSession
为了获得实际的session信息,必须要实现IAbpSession接口。虽然你可以用自己的方式实现它,但在module-zero项目中已经完全实现。
IAbpSession已经完全集成到ABP(实例的设置系统和授权系统)中。
注入Session
IAbpSession一般属性注入到需要的类中,除非没有Session信息导致IAbpSession不可能工作。如果我们使用了属性注入,那么我们可以使用NullAbpSession.Instance作为默认值,如下所示:
public class MyClass : ITransientDependency
{
public IAbpSession AbpSession { get; set; }
public MyClass()
{
AbpSession = NullAbpSession.Instance;
}
public void MyMethod()
{
var currentUserId = AbpSession.UserId;
//...
}
}
因为授权是应用层的事儿,所以建议在应用层和更高的层使用IAbpSession(一般我们不再领域层使用)。ApplicationService,AbpController,AbpApiController已经注入了 AbpSession。因此,你可以直接在应用层服务方法中为实例使用AbpSession属性。
使用Session属性
AbpSession定义了一些key属性:
- UserId:当前用户的Id。值为null,表示当前的用户不存在。如果调用的代码授权给某个用户的话,那么值不为null。
- TenantId:当前租户的Id。如果当前的租户不存在,值就为null。
- MultiTenancySide:可能是Host(租主)或者Tenant(租户)。
UserId和TenantId是nullable(可空的),也存在不可空的GetUserId()和 GetTenantId()方法。如果你确定当前的用户存在,那么你可以调用GetUserId()。如果当前的用户为null,那么该方法就会抛异常。GetTanantId()也是类似的。
ABP理论学习之缓存Caching
介绍
ABP提供了缓存的抽象,它内部使用了这个缓存抽象。虽然默认的实现使用了MemoryCache,但是也可以为其他的缓存提供者进行实现和改变。
ICacheManager
缓存的主要接口是ICacheManager。我们可以注入该接口,然后使用该接口获得一个缓存对象。例如:
public class TestAppService : ApplicationService
{
private readonly ICacheManager _cacheManager;
public TestAppService(ICacheManager cacheManager)
{
_cacheManager = cacheManager;
}
public Item GetItem(int id)
{
//从缓存中获取
return _cacheManager
.GetCache("MyCache")
.Get(id.ToString(), () => GetFromDatabase(id)) as Item;
}
public Item GetFromDatabase(int id)
{
//... 从数据库中检索
}
}
在这个例子中,我们注入了ICacheManager,并获取了一个叫做MyCache的缓存对象。
警告:GetCache方法
不要在构造函数中使用GetCache方法。如果你的类是transient(每次使用都会创建)的,那么这可能会释放缓存,因为第二次创建类的对象时,会再次调用构造函数,之前的第一次的缓存可能会被释放。
ICache
ICacheManager.GetCache方法返回一个ICache。缓存对象是单例的,第一次请求时会创建缓存,以后都是返回相同的缓存对象。因此,我们可以在不同的类(客户端)中共享具有相同名字的相同缓存。
在样例代码中,我们看到了ICache.Get方法的简单使用。它有两个参数:
- key:缓存中一个条目的唯一字符串键。
- 工厂:没有找到给定key的缓存条目时调用的action。工厂方法应该创建并返回实际的条目。如果给定的key在缓存中找到了,那么不会调用该action。
ICache接口也有像GetOrDefault,Set,Remove,Clear的方法。同时,这些方法也有异步(async)版本。
ITypedCache
ICache接口的key为string类型,value为object类型。ITypeCache是ICache的包装器,提供类型安全、泛型的cache。为了将ICache转为ITypedCache,我们可以使用AsTyped扩展方法,如下所示:
ITypedCache<int, Item> myCache = _cacheManager.GetCache("MyCache").AsTyped<int, Item>();
这样,我们不需要转换就可以使用Get方法。
配置
默认的缓存有效期是60min。因此,如果你在60min内都没有使用缓存中的元素,那么它会自动从缓存中移除。对于所有的缓存或者特定的某个缓存,你都可以配置有效期。
//为所有缓存配置有效期
Configuration.Caching.ConfigureAll(cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromHours(2);
});
//为特定的缓存配置有效期
Configuration.Caching.Configure("MyCache", cache =>
{
cache.DefaultSlidingExpireTime = TimeSpan.FromHours(8);
});
这些代码应该放到模块中的PreInitialize方法中。有了这样的配置,MyCache会有8小时的有效期,而其他cache会有2小时有效期。
一旦cache首次创建(第一次请求时),就会调用配置的action。配置并不只局限于DefaultSlidingExpireTime(默认滚动有效期),因为cache对象是一个ICache,你可以使用它的属性和方法自由地配置并初始化。
ABP理论学习之日志记录
服务端
ABP使用的是Castle Windsor的日志记录设备。它可以和不同的日志类库一起工作,比如Log4Net,NLog,Serilog等等。Castle为所有的日志类库提供了一个公共的接口。因此,你完全独立于特定的日志记录类库,而且,如果需要的话,你可以轻松地改变应用程序的日志类库。
Log4Net是.Net中最流行的日志类库之一。ABP模板中自带了经过合适配置的Log4Net。但是,只存在一行log4net的依赖(看下面),因此,你可以将它改为你最喜欢的类库。
无论你选择了什么日志类库,最终要记录的日志代码都是相同的(这得感谢Castle公共的ILogger接口)。
一开始,我们要处理一下记录日志的Logger对象。因为ABP强烈推荐使用依赖注入,所以我们可以使用属性注入模式轻松地注入一个Logger对象。如下所示:
using Castle.Core.Logging; //1: 导入 Logging 命名空间
public class TaskAppService : ITaskAppService
{
//2: 使用属性注入获得 logger
public ILogger Logger { get; set; }
public TaskAppService()
{
//3: 如果没有提供Logger,就不能记录日志
Logger = NullLogger.Instance;
}
public void CreateTask(CreateTaskInput input)
{
//4: 记录日志
Logger.Info("Creating a new task with description: " + input.Description);
//TODO: 保存到数据库...
}
}
- 导入Castle的ILogger接口的命名空间。
- 定义一个公有的叫做Logger的ILogger对象。这是记录日志的对象。创建TaskAppService对象之后,依赖注入系统会设置(注入)这个属性。这就是所谓的属性注入模式。
- 将Logger设置为NullLogger.Instance。即使没有这行代码,系统也会工作地很好。但是这是属性注入模式的最佳实践。如果没给Logger设置任何值,那么当我们使用它的时候会因为它是null而抛出“空指针”异常。这个保证了它不为null。因此,如果没有给Logger设置值,那么它是NullLogger。这就是所谓的null对象模式。NullLogger实际上什么都没做,也没有记录任何日志。因此,我们的类要不要一个实际的logger都能工作。
- 最后,我们记录了一个info等级的日志文本。存在多种不同的等级(看下面)。
如果我们调用了CreateTask方法,并检查日志文件,就会看到像下面一样的一长行字符串。
INFO 2014-07-13 13:40:23,360 [8 ] SimpleTaskSystem.Tasks.TaskAppService - Creating a new task with description: Remember to drink milk before sleeping!
ABP为MVC控制器,Web API控制器和应用服务类提供了基类。比如,Web层对应的基类是XXXControllerBase(后缀为ControllerBase)。这些基类中都声明了Logger属性。因此你可以直接使用Logger来记录日志,无需注入。例子:
public class HomeController : SimpleTaskSystemControllerBase
{
public ActionResult Index()
{
Logger.Debug("A sample log message...");
return View();
}
}
注意,SimpleTaskSystemControllerBase是继承了AbpController的应用基类控制器。因此,可以在控制器中直接使用Logger。Logger也存在于Web Api控制器的AbpApiController基类,以及应用服务层的基类(后缀为AppServiceBase的类)。你也可以为其他的类编写自己的公共基类,这样,你就不需要每次注入logger了。
当你从ABP模板创建应用程序时,Log4Net的所有配置已经完成了。
默认的日志格式配置如下:
- 日志级别:DEBUG, INFO, WARN, ERROR或者FATAL。
- 日期和时间:记录日志的时间。
- 线程号:记录日志的线程号码。
- Logger名称:通常是记录日志的类名。
- 日志文本:实际记录的日志文本。
配置定义在应用的log4net.config文件中,如下所示:
<?xml version="1.0" encoding="utf-8" ?>
<log4net>
<appender name="RollingFileAppender" type="log4net.Appender.RollingFileAppender" >
<file value="Logs/Logs.txt" />
<appendToFile value="true" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="10000KB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%-5level %date [%-5.5thread] %-40.40logger - %message%newline" />
</layout>
</appender>
<root>
<appender-ref ref="RollingFileAppender" />
<level value="DEBUG" />
</root>
<logger name="NHibernate">
<level value="WARN" />
</logger>
</log4net>
Log4Net是高度可配置的、健壮的日志记录类库。你可以使用不同的格式将日志记录到不同的目标上(文本文件,数据库等)。你也可以设置最小日志等级(正如此配置中为NHibernate配置的那样)。你也可以记录不同的日志到不同的文件中。当到达一个指定的大小时,它会自动备份并创建一个新的日志文件等等(本例中,滚动文件适配器的每个文件大小是10MB)。
最后,我们在Global.asax文件中,声明了要使用log4net.config文件中的Log4Net。
public class MvcApplication : AbpWebApplication
{
protected override void Application_Start(object sender, EventArgs e)
{
IocManager.Instance.IocContainer.AddFacility<LoggingFacility>(f => f.UseLog4Net().WithConfig("log4net.config"));
base.Application_Start(sender, e);
}
}
这是我们直接依赖log4net的唯一代码行。而且,只有web项目依赖log4net类库的nuget包。因此,你可以轻松地切换到其他日志类库,而且不需要改变记录日志的代码。
客户端
ABP为客户端定义了一个javascript日志记录API。默认会将日志记录到浏览器的控制台。记录日志的javascript代码样例如下:
abp.log.warn('a sample log message...');