项目

数据传输对象

数据传输对象(DTO)用于在应用层表示层或其他类型客户端之间传输数据。

通常,应用服务 由表示层调用(可选)时,会以DTO作为参数。它使用领域对象执行特定业务逻辑,并(可选)返回一个DTO给表示层。这样,表示层就与领域层完全隔离

使用DTO的必要性

如果您已经了解并确认使用DTO的好处,可以跳过本节

起初,为每个应用服务方法创建一个DTO类可能看起来繁琐且耗时。但如果正确使用,它们能保护您的应用程序。为什么?如何实现?

领域层的抽象

DTO提供了一种高效方式,将领域对象从表示层抽象化。实际上,您的层次结构得到了正确分离。如果您想完全更改表示层,可以继续使用现有的应用和领域层。或者,您可以重写领域层,完全更改数据库模式、实体和O/RM框架,而无需更改表示层。当然,这要求您的应用服务契约(方法签名和DTO)保持不变。

数据隐藏

假设您有一个User实体,具有Id、Name、EmailAddress和Password属性。如果UserAppServiceGetAllUsers()方法返回List<User>,任何人都可以访问所有用户的密码,即使您不在屏幕上显示它。这不仅仅是安全问题,还涉及数据隐藏。应用服务应仅返回表示层(或客户端)所需的数据,不多不少。

序列化与懒加载问题

当您将数据(对象)返回到表示层时,很可能会进行序列化。例如,在返回JSON的REST API中,您的对象将被序列化为JSON并发送给客户端。在这方面,将实体返回到表示层可能会有问题,特别是如果您使用关系数据库和ORM提供程序(如Entity Framework Core)。为什么?

在实际应用中,您的实体可能相互引用。User实体可能引用其Role。如果您想序列化User,其Role也会被序列化。Role类可能有List<Permission>,而Permission类可能引用PermissionGroup类,依此类推……想象所有这些对象同时被序列化。您可能轻易且意外地序列化整个数据库!此外,如果您的对象有循环引用,它们可能根本无法序列化。

解决方案是什么?将属性标记为NonSerialized?不,您无法知道何时应该序列化,何时不应该。它可能在一个应用服务方法中需要,在另一个中不需要。返回安全、可序列化且专门设计的DTO在这种情况下是一个好选择。

几乎所有的O/RM框架都支持懒加载。这是一个在需要时从数据库加载实体的功能。假设User类引用Role类。当您从数据库获取User时,Role属性(或集合)未填充。当您首次读取Role属性时,它会从数据库加载。因此,如果您将这样的实体返回到表示层,它将导致通过执行额外查询从数据库检索额外实体。如果序列化工具读取实体,它会递归读取所有属性,并再次可能检索整个数据库(如果实体之间存在关系)。

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

如果您确信要使用DTO,我们可以继续了解ABP关于DTO的提供和建议。

ABP不强制您使用DTO,但强烈建议将使用DTO作为最佳实践

标准接口与基类

DTO是一个简单的无依赖类,您可以以任何方式设计它。但ABP引入了一些接口来确定标准属性的命名约定,以及基类来在声明公共属性避免重复

这些都不是必需的,但使用它们可以简化和标准化您的应用程序代码。

与实体相关的DTO

您通常创建与实体对应的DTO,这会导致与实体相似的类。ABP提供了一些基类来简化创建此类DTO的过程。

EntityDto

IEntityDto<TKey> 是一个简单的接口,仅定义 Id 属性。您可以为匹配 实体 的 DTO 实现它或继承 EntityDto<TKey>

示例:

using System;
using Volo.Abp.Application.Dtos;

namespace AbpDemo
{
    public class ProductDto : EntityDto<Guid>
    {
        public string Name { get; set; }
        //...
    }
}

审计DTO

如果您的实体继承自审计实体类(或实现审计接口),您可以使用以下基类创建DTO:

  • CreationAuditedEntityDto
  • CreationAuditedEntityWithUserDto
  • AuditedEntityDto
  • AuditedEntityWithUserDto
  • FullAuditedEntityDto
  • FullAuditedEntityWithUserDto

可扩展DTO

如果您想为 DTO 使用 对象扩展系统 ,可以使用或继承以下 DTO 类:

  • ExtensibleObject 实现 IHasExtraProperties(其他类继承此类)。
  • ExtensibleEntityDto
  • ExtensibleCreationAuditedEntityDto
  • ExtensibleCreationAuditedEntityWithUserDto
  • ExtensibleAuditedEntityDto
  • ExtensibleAuditedEntityWithUserDto
  • ExtensibleFullAuditedEntityDto
  • ExtensibleFullAuditedEntityWithUserDto

列表结果

通常向客户端返回DTO列表。IListResult<T>接口和ListResultDto<T>类用于使其标准化。

IListResult<T>接口的定义:

public interface IListResult<T>
{
    IReadOnlyList<T> Items { get; set; }
}

示例:返回产品列表

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace AbpDemo
{
    public class ProductAppService : ApplicationService, IProductAppService
    {
        private readonly IRepository<Product, Guid> _productRepository;

        public ProductAppService(IRepository<Product, Guid> productRepository)
        {
            _productRepository = productRepository;
        }

        public async Task<ListResultDto<ProductDto>> GetListAsync()
        {
            //从存储库获取实体
            List<Product> products = await _productRepository.GetListAsync();

            //将实体映射到DTO
            List<ProductDto> productDtos =
                ObjectMapper.Map<List<Product>, List<ProductDto>>(products);

            //返回结果
            return new ListResultDto<ProductDto>(productDtos);
        }
    }
}

您可以简单地返回productDtos对象(并更改方法返回类型),这没有问题。返回ListResultDto会将您的List<ProductDto>包装到另一个对象中作为Items属性。这有一个优点:您以后可以向返回值添加更多属性,而不会破坏远程客户端(当它们获取值作为JSON结果时)。因此,特别是在开发可重用应用模块时,建议这样做。

分页与排序列表结果

更常见的是从服务器请求分页列表并向客户端返回分页列表。ABP定义了一些接口和类来标准化此过程:

输入(请求)类型

以下接口和类用于标准化客户端发送的输入。

  • ILimitedResultRequest:定义MaxResultCountint)属性,以从服务器请求有限结果。
  • IPagedResultRequest:继承自ILimitedResultRequest(因此固有地具有MaxResultCount属性),并定义SkipCountint)以在从服务器请求分页结果时声明跳过计数。
  • ISortedResultRequest:定义Sortingstring)属性,以从服务器请求排序结果。排序值可以是"Name"、"Name DESC"、"Name ASC, Age DESC"等。
  • IPagedAndSortedResultRequest继承自IPagedResultRequestISortedResultRequest,因此具有MaxResultCountSkipCountSorting属性。

建议继承以下基DTO类之一,而不是手动实现接口:

  • LimitedResultRequestDto实现ILimitedResultRequest
  • PagedResultRequestDto实现IPagedResultRequest(并继承自LimitedResultRequestDto)。
  • PagedAndSortedResultRequestDto实现IPagedAndSortedResultRequest(并继承自PagedResultRequestDto)。
最大结果数

LimitedResultRequestDto(以及其他类)通过以下规则限制和验证MaxResultCount

  • 如果客户端未设置MaxResultCount,则假定为10(默认页面大小)。可以通过设置LimitedResultRequestDto.DefaultMaxResultCount静态属性来更改此值。
  • 如果客户端发送的MaxResultCount大于1,000,则会产生验证错误。这对于保护服务器免受服务滥用非常重要。如果需要,可以通过设置LimitedResultRequestDto.MaxMaxResultCount静态属性来更改此值。

建议在应用程序启动时设置静态属性,因为它们是静态的(全局)。

输出(响应)类型

以下接口和类用于标准化发送到客户端的输出。

  • IHasTotalCount定义TotalCountlong)属性,以在分页情况下返回记录的总数。
  • IPagedResult<T>继承自IListResult<T>IHasTotalCount,因此具有ItemsTotalCount属性。

建议继承以下基DTO类之一,而不是手动实现接口:

  • PagedResultDto<T>继承自ListResultDto<T>,并实现IPagedResult<T>

示例:从服务器请求分页和排序结果并返回分页列表

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic.Core;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;

namespace AbpDemo
{
    public class ProductAppService : ApplicationService, IProductAppService
    {
        private readonly IRepository<Product, Guid> _productRepository;

        public ProductAppService(IRepository<Product, Guid> productRepository)
        {
            _productRepository = productRepository;
        }

        public async Task<PagedResultDto<ProductDto>> GetListAsync(
            PagedAndSortedResultRequestDto input)
        {
            //创建查询
            var query = _productRepository
                .OrderBy(input.Sorting);

            //从存储库获取总数
            var totalCount = await query.CountAsync();
            
            //从存储库获取实体
            List<Product> products = await query                
                .Skip(input.SkipCount)
                .Take(input.MaxResultCount).ToListAsync();

            //将实体映射到DTO
            List<ProductDto> productDtos =
                ObjectMapper.Map<List<Product>, List<ProductDto>>(products);

            //返回结果
            return new PagedResultDto<ProductDto>(totalCount, productDtos);
        }
    }
}

ABP还定义了一个PageBy扩展方法(与IPagedResultRequest兼容),可以代替Skip + Take调用:

var query = _productRepository
    .OrderBy(input.Sorting)
    .PageBy(input);

注意,我们向项目添加了Volo.Abp.EntityFrameworkCore包,以便能够使用ToListAsyncCountAsync方法,因为它们不包含在标准LINQ中,而是由Entity Framework Core定义。

如果您不理解示例代码,请参阅 存储库文档

相关主题

验证

应用服务 方法、控制器操作、页面模型输入等的输入会自动验证。您可以使用标准数据注释属性或自定义验证方法来执行验证。

有关更多信息,请参阅 验证文档

对象到对象映射

当您创建与实体相关的DTO时,通常需要映射这些对象。ABP提供了一个对象到对象映射系统来简化映射过程。请参阅以下文档:

最佳实践

您可以自由设计DTO类。但是,有一些最佳实践和建议您可能希望遵循。

通用原则

  • DTO应该是良好可序列化的,因为它们通常被序列化和反序列化(为JSON或其他格式)。如果您有带参数的构造函数,建议有一个空的(无参数)公共构造函数。
  • DTO不应包含任何业务逻辑,除了一些正式的 验证 代码。
  • 不要从实体继承DTO,也不要引用实体应用启动模板 通过分离项目已经防止了这一点。
  • 如果您使用自动 对象到对象映射 库(如 AutoMapper ),启用映射配置验证以防止潜在错误。

输入DTO原则

  • 仅定义用例所需的属性。不要包含未用于用例的属性,这会使开发人员困惑。

  • 不要在不同应用服务方法之间重用输入DTO。因为不同的用例将需要和使用DTO的不同属性,这导致某些属性在某些情况下未使用,使服务更难以理解和使用,并在未来导致潜在错误。

输出DTO原则

  • 如果您在所有情况下填充所有属性,可以重用输出DTO

另请参阅

在本文档中