ABP理论学习之数据传输对象(DTO)

返回总目录

本篇目录

  • 为何需要DTO
    • 领域层抽象
    • 数据隐藏
    • 序列化和懒加载问题
  • DTO惯例和验证
  • DTO和实体的自动映射
    • 使用特性和扩展方法进行映射
  • 帮助接口

DTO用于应用层展现层间的数据传输。

展现层调用具有DTO参数的应用服务方法,然后应用服务使用领域对象来执行一些特定的业务逻辑,最后返回给展现层一个DTO。因此,展现层完全独立于领域层。在一个理想的分层应用中,展现层不直接和领域对象打交道(仓储,实体...)。

为何需要DTO

为每个应用服务方法创建一个DTO起初可能被看作是一项乏味而又耗时的事情。但如果正确地使用它,那么DTOs可能会拯救你应用。为啥呢?

领域层抽象

DTO为展现层抽象领域对象提供了一种有效方式。这样,层与层之间就正确分离了。即使你想完全分离展现层,仍然可以使用已存在的应用层和领域层。相反,只要领域服务的契约(方法签名和DTOs)保持不变,即使重写领域层,完全改变数据库模式,实体和ORM框架,也不需要在展现层做任何改变。

数据隐藏

试想你有一个User实体,包含Id,Name,EmailAddress和Password字段。如果UserAppService的GetAllUsers()方法返回一个List ,即使你没有在屏幕上显示它,那么任何人也都能看到所有user的密码。它不是涉及安全的,而是与数据隐藏相关的。应用服务都应该返回给展现层需要的,不要更多,也不很少,要的是恰到好处。

序列化和懒加载问题

当返回给展现层一个对象时,它很可能在某个地方序列化。比如,一个MVC方法返回JSON,一个对象会被序列化成JSON,然后发送到客户端。在那种情况,将一个实体返回到展现层是有问题的。这是怎么回事呢?

在一个真实应用中,实体之间是相互引用的。User实体可能有一个Role的引用。因此,如果你想序列化User,那么Role也会序列化。而且,如果Role有一个List 且Permission类有一个PermissionGroup类的引用等等。你能想象所有的对象都会被序列化的那种场景吗?你可能会意外地序列化整个数据库。那么解决方案是什么呢?把属性标记为NonSerilized吗?不,你可能不知道它何时应该序列化,何时不应该。它可能在一个应用方法中需要,可能在另一个就不需要了。因此,在这种情景中,设计一个可安全序列化的,特别设计的DTOs是一种好的选择。

几乎所有的ORM框架都支持懒加载。它的特征是当需要时才从数据库中加载实体。假如说User类有一个Role类的引用。当从数据库中获得一个User时,此时Role属性还没有填充,当第一次读该Role属性时,它才从数据库中加载。因此,不要将这样的一个实体直接返回给展现层,它可能会轻易造成从数据库检索额外的实体。如果序列化工具读到了该实体,它会递归地读取所有属性,最终整个数据库可能会被检索(如果实体间有合适的关系)。

在展现层使用实体还会有更多的问题。最好压根不要在将包含领域(业务)层的程序集引用到展现层上。

DTO惯例和验证

ABP高度支持DTOs,它提供了一些符合惯例的类和接口,并且对于DTO的命名和用法提出了一些建议。当按照下面描述的那样编写代码时,ABP会轻易地自动处理一些事情。

举个例子

让我们看一个完整的例子。假如我们想要开发一个应用服务方法,作用是使用一个名字来搜索人,并返回一个人的集合。这种情况下,我们可能会有一个如下的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/output参数命名为MethodNameInput和 MethodNameOutput,并为每个应用服务方法定义一个单独的input和output DTO。即使你的方法只需要或返回一个参数,最好也创建一个DTO类。这样,你的代码回更具有扩展性。以后你可以添加更多的属性而不用改变方法的签名,而且也不用使已存在的客户端应用发生重大变化。

当然,如果你的方法没有返回值,那么方法可以返回void。如果以后添加了一个返回值,也不会打破已存在的应用。如果你的方法不需要任何参数,那么你也不必定义一个输入DTO。但是如果未来很可能添加参数,那么也许最好还是编写一个输入DTO。这取决于你。

让我们看一下为这个例子定义的输入和输出的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; }
}

验证:按照惯例,输入DTO实现了 IInputDto接口,输出DTO实现了 IOutputDto接口。当实现了IInputDto时,ABP会在方法执行前自动验证输入。这和ASP.NET MVC的验证很相似,但是注意应用服务不是控制器,它是纯粹的C#类。ABP使用拦截来自动检查输入。关于更多的验证,请看下篇DTO验证。

EntityDto是一个声明了Id属性的简单类。因为这对于所有的实体都是公用的。如果你的实体的主键不是int的,那么还有一个泛型版本。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 };
    }
}

我们从数据库中获得实体,再将它们转成DTOs,然后返回到输出。注意我们没有验证输入,因为ABP会自动验证。ABP甚至会检查输入参数是否为null,如果为null,就会抛出异常。

但是很可能你不喜欢从一个Person实体到一个PersonDto对象的转换代码。这是相当无聊的事情,而且,Person实体可能会有更多的属性。

DTO和实体的自动映射

幸好,我们有工具可以让这个变得很简单。AutoMapper就是之一(要学习AutoMapper,请看我的AutoMapper系列教程。它已经发布到Nuget上了,你可以轻松地将它添加到项目中。让我们再次写一下SearchPeople方法,但是这次是用AutoMapper:

public SearchPeopleOutput SearchPeople(SearchPeopleInput input)
{
    var peopleEntityList = _personRepository.GetAllList(person => person.Name.Contains(input.SearchedName));
    return new SearchPeopleOutput { People = Mapper.Map<List<PersonDto>>(peopleEntityList) };
}

这样就ok了。你可以给实体和DTO添加更多的属性而不需要转换代码做任何改变。唯一要做的事情就是在使用前定义一个映射:

Mapper.CreateMap<Person, PersonDto>();

AutoMapper创建了映射代码。这样,动态映射就不会成为性能问题了。它既快速又容易。AutoMapper为Person实体创建了PersonDto,并使用命名规范赋予DTO属性。命名规范可能是复杂的且可配置的。此外,你还可以定义自定义映射以及更多。

使用特性和扩展方法进行映射

ABP提供了若干特性和扩展方法来定义映射。首先,要将Abp.AutoMapper nuget包添加到项目中。然后,AutoMap特性是双向映射方式, AutoMapFromAutoMapTo是单向映射方式。最后,使用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>(); //从obj1的副本创建一个新的MyClass2对象

上面的代码从MyClass1的对象创建了MyClass2一个新的对象。此外,你可以像下面那样,映射到一个已存在的对象:

var obj1 = new MyClass1 { TestProp = "Test value" };
var obj2 = new MyClass2();
obj1.MapTo(obj2);

帮助接口

ASP.NET 提供了一些实现标准化公共DTO属性名称的帮助接口。

ILimitedResultRequest定义了 MaxResultCount属性。这样你就可以在你的输入DTO中实现它来标准化有限的结果集。

IPagedResultRequest通过添加了 SkipCount扩展了 ILimitedResultRequest。这样,我们可以在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的输出DTO。命名标准化帮助我们创建可重复使用的代码和惯例。你也可以在 Abp.Application.Services.Dto命名空间下看到其他的接口和类。

你可能感兴趣的:(ABP理论学习之数据传输对象(DTO))