解析ABP框架中的数据传输对象与应用服务
数据传输对象(dtos)
数据传输对象(data transfer objects)用于应用层和展现层的数据传输。
展现层传入数据传输对象(dto)调用一个应用服务方法,接着应用服务通过领域对象执行一些特定的业务逻辑并且返回dto给展现层。这样展现层和领域层被完全分离开了。在具有良好分层的应用程序中,展现层不会直接使用领域对象(仓库,实体)。
1.数据传输对象的作用:
为每个应用服务方法创建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。所以,当你返回这样一个实体给展现层时,很容易引起副作用(从数据库中加载)。如果序列化工具读取实体,它将会递归地读取所有属性,这样你的整个数据库都将会被读取。
在展现层中使用实体还会有更多的问题。最佳的方案就是展现层不应该引用任何包含领域层的程序集。
2.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实体包含大量属性时更是如此。
3.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的文档获取更多信息。
4.使用特性(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的属性
5.辅助接口和类型
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命名空间下查看其他的接口和类型。
应用服务
应用服务用于将领域(业务)逻辑暴露给展现层。展现层通过传入dto(数据传输对象)参数来调用应用服务,而应用服务通过领域对象来执行相应的业务逻辑并且将dto返回给展现层。因此,展现层和领域层将被完全隔离开来。在一个理想的层级项目中,展现层应该从不直接访问领域对象。
1.iapplicationservice接口
在abp中,一个应用服务需要实现iapplicationservice接口。最好的实践是针对每个应用服务都创建相应的接口。所以,我们首先定义一个应用服务接口,如下所示:
public interface ipersonappservice : iapplicationservice { void createperson(createpersoninput input); }
ipersonappservice只有一个方法,它将被展现层调用来创建一个新的person。createpersoninput是一个dto对象,如下所示:
public class createpersoninput : iinputdto { [required] public string name { get; set; } public string emailaddress { get; set; } }接着,我们实现ipersonappservice接口:
public class personappservice : ipersonappservice { private readonly irepository<person> _personrepository; public personappservice(irepository<person> personrepository) { _personrepository = personrepository; } public void createperson(createpersoninput input) { var person = _personrepository.firstordefault(p => p.emailaddress == input.emailaddress); if (person != null) { throw new userfriendlyexception("there is already a person with given email address"); } person = new person { name = input.name, emailaddress = input.emailaddress }; _personrepository.insert(person); } }
以下是几个重要提示:
- personappservice通过irepository来执行数据库操作。它通过构造器注入模式来生成。我们在这里使用了依赖注入。
- personappservice实现了iapplicationservice(通过ipersonappservice继承iapplicationservice)。abp会自动地把它注册到依赖注入系统中,并可以注入到别的类型中使用。
- createperson方法需要一个createpersoninput类型的参数。这是一个作为输入的dto,它将被abp自动验证其数据有效性。可以查看dto和数据有效性验证(validation)文档获取相关细节。
2.应用服务类型
应用服务(application services)需要实现iapplicationservice接口。当然,你可以选择将你的应用服务(application services)继承自applicationservice基类,这样你的应用服务也就自然而然的实现iapplicationservice接口了。applicationservice基类提供了方便的日志记录和本地化功能。在此建议你针对你的应用程序创建一个应用服务基类继承自applicationservice类型。这样你就可以添加一些公共的功能来提供给你的所有应用服务使用。一个应用服务示例如下所示:
public class taskappservice : applicationservice, itaskappservice { public taskappservice() { localizationsourcename = "simpletasksystem"; } public void createtask(createtaskinput input) { //记录日志,logger定义在applicationservice中 logger.info("creating a new task with description: " + input.description); //获取本地化文本(l是localizationhelper.getstring(...)的简便版本, 定义在 applicationservice类型) var text = l("samplelocalizabletextkey"); //todo: add new task to database... } }
本例中我们在构造函数中定义了localizationsourcename,但你可以在基类中定义它,这样你就不需要在每个具体的应用服务中定义它。查看日志记录(logging)和本地化(localization)文档可以获取更多的相关信息。
上一篇: Photoshop打造燃烧的红色蜡烛
下一篇: MySQL启用慢查询日志记录方法