数据传输对象
数据传输对象用来在应用层和展示层之间传输数据。
展示层调用应用服务方法并传递一个数据传输对象(DTO),然后应用服务使用领域对象执行一些特定的业务逻辑并返回给展示层一个DTO。因此,展示层完全与领域层隔离。在一个理想的分层应用中,展示层不直接使用领域对象(仓储、实体...)。
最初,为每一个应用服务方法创建DTO看起来乏味且耗时,但是如果你正确使用它,他们会拯救你的应用。为什么这么说呢?
DTOs提供了一种从展示层抽象领域对象的有效方式。因此,你的分层是正确分离的。即使你想完全改变展示层,你可以继续使用已有的应用和领域层。相反,你可以重写领域层,完全改变数据模式、实体和ORM框架而展示层不用做任何改变,只要你应用服务的约定(方法签名和DTOs)保持不变。
假如你有一个user实体,它有属性Id,Name,EmailAddress和Password。如果UserAppService的GetAllUsers()方法返回一个List<User>,任何人都可以看到所有用户的密码,即使你不在屏幕上显示。这不是关于安全,而是数据隐藏。应用服务应该给展示层返回它需要的。不是更多而是更少。
当你返回一个数据(对象)到展示层时,它可能会在某处被序列化。例如,在一个返回JSON的MVC方法里,你的对象被序列化为JSON并发送到客户端。在这种情况下,返回实体到展示层是有问题的。如何?
在一个真实应用里,你的实体将会互相引用。User实体引用Roles。所以,如果你想序列化User,它的Role是也被序列化。甚至Role类或许有一个List<Permission> ,Permission类可能引用PermissionGroup类等等...你能想象所有这些对象都被序列化吗?你可能意外的序列化了整个数据库。如果你的对象存在循环引用,它就不能被序列化。
解决方案是什么呢?标记属性为不可序列化?不,你不知道什么时候它被序列化什么时候不被序列化。它可能在一个应用服务方法里需要序列化,在其他却不需要。所以,在这种情况下返回一个安全的可序列化的、特定设计的DTOs是一个好的选择。
几乎所有的ORM框架支持懒加载。当需要的时候从数据库加载实体。比如说User类有一个Role类的引用。当你从数据库里获取一个User时,Role属性没有填充。当你第一次读Role属性时,它才从数据库加载。所以,如果你返回给展示层一个实体,它将导致从数据库中提取额外的实体。如果一个序列化工具读取这个实体,它递归读取所有属性,你的整个数据库再一次被提取了(如果实体之间有恰当的关系的话)。
我们可以说更多关于在展示层使用实体的问题。最好不要引用包含领域(业务)层到展示层的程序集。
ABP强支持DTOs。它提供了关于DTOs的一些约定类和接口并建议了一些命名和使用约定。当你编写如这里描述的代码时,ABP简单的自动处理一些任务。
让我们看一个完整的示例。比如说我们想开发一个应用服务方法,用来查询people,提供一个name参数并返回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建议以MethodNameInput和MethodNameOutput的形式命名input/output参数,并为每个应用服务方法定义一个分离的input和outputDTO。即使你的方法只接收或返回衣蛾参数,最好也创建一个DTO类。因此,你的代码将更易扩展。你可以以后添加更多的属性而不用更改你的方法的签名并且不会破坏已有的客户端应用。
当然,如果没有返回值的话,你的方法可以返回void。如果你以后添加一个返回值,它不会破坏已有的应用。如果你的方法不接受任何参数,你就不需要定义一个input DTO。但是,如果将来有可能添加参数,最好还是写一个input DTO类。由你来决定。
让我们来看看这个示例定义的input和output DTO类:
public class SearchPeopleInput { [StringLength(40, MinimumLength = 1)] public string SearchedName { get; set; } } public class SearchPeopleOutput { public List<PersonDto> People { get; set; } } public class PersonDto : EntityDto { public string Name { get; set; } public string EmailAddress { get; set; } }
ABP在方法执行前自动校验input。和ASP.NET MVC的验证类似,但是注意应用服务不是一个控制器,它是一个普通的C#类。ABP自动拦截并检查input。这有更多关于验证的,参见DTO validation文档。
EntityDto是一个简单的类,它声明了Id属性,因为它对所有的实体都通用。如果你的实体主键不是int,有一个泛型版本。你不用必须使用它,但是建议定义一个Id属性。
PersonDto如你所见不包含Password属性,因为对展示层来说是不需要的。甚至发送所有people的密码到展示层是危险的。想想一个JavaScript客户端请求它,任何人都很容易的抓取到所有的密码。
在进行下一步之前我们先实现IPersonAppService:
public class PersonAppService : IPersonAppService { private readonly IPersonRepository _personRepository; public PersonAppService(IPersonRepository personRepository) { _personRepository = personRepository; } public SearchPeopleOutput SearchPeople(SearchPeopleInput input) { //Get entities var peopleEntityList = _personRepository.GetAllList(person => person.Name.Contains(input.SearchedName)); //Convert to DTOs var peopleDtoList = peopleEntityList .Select(person => new PersonDto { Id = person.Id, Name = person.Name, EmailAddress = person.EmailAddress }).ToList(); return new SearchPeopleOutput { People = peopleDtoList }; } }
我们从数据库获取实体,把他们转换成DTOs并返回output。注意,我们不校验input,ABP校验它。它甚至检查input参数是否为null,如果是便抛出异常。这使得我们不用再每个方法里编写检查语句。
但是,可能你不喜欢从一个Person实体到一个PersonDto对象的转换代码。它确实是一个乏味的工作。Person实体有更多的属性。
幸运的是有工具可以使得这个非常简单。AutoMapper就是其中之一。参见AutoMapper集成文档了解如何使用它。
ABP提供了一些帮助接口,可以用来实现以标准化通用DTO的属性名称。
ILimitedResultRequest定义了MaxResultCount属性。所以,你可以在你的input DTOs里实现它来标准化限制结果集。
IPageResultRequest通过添加SkipCount扩展了ILimitedResultRequest。所以,我们可以在SearchPeopleInput分页里实现这个接口:
public class SearchPeopleInput : 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命名空间下的其他接口和类。