数据传输对象
DTO 是简单对象,用于在应用层和展示层传递状态数据。所以,应用服务方法返回 DTO。
DTO原则和最佳实践:
- DTO应该可序列化,因为大多数时候,需要网络传输。
- 应该有一个无参构造函数
- 不能包含任何业务逻辑
- 不能继承或引用实体
输入DTO和输出DTO在本质上不同:一个用于给应用服务方法传递参数,一个作为应用服务方法的返回值,根据业务需要区别对待。
输入DTO最佳实践
不要在输入DTO中定义不使用的属性
只定义需要用的属性,否则,无用的属性只会让客户端在使用应用服务方法时感到困惑。当然可以定义可选属性,但是确保当客户端在使用时,不应该影响到用例的工作方式。
这条规则看起来没什么必要,谁会为方法仓储(输入DTO)添加不使用的属性呢?但是,它经常发生,尤其是当你想重用输入DTO对象时,会将多个DTO属性放在一个DTO对象中。
不要重用输入DTO
为每个用例(应用服务方法)定义特定的输入DTO,否则,在某些情况下不会添加一些不被使用的属性,这就违反了上面定义的规则。
有时候,在两个不同的用例中使用相同的DTO似乎很有吸引力,因为他们如此相似。甚至,当前是一模一样,可能后面随着业务变化才会有可能不同,此时也应该不要重用输入DTO。因为和用例间的耦合相比,代码复制可能是更好的做法。
重用DTO的另一种方式是:DTO继承,这同样会产生上面描述的问题.。
示例:用户应用服务
public interface IUserAppService:IApplicationService { Task CreateAsync(UserDto input); Task UpdateAsync(UserDto input); Task ChangePasswordAsync(UserDto input); }
IUserAppService 在所有方法(用例)使用 UserDto 作为输入DTO,UserDto定义如下:
public class UserDto { public Guid Id{get;set;} public string UserName{get;set;} public string Email{get;set;} public string Password{get;set;} public DateTime CreationTime{get;set;} }
- Id 在 Create 方法中不被使用,因为 Id 由服务器生成。
- Password 在 Update 方法中不使用,因为有修改密码的单独方法。
- CreationTime 未被使用,且不应该由客户端发送给服务端,应该在服务端设置创建时间。
正确的实现,如下:
public interface IUserAppService:IApplicationService { Task CreateAsync(UserCreationDto input); Task UpdateAsync(UserUpdateDto input); Task ChangePasswordAsync(UserChangePasswordDto input); }
然后定义对应的DTO类:
public class UserCreationDto { public string UserName {get;set;} public string Email{get;set;} public string Password{get;set;} } public class UserUpdateDto { public Guid Id{get;set;} public string UserName{get;set;} public string Email{get;set;} } public class UserChangePasswordDto { public Guid Id{get;set;} public string Password{get;set;} }
尽管需要编写更多的代码,但是这是一种更易维护的方法。
特殊情况:举个例子,如果你有一个报表页,页面中有多个过滤条件,对应多个应用服务方法(显示报表、导出Excel、导出CSV),此时应该使用相同的输入DTO参数,返回不同的结果。因为当页面过滤条件改变时,修改一个DTO而对整个页面对应的应用服务方法参数生效。
输入DTO中验证逻辑
- 仅在DTO内部执行简单验证,使用数据注解特性或实现 IValidatableObject 接口
- 不要执行领域验证,举个例子,不要在DTO中检测用户名是否唯一的验证。
示例:使用数据注解特性
using System.ComponentModel.DataAnnotations; namespace IssueTracking.Users { public class UserCreationDto { [Required] [StringLength(UserConsts.MaxUserNameLength)] public string UserName {get;set;} [Required] [EmailAddress] [StringLength(UserConsts.MaxEmailLength)] public string Email{get;set;} [Required] [StringLength(UserConsts.MaxEmailLength,MinimumLength=UserConsts.MinPasswordLength)] public string Password{get;set;} } }
ABP框架自动验证输入DTO,验证失败则抛出AbpValidationException异常,返回 400 HTTP 状态码。
某些开发者认为将验证规则和DTO类分离可能会更好。我们认为声明式(数据注解)是实用的,不会导致任何设计问题。当然,ABP支持 FluentValidation集成。
输出DTO最佳实践
- 保持输出DTO数量最小,尽可能重用,但是不能将输入DTO作为输出DTO使用。
- 输出DTO可以包含比用例需要的更多属性
- Create 和 Update 方法中返回DTO
以上建议的主要原因是:
- 使客户端代码易于开发和扩展
- 在客户端端处理不同但相似的DTO容易混淆
- 输入DTO中的更多属性可能未来会在UI/客户端中被使用,返回实体的所有属性(已经考虑过安全性和特殊情况)使客户端代码易于改进,而不需要修改后端代码。
- 如果是通过API暴露给第三方客户端,避免不同需求返回不同DTO
- 使服务端代码易于开发和扩展
- 更少的类,易于理解和维护
- 可以重用实体到DTO(AutoMapper)的对象映射代码
- 不同方法返回相同类型,使添加新方法变得简单明了。
示例:从不同方法返回不同DTO
public interface IUserAppService:IApplicationService { UserDto Get(Guid id); List
示例中没有使用异步方法,在实际开发时应该是异步方法。
上面的示例代码中,为每个方法返回不同DTO类型,这样会导致我们需要处理非常多的数据查询,映射实体到DTO的重复代码。
按照以下方式定义就简单多了:
public interface IUserAppService:IApplicationService { UserDto Get(Guid id); List
使用一个输出DTO:
public class UserDto { public Guid Id{get;set;} public string UserName{get;set;} public string Email{get;set;} public DateTiem CreationTime{get;set;} public List
- 移除 GetUserNameAndEmail 和 GetRoles 方法,因为 Get 方法已经返回足够需要的信息。
- GetList 返回对象与 Get 相同
- Create 和 Update 同样返回 UserDto
由此可见,返回相同DTO更加简洁。
为什么创建或更新之后要返回DTO? 想象一个用例场景,在页面中显示表格数据,当更新之后,获取返回对象,并对表格数据源进行更新,这样就不需要再次调用 GetList 方法,这是我们建议在 Create 和 Update 方法中返回 DTO 的原因。
讨论
以上关于输出DTO的建议,并不适用所有场景。
出于性能考虑,这些建议可以被忽略,特别是当存在大型数据集返回结果时,或者用户界面需要发起很多并发请求时,此时应该创建特定的输出DTO,只包含尽可能少的信息。
可维护性和性能,需要开发者权衡,上面的建议适用于性能损失可忽略不计的应用。
对象映射
自动对象映射是一个非常有用的工具,两个对象的属性相同或相似,将一个对象的值复制给另一个对象。
DTO和实体类通常具有相同或相似的属性,通常需要根据实体和业务需求来创建DTO对象。ABP框架对象映射基于 AutoMapper,相比手动赋值,效率更高。
- 仅对实体到输出DTO使用自动对象映射。
- 输入DTO到实体,不适用自动对象映射。
不使用输入DTO到实体自动映射的原因:
- 实体类通常有构造函数,接收参数并在创建时,进行参数验证。自动对象映射操作通常需要无参构造函数创建对象。
- 实体属性设置器大多是私有的,应该使用方法设置属性值。
- 通常需要仔细验证和处理用户/客户端输入,而不是盲目地映射到实体属性。
虽然其中一些问题可以通过映射配置来解决(例如,AutoMapper允许定义自定义映射规则),但它使你的业务逻辑隐含/隐藏,并与基础设施紧密耦合。我们认为业务代码应该是明确的、清晰的、容易理解的。