数据传输对象

数据传输对象用于在应用层表示层之间传输数据 

表示层使用数据传输对象(DTO调用应用程序服务方法然后,应用程序服务使用这些域对象来执行某些特定的业务逻辑,然后最终将DTO返回给表示层。因此,表示层与域层完全隔离。在理想的分层应用程序中,表示层永远不会与域对象(存储库或 实体 ...)一起使用。

对DTO的需求

首先,为每个Application Service方法创建一个DTO类可以被视为繁琐且耗时的工作。但是,如果您正确使用它们,它们可以保存您的应用程序。为什么?

域层的抽象

DTO提供了一种从表示层抽象域对象的有效方法。实际上,您的图层是正确分隔的。如果要完全更改表示层,可以继续使用现有的应用程序和域图层。或者,您可以重新编写域层,完全更改数据库架构,实体和O / RM框架,而无需更改表示层。当然,只要您的应用程序服务的合同(方法签名和DTO)保持不变即可。

数据隐藏

假设您有一个具有Id,Name,EmailAddress和Password属性的User实体。如果UserAppService的GetAllUsers()方法返回List <User>,则任何人都可以看到所有用户的密码,即使您没有在屏幕上显示它。这不仅仅是关于安全性,而是关于数据隐藏。应用程序服务应该返回表示层所需的内容。不多也不少。

序列化和延迟加载问题

将数据(对象)返回到表示层时,它很可能是序列化的。例如,在返回JSON的MVC方法中,您的对象可以序列化为JSON并发送到客户端。在这方面,将实体返回到表示层可能是有问题的。怎么样?

在实际应用程序中,您的实体将相互引用。用户实体可以引用它的角色。如果要序列化用户,则其角色也会序列化。Role类可以有List <Permission>,Permission类可以引用PermissionGroup类等等......想象一下所有这些对象是一次序列化的。您可以轻松地,不小心地序列化整个数据库!此外,如果您的对象具有循环引用,则无法序列化它们

解决方案是什么?将属性标记为NonSerialized?不,你不知道什么时候应该序列化,什么时候不应该序列化。在一种应用服务方法中可能需要它,而在其他应用服务方法中则不需要。在这种情况下,返回安全,可序列化和专门设计的DTO是一个不错的选择。

几乎所有的O / RM框架都支持延迟加载。它是一种在需要时从数据库加载实体的功能。假设User类具有对Role类的引用。从数据库获取用户时,不会填充Role属性。第一次读取Role属性时,它是从数据库加载的。因此,如果将此类实体返回到表示层,则会使其从数据库中检索其他实体。如果序列化工具读取实体,它会递归读取所有属性,并且可以再次检索整个数据库(如果实体之间存在合适的相关属性)。

如果在表示层中使用实体,则可能会出现更多问题。最好不要在表示层中引用域/业务层程序集。

DTO惯例和验证

ASP.NET Boilerplate强烈支持DTO。它提供传统的类,接口和标准化DTO命名和使用约定。当您按照文档中所述编写代码时,ASP.NET Boilerplate将轻松实现一些常见任务的自动化。 

这是一个很好的例子。假设我们要开发一个应用程序服务方法,用于按名称搜索人员,然后返回人员 列表在这种情况下,我们可能有一个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);
}

ASP.NET Boilerplate建议将输入/输出参数命名为MethodName Input和MethodName Output,并为每个应用程序服务方法定义分离的输入和输出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; }
}

ASP.NET Boilerplate 在执行方法之前自动验证输入。它类似于ASP.NET MVC的模型验证,但请注意,应用程序服务不是Controller,它是一个普通的旧C#类。ASP.NET Boilerplate进行拦截并自动检查输入。有关详细信息,请参阅DTO验证文档。

EntityDto是一个声明Id属性的简单类,因为它们对于大多数实体是通用的。如果您的实体的主键不是int,则它具有通用版本。您不必使用它,但定义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,然后返回输出。请注意,我们没有验证输入?ASP.NET Boilerplate 验证了它。它甚至检查输入参数是否为null,如果是,则抛出异常。这使我们免于在每种方法中编写保护条款!

你可能在想,“我不是必须编写一大堆代码来在Person实体和PersonDto对象之间移动数据吗?” 这真是一项乏味的工作!Person实体可以拥有更多属性。

DTO和实体之间的自动映射

幸运的是,有一些工具可以让这很容易! AutoMapper就是其中之一。请参阅AutoMapper集成文档以了解如何在ASP.NET Boilerplate中使用它。

帮助程序接口和类

ASP.NET Boilerplate提供了一些辅助接口,可以实现这些接口以标准化常见的DTO属性名称。

ILimitedResultRequest定义MaxResultCount属性。这样,您可以在输入DTO中实现它以标准化限制结果集。

IPagedResultRequest扩展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命名空间下的接口和类以 获取更多信息。

nidie.com.cn - 用心与你沟通