详解ABP框架中的数据过滤器与数据传输对象的使用
数据过滤器(data filters)
在数据库开发中,我们一般会运用软删除(soft-delete)模式,即不直接从数据库删除数据,而是标记这笔数据为已删除。因此,如果实体被软删除了,那么它就应该不会在应用程序中被检索到。要达到这种效果,我们需要在每次检索实体的查询语句上添加sql的where条件isdeleted = false。这是个乏味的工作,但它是个容易被忘掉的事情。因此,我们应该要有个自动的机制来处理这些问题。
abp提供数据过滤器(data filters),它使用自动化的,基于规则的过滤查询。abp已经有一些预定义的过滤器,当然也可以自行创建你专属的过滤器。
注意:只针对entityframework:abp数据过滤器仅实现在entityframework。还无法在其它orm工具中使用。见其它orm章节于本文末端。
预定义过滤器
1.软删除接口(isoftdelete)
软删除过滤器(soft-delete filter )会过滤从数据库查询出来的实体且是自动套用(从结果集中提取出来)。如果实体需要被软删除,它需要实现isoftdelete接口,该接口仅定义了一个isdeleted属性。例:
public class person : entity, isoftdelete { public virtual string name { get; set; } public virtual bool isdeleted { get; set; } }
person实体实际上并没有从数据库中被删除,当删除此实体时,isdeleted属性值会被设定为true。当你使用irepository.delete方法时,abp会自动完成这些工作(你可以手动设定isdeleted为true,但是delete方法更加自然且是较建议的方式)。
当实现了isoftdelete之后,当你已经从数据库中取得了people列表,已被删除的people实体并不会被检索到。在这里有一个示例类,该类使用了person仓储来取得所有的people实体:
public class myservice { private readonly irepository<person> _personrepository; public myservice(irepository<person> personrepository) { _personrepository = personrepository; } public list<person> getpeople() { return _personrepository.getalllist(); } }
getpeople方法仅取得person实体,该实体其isdeleted = false(非删除状态)。所有的仓储方法以及导航属性都能够正常运作。我们可以添加一些其它的where条件,join...等等。它将会自动地添加isdeleted=false条件到生成的sql查询语句中。
注意:何时启用?isoftdelete过滤器总是启用,除非你直接禁用它。
提醒:如果你实现了ideletionaudited接口(该接口继承自isoftdelete),删除创建时间和被删除的用户id,这些都会由abp进行自动的处理。
2.多租接口(imusthavetenant)
如果你创建一个多租户的应用程序(储存所有租户的数据于单一一个数据库中),你肯定不会希望某个租户看到其它租户的资料。你可以实现imusthavetenant接口于此案例中,示例如下:
public class product : imusthavetenant { public virtual int tenantid { get; set; } public virtual string name { get; set; } }
imusthavetenant定义了tenantid来区别不同的租户实体。abp使用iabpsession来取得当前tenantid并且自动地替当前租户进行过滤查询的处理。
注意:何时启用?imusthavetenant默认是启用的。如果当前使用并没有登入到系统或是当前用户是一个管理级使用者(管理级使用者即为一个最高权限的使用者,它可以管理所有租户和租户的资料),abp会自动地禁用imusthavetenant过滤器。因此,所有的租户的数据都可以被应用程序所检索。注意,这与安全性无关,你应该要对敏感数据进行验证授权处理。
3.多租接口(imayhavetenant)
如果一个实体类由多个租户(tenant)以及管理级使用者(host)所共享(这意味着该实体对象或许由租户(tenant)或是管理级使用者(host)所掌控),你可以使用imayhavetenantfilter。imayhavetenant接口定义了tenantid但是它是可空类(nullable)。
public class product : imayhavetenant { public virtual int? tenantid { get; set; } public virtual string name { get; set; } }
当为null值,则代表这是一个管理级使用者(host)所掌控的实体,若为非null值,则代表这个实体是由租户(tenant)所掌控,而其id值即为tenantid。abp使用iabpsession接口来取得当前tenantid。imayhavetenant过滤器并不如imusthavetenant普遍常用。但是当作为管理级使用者(host)和租户(tenant)所需要的通用结构使用时,你或许会需要用到它。
何时启用?imayhavetenant接口总是启用的,除非你直接禁用它。
禁用过滤器
可以在工作单元(unit of work)中调用disablefilter方法来禁用某个过滤器,如下所示:
var people1 = _personrepository.getalllist(); using(_unitofworkmanager.current.disablefilter(abpdatafilters.softdelete)) { var people2 = _personrepository.getalllist(); } var people3 = _personrepository.getalllist();
disablefilter方法取得一或多个过滤器名称,且类型皆为string。abpdatafilters.softdelete是一个常数字符串其包含了abp标准的软删除过滤器。
people2亦可取得已标记为删除的people实体,而people1和people3将会是唯一的非已标记为删除的people实体。若配合使用using语法,你可以禁用其控制范围内(scope)的过滤器。如果你不使用 using 语法 ,此过滤器会被一直禁用,直到工作单元(unit of work)结束或者再度启用它。(意思是:如果你使用"using"关键字声明,过滤器是启用状态;当前工作单元(unit of work)结束后,过滤器是禁止状态。如果不使用"using"关键字声明,默认过滤器是禁用状态,此时可以手动启用过滤器。)
你可以注入iunitofworkmanager并且在上述示例中使用。同样的,你可以使用currentunitofwork属性作为一个在应用服务中的简便方式(它是从applicationservice类继承而来的)。
注意:关于using语法:假如过滤器在你调用disablefilter方法并配合using语法之前已是启用,则过滤器会被禁用,并且会自动地在using语法结束后在度启用。但是若过滤器在using语法之前就已经被禁用了,disablefilter方法实际上并不做任何式,并且过滤器会维持禁用状态即便是using语法的结束后。
启用过滤器
你可以在工作单元(unit of work)中使用enablefilter方法启用过滤器,如同disablefilter方法一般(两者互为正反两面)。enablefilter亦会返回disposable来自动地重新禁用过滤器。
设定过滤器参数
过滤器可以被参数化(parametric)。imusthavetenant过滤器是这类过滤器的一个范本,因为当前租户(tenant)的id是在执行时期决定的。对于这些过滤器,如果真有需要,我们可以改变过滤器的值。举例如下:
currentunitofwork.setfilterparameter("personfilter", "personid", 42);
另一个示例如下:设定imayhavetenant过滤器的tenantid值:
currentunitofwork.setfilterparameter(abpdatafilters.mayhavetenant, abpdatafilters.parameters.tenantid, 42);
欲创建定制的过滤器并且整合到abp中,首先我们需要定义一个接口,该接口将会由使用这个过滤器的实体所实现。假设我们想要自动化地依personid进行过滤,示例如下:
public interface ihasperson { int personid { get; set; } }
然后我们就可以实现这个接口在我们的实体上了,示例如下:
public class phone : entity, ihasperson { [foreignkey("personid")] public virtual person person { get; set; } public virtual int personid { get; set; } public virtual string number { get; set; } }
因为abp使用entityframework.dynamicfilters这个过滤器,我们使用它的规则(rule)来定义过滤器。在我们的dbcontext类中,我们重写了onmodelcreating并且定义了过滤器如下示例所示:
protected override void onmodelcreating(dbmodelbuilder modelbuilder) { base.onmodelcreating(modelbuilder); modelbuilder.filter("personfilter", (ihasperson entity, int personid) => entity.personid == personid, 0 ); }
personfilter过滤器在这里是一个唯一的过滤器名称。再来就是过滤器接口的参数定义和personid过滤器参数(不一定需要,假如过滤器是属于不可参数化(parametric)型),最后一个参数为personid的默认值。
最后一个步骤,我们需要注册这个过滤器到abp工作单元(unit of work)系统中,设定的位置在我们模块里的preinitialize方法。
configuration.unitofwork.registerfilter("personfilter", false);
第一个参数是我们刚刚所定义的唯一名称,第二个参数指示这个过滤器预设是启用还是禁用。在声明完这些可参数化(parametric)的过滤器后,我们可以在执行期间指定它的值来操作这个过滤器。
using(currentunitofwork.enablefilter("personfilter")) { currentunitofwork.setfilterparameter("personfilter", "personid", 42); var phone = _phonerepository.getalllist(); // ... }
我们可以从有些数据源中取得personid而不需要写死在程序代码中。上述示例是为了要能够程序化过滤器。过滤器可拥有0到更多的参数。假如是无参数的过滤器,它就不需要设定过滤器的值。同样地,假如它预设是启用,就不需要手动启用(当然的,我们也可以禁用它)。
entityframework.dynamicfilters的文件:若需要更多关于动态数据过滤器的相关信息,可以见其在git上的文件https://github.com/jcachat/entityframework.dynamicfilters
我们可以为安全性创建一个定制化的过滤器,主/被动实体,多租户...诸如此类的。
其它对象关系映射工具
abp数据过滤器仅实现在entity framework上。对于其它orm工具则尚不可用。但是, 实际上,你可以模仿这个模式到其它使用仓储来取得数据的案例下。这此案例中, 你可以创建一个定制的仓储并且覆写getall方法,如果有需要的话,可以一起修改其它资料检索方法。
数据传输对象(dtos)
数据传输对象(data transfer objects)用于应用层和展现层的数据传输。
展现层传入数据传输对象(dto)调用一个应用服务方法,接着应用服务通过领域对象执行一些特定的业务逻辑并且返回dto给展现层。这样展现层和领域层被完全分离开了。在具有良好分层的应用程序中,展现层不会直接使用领域对象(仓库,实体)。
数据传输对象的作用
为每个应用服务方法创建dto看起来是一项乏味耗时的工作。但如果你正确使用它们,这将会解救你的项目。为啥呢?
1.抽象领域层 (abstraction of domain layer)
在展现层中数据传输对象对领域对象进行了有效的抽象。这样你的层(layers)将被恰当的隔离开来。甚至当你想要完全替换展现层时,你还可以继续使用已经存在的应用层和领域层。反之,你可以重写领域层,修改数据库结构,实体和orm框架,但并不需要对展现层做任何修改,只要你的应用层没有发生改变。
2.数据隐藏 (data hiding)
想象一下,你有一个user实体拥有属性id, name, emailaddress和password。如果userappservice的getallusers()方法的返回值类型为list。这样任何人都可以查看所有人的密码,即使你没有将它打印在屏幕上。这不仅仅是安全问题,这还跟数据隐藏有关。应用服务应只返回展现层所需要的,不多不少刚刚好。
3.序列化 & 惰性加载 (serialization & lazy load problems)
当你将数据(对象)返回给展现层时,数据有可能会被序列化。举个例子,在一个返回json的mvc的action中,你的对象需要被序列化成json并发送给客户端。直接返回实体给展现层将有可能会出现麻烦。
在真实的项目中,实体会引用其他实体。user实体会引用role实体。所以,当你序列化user时,role也将被序列化。而且role还拥有一个list并且permission还引用了permissiongroup等等….你能想象这些对象都将被序列化吗?这有很有可能使整个数据库数据意外的被序列化。那么该如何解决呢?将属性标记为不可序列化?不行,因为你不知道属性何时该被序列化何时不该序列化。所以在这种情况下,返回一个可安全序列化,特别定制的数据传输对象是不错的选择哦。
几乎所有的orm框架都支持惰性加载。只有当你需要加载实体时它才会被加载。比如user类型引用role类型。当你从数据库获取user时,role属性并没有被填充。当你第一次读取role属性时,才会从数据库中加载role。所以,当你返回这样一个实体给展现层时,很容易引起副作用(从数据库中加载)。如果序列化工具读取实体,它将会递归地读取所有属性,这样你的整个数据库都将会被读取。
在展现层中使用实体还会有更多的问题。最佳的方案就是展现层不应该引用任何包含领域层的程序集。
dto 约定 & 验证
abp对数据传输对象提供了强大的支持。它提供了一些相关的(conventional)类型 & 接口并对dto命名和使用约定提供了建议。当你像这里一样使用dto,abp将会自动化一些任务使你更加轻松。
一个例子 (example)
让我们来看一个完整的例子。我们相要编写一个应用服务方法根据name来搜索people并返回people列表。person实体代码如下:
public class person : entity { public virtual string name { get; set; } public virtual string emailaddress { get; set; } public virtual string password { get; set; } }
首先,我们定义一个应用服务接口:
public interface ipersonappservice : iapplicationservice { searchpeopleoutput searchpeople(searchpeopleinput input); }
abp建议命名input/ouput对象类似于methodnameinput/methodnameoutput,对于每个应用服务方法都需要将input和output进行分开定义。甚至你的方法只接收或者返回一个值,也最好创建相应的dto类型。这样,你的代码才会更具有扩展性,你可以添加更多的属性而不需要更改方法的签名,这并不会破坏现有的客户端应用。
当然,方法返回值有可能是void,之后你添加一个返回值并不会破坏现有的应用。如果你的方法不需要任何参数,那么你不需要定义一个input dto。但是创建一个input dto可能是个更好的方案,因为该方法在将来有可能会需要一个参数。当然是否创建这取决于你。 input和output dto类型定义如下:
public class searchpeopleinput : iinputdto { [stringlength(40, minimumlength = 1)] public string searchedname { get; set; } } public class searchpeopleoutput : ioutputdto { public list<persondto> people { get; set; } } public class persondto : entitydto { public string name { get; set; } public string emailaddress { get; set; } }
验证:作为约定,input dto实现iinputdto 接口,output dto实现ioutputdto接口。当你声明iinputdto参数时, 在方法执行前abp将会自动对其进行有效性验证。这类似于asp.net mvc验证机制,但是请注意应用服务并不是一个控制器(controller)。abp对其进行拦截并检查输入。查看dto 验证(dto validation)文档获取更多信息。 entitydto是一个简单具有与实体相同的id属性的简单类型。如果你的实体id不为int型你可以使用它泛型版本。entitydto也实现了idto接口。你可以看到persondto并不包含password属性,因为展现层并不需要它。
跟进一步之前我们先实现ipersonappservice:
public class personappservice : ipersonappservice { private readonly ipersonrepository _personrepository; public personappservice(ipersonrepository personrepository) { _personrepository = personrepository; } public searchpeopleoutput searchpeople(searchpeopleinput input) { //获取实体 var peopleentitylist = _personrepository.getalllist(person => person.name.contains(input.searchedname)); //转换成dto var peopledtolist = peopleentitylist .select(person => new persondto { id = person.id, name = person.name, emailaddress = person.emailaddress }).tolist(); return new searchpeopleoutput { people = peopledtolist }; } }
我们从数据库获取实体,将实体转换成dto并返回output。注意我们没有手动检测input的数据有效性。abp会自动验证它。abp甚至会检查input是否为null,如果为null则会抛出异常。这避免了我们在每个方法中都手动检查数据有效性。
但是你很可能不喜欢手动将person实体转换成persondto。这真的是个乏味的工作。peson实体包含大量属性时更是如此。
dto和实体间的自动映射
还好这里有些工具可以让映射(转换)变得十分简单。automapper就是其中之一。你可以通过nuget把它添加到你的项目中。让我们使用automapper来重写searchpeople方法:
public searchpeopleoutput searchpeople(searchpeopleinput input) { var peopleentitylist = _personrepository.getalllist(person => person.name.contains(input.searchedname)); return new searchpeopleoutput { people = mapper.map<list<persondto>>(peopleentitylist) }; }
这就是全部代码。你可以在实体和dto中添加更多的属性,但是转换代码依然保持不变。在这之前你只需要做一件事:映射
mapper.createmap<person, persondto>();
automapper创建了映射的代码。这样,动态映射就不会成为性能问题。真是快速又方便。automapper根据person实体创建了persondto,并根据命名约定来给persondto的属性赋值。命名约定是可配置的并且很灵活。你也可以自定义映射和使用更多特性,查看automapper的文档获取更多信息。
使用特性(attributes)和扩展方法来映射 (mapping using attributes and extension methods)
abp提供了几种attributes和扩展方法来定义映射。使用它你需要通过nuget将abp.automapper添加到你的项目中。使用automap特性(attribute)可以有两种方式进行映射,一种是使用automapfrom和automapto。另一种是使用mapto扩展方法。定义映射的例子如下:
[automap(typeof(myclass2))] //定义映射(这样有两种方式进行映射) public class myclass1 { public string testprop { get; set; } } public class myclass2 { public string testprop { get; set; } }
接着你可以通过mapto扩展方法来进行映射:
var obj1 = new myclass1 { testprop = "test value" }; var obj2 = obj1.mapto<myclass2>(); //创建了新的myclass2对象,并将obj1.testprop的值赋值给新的myclass2对象的testprop属性。
上面的代码根据myclass1创建了新的myclass2对象。你也可以映射已存在的对象,如下所示:
var obj1 = new myclass1 { testprop = "test value" }; var obj2 = new myclass2(); obj1.mapto(obj2); //根据obj1设置obj2的属性
辅助接口和类型
abp还提供了一些辅助接口,定义了常用的标准化属性。
ilimitedresultrequest定义了maxresultcount属性。所以你可以在你的input dto上实现该接口来限制结果集数量。
ipagedresultrequest扩展了ilimitedresultrequest,它添加了skipcount属性。所以我们在searchpeopleinput实现该接口用来分页:
public class searchpeopleinput : iinputdto, ipagedresultrequest { [stringlength(40, minimumlength = 1)] public string searchedname { get; set; } public int maxresultcount { get; set; } public int skipcount { get; set; } }
对于分页请求,你可以将实现ihastotalcount的output dto作为返回结果。标准化属性帮助我们创建可复用的代码和规范。可在abp.application.services.dto命名空间下查看其他的接口和类型。