数据传输对象
本节内容:
Data Transfer Objects(DTO)用来在应用层和展现层之间传输数据。
展现层使用一个DTO调用一个应用服务方法,然后应用服务使用服务对象执行一些特定业务逻辑,并返回一个DTO给展现层。因此,展现层是完全独立于领域层的。在一个理想的分层应用里,展现层不直接使用领域对象(仓储、实体...)。
首先为每个应用服务方法创建一个DTO看起来是件乏味且费时的工作,但如果你正确使用它,它能解救你的应用。为什么呢?
dto提供一个有效的方法从展现层抽象领域对象,因此,你的层正确分离开,即使你想完全地改变展现层,也可以继续使用已存在的应用和领域层。相反,你可以重写你的领域层、完全改变数据库结构、实体和ORM框架,只要你的应用服务契约(方法签名和DTO)保持不变,展现层也不用做任何修改。
考虑一下:你有一个User实体,它有Id、Name、EmailAddress和Password属性,如果UserAppService的GetAllusers()方法返回一个List<User>,任何人都可以看到所有用户的密码,即使你没有在屏幕上显示它,也是不安全的。不只是数据安全,还有关于数据的隐藏,应用服务应该只向展现层返回必要的数据,不多也不少。
当你返回一个数据(一个对象)给展现层时,它可能会在某处被序列化,例如:在一个返回Json的MVC方法里,你的对象会被序列化成JSON,然后发送给客户端,在这种情况下,如果返回一个实体给展示层可能会有问题,为什么呢?
在一个真实的应用里,你的实体间可能存在相互引用,User实体可能关联到Roles,所以如果你想序列化User,那么它的Roles也要被序列化,而Role类可能包含一个List<Permission>,Permission类可能又关联到PermissionGroup类等等。你能想到序列化这些对象,可能你就意外的序列化了你整个数据库,而如果你的对象存在循环引用,它就不能被序列化了。
怎么解决呢?把属性标记为NonSerialized(不序列化)?不,你不知道它何时应当被序列化又何时不应当被序列化,可能在这个应用服务里要序列化,而在另一个服务里不要序列化,所以返回一个安全地可序列化的,特殊设计的DTO是一个好的选择。
几乎所有ORM框架都支持延迟加载,它是一个在需要时从数据库加载实体的特性。假设User类有一个指向Role类的引用,当你从数据库获取一个User时,Role属性没有被填充,当你第一次读取Role属性时,它再从数据库中加载。所以你返回这么一个实体给展现层,它将去数据库获取额外的实体。如果一个序列化工具读取这个实体,它递归读取所有属性,可能又会序列化你整个数据库(如果实体间存在适当的关系)。
我们可以说出在展现层使用实体的更多问题,最好的做法是在应用层里不引用包含领域(业务)层的程序集。
ABP强支持DTO,它提供了一些约定类和接口,并建议了一些命名和使用约定,当你如本节描述的这样去写代码,ABP会自动完成一些任务。
让我们看一个完整的示例,假设我们想开发一个通过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建议命名输入/输出参数为:MethodNameInput和MethodNameOutput,并为每个应用服务方法定义单独的输入和输入DTO。即使你的方法只接受/返回一个参数,也最好是创建一个DTO类,因为你的代码将来可能需要扩展,你可以稍后添加更多属性,而不必修改你方法的签名也不用打断你已存在的客户端应用。
当然,如果你的方法没有返回值,也就是void,如果你在以后添加一个返回值,它也不会打断已存在的应用。如果你的方法没有参数,你不需要定义一个输入DTO,但如果将来可能会添加参数,最好先添加一个输入DTO类,这取决于你。
让我们看一下这个例子的输入和输出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会自动验证输入,这类似于Asp.net Mvc的验证,但请注意:应用服务不是一个控制器,它就是一个单纯的C#类,ABP拦截它并自动检查输入。有很多的验证,请查阅DTO 验证文档。
EntityDto是一个实体通用只定义Id属性的简单类,如果你有实体主键不是int,有一个泛型版本可以用。你可以不用EntityDto,但最好定义一个Id属性。
PersonDto如你所见,不包含Password属性,因为展现层不需要它,并且发送所有用户的密码给展现层也是危险的,想象一下:一个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 }; } }
我们从数据库获取实体,把它们转换成DTO再返回给输出,注意:我们没有验证输入,ABP验证了它,它甚至验证了输入参数是否为空,为空时抛出异常,这就省得我们在每个方法里写验证代码。
但是你可能不喜欢写把一个Person实体转换成PersonDto对象的代码,它是确实是一个乏味的工作,Person实体可能包含很多属性。
幸运地是:有工具使这件事变得容易,AutoMapper是其中之一,它发布在nuget上,你可以很容易地把它加入到你的项目里。让我们使用AutoMap再写一下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,并按命名约定给DTO的属性赋值。命名约定可以很复杂和配置,同样,你也可以定义自己的配置及更多内容。更多信息查询AutoMapper的文档。
你可在你的模块里的PostInitialzie里定义映射。
ABP提供了多个特性和扩展方法用来定义映射,为使用它,先在你的项目里添加Abp.AutoMapper的nuget包,然后使用AutoMap特性进行双向映射,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
上面的代码从一个MyClass1对象创建一个新的MyClass2对象,同样,你也可以映射一个已存在的对象,如下所示:
var obj1 = new MyClass1 { TestProp = "Test value" }; var obj2 = new MyClass2(); obj1.MapTo(obj2); //从obj1设置obj2的属性
ABP提供一些辅助接口,在被实现时,标准化通用DTO属性名。
ILimitedResultRequest定义了MaxResultCount属性,所以你可以在你的输入DTO类里实现它,用来标准化限制结果集。
IPagedResultRequst扩展了ILimitedResultRequest,添加了SkipCount。所以我们可以为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的输出DTO。命名标准化帮助我们创建可重用的代码和约定。在Abp.Application.Services.Dto命名空间下查看其它接口和类。