1 为何需要DTO
为每个应用服务方法创建一个DTO起初可能被看作是一项乏味而又耗时的事情。但如果正确地使用它,那么DTOs可能会拯救你应用。为啥呢?
1.1 领域层抽象
DTO为展现层抽象领域对象提供了一种有效方式。这样,层与层之间就正确分离了。即使你想完全分离展现层,仍然可以使用已存在的应用层和领域层。相反,只要领域服务的契约(方法签名和DTOs)保持不变,即使重写领域层,完全改变数据库模式,实体和ORM框架,也不需要在展现层做任何改变。
1.2 数据隐藏
试想你有一个User实体,包含Id,Name,EmailAddress和Password字段。如果UserAppService的GetAllUsers()方法返回一个List,即使你没有在屏幕上显示它,那么任何人也都能看到所有user的密码。它不是涉及安全的,而是与数据隐藏相关的。应用服务都应该返回给展现层需要的,不要更多,也不很少,要的是恰到好处。
1.3 序列化和懒加载问题
在一个真实应用中,实体之间是相互引用的。User实体可能有一个Role的引用。因此,如果你想序列化User,那么Role也会序列化。而且,如果Role有一个List且Permission类有一个PermissionGroup类的引用等等。你能想象所有的对象都会被序列化的那种场景吗?你可能会意外地序列化整个数据库。那么解决方案是什么呢?把属性标记为NonSerilized吗?不,你可能不知道它何时应该序列化,何时不应该。它可能在一个应用方法中需要,可能在另一个就不需要了。因此,在这种情景中,设计一个可安全序列化的,特别设计的DTOs是一种好的选择。
几乎所有的ORM框架都支持懒加载。它的特征是当需要时才从数据库中加载实体。假如说User类有一个Role类的引用。当从数据库中获得一个User时,此时Role属性还没有填充,当第一次读该Role属性时,它才从数据库中加载。因此,不要将这样的一个实体直接返回给展现层,它可能会轻易造成从数据库检索额外的实体。如果序列化工具读到了该实体,它会递归地读取所有属性,最终整个数据库可能会被检索(如果实体间有合适的关系)。
在展现层使用实体还会有更多的问题。最好压根不要在将包含领域(业务)层的程序集引用到展现层上。
2 DTO惯例和验证
BatchDeleteInput
using System.Collections.Generic;
namespace Rdf.Application.Services.Dto
{
public class BatchDeleteInput : IInputDto
{
public List<string> IdList { get; set; }
}
}
JsonMessage
namespace Rdf.Application.Services.Dto
{
public class JsonMessage : IOutputDto
{
public int ErrCode { get; set; }
public string ErrMsg { get; set; }
public object Result { get; set; }
}
}
PageInput
namespace Rdf.Application.Services.Dto
{
public class PageInput : IInputDto
{
public int PageIndex { get; set; }
public int PageSize { get; set; }
public string OrderBy { get; set; }
public string Keyword { get; set; }
}
}
PageOutput
using System.Collections;
namespace Rdf.Application.Services.Dto
{
public class PageOutput : IOutputDto
{
public int Total { get; set; }
public IEnumerable Records { get; set; }
}
}
IDto
namespace Rdf.Application.Services.Dto
{
/// <summary>
/// This interface must be implemented by all DTO classes to identify them by convention.
/// </summary>
public interface IDto
{
}
}
IValidate
namespace Rdf.Application.Services.Dto
{
/// <summary>
/// This interface is implemented by classes those are needed to validate before use.
/// </summary>
public interface IValidate
{
}
}
IInputDto
namespace Rdf.Application.Services.Dto
{
/// <summary>
/// This interface is used to define DTOs those are used as input parameters.
/// </summary>
public interface IInputDto : IDto, IValidate
{
}
}
IOutputDto
namespace Rdf.Application.Services.Dto
{
/// <summary>
/// This interface is used to define DTOs those are used as output parameters.
/// </summary>
public interface IOutputDto : IDto
{
}
}
3 DTO和实体的自动映射
幸好,我们有工具可以让这个变得很简单,AutoMapper就是之一。
Configuration.cs
using AutoMapper;
namespace Rdf.Web.AutoMapper
{
public class Configuration
{
public static void Configure()
{
Mapper.Initialize(cfg =>
{
cfg.AddProfile<Profiles.ActProfile>();
});
}
}
}
ActProfile.cs
using AutoMapper;
using Rdf.Application.Act.Dtos;
using Rdf.Domain.Entities.Act;
namespace Rdf.Web.AutoMapper.Profiles
{
public class ActProfile : Profile
{
protected override void Configure()
{
CreateMap<CreateRoleInput, Role>();
}
}
}
Startup.cs
// InitMapping
AutoMapper.Configuration.Configure();