在软件开发中,特别是在分布式系统和微服务架构中,数据传输对象(DTO, Data Transfer Object) 是一个非常重要的设计模式。它用于简化数据在不同层或组件之间的传输过程,提高代码的可维护性和性能。本文将详细介绍 C# 中的 DTO 。
DTO 是一种 仅包含数据、不含业务逻辑 的轻量级对象,其核心目标是在系统不同层级或组件之间高效、安全地传输数据。DTO 将所需数据整合打包,避免直接暴露复杂的领域实体或数据库表结构,减少不必要的数据传递开销与耦合度。例如,在电商系统中,数据库可能存储了包含 20 个字段的订单实体,但前端只需展示订单号、总价等 5 个字段。此时 DTO 可以精准筛选数据,避免传输冗余信息。
假设我们有一个 User
实体类:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
我们可以为这个实体创建一个 DTO 类:
public class UserDto
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
在这个例子中,UserDto
只包含了我们需要在网络上传输的字段,而忽略了 CreatedAt
和 UpdatedAt
字段。
get
/set
方法,不涉及数据验证或业务逻辑。User
转换为 UserDTO
时,剔除密码字段。DTO 应仅作为数据容器。若需数据验证,可通过注解(如 [Required]
)实现,而非在 DTO 中添加方法。
public class ProductDTO
{
[Required]
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
XXXDTO
或 XXXViewModel
命名(如 OrderDTO
)。OrderDTO
包含 List
)。特性 | DTO | 实体类(Entity) |
---|---|---|
用途 | 数据传输 | 表示业务实体或数据库映射 |
行为 | 无业务逻辑,仅属性 | 可能包含业务方法或数据访问逻辑 |
字段控制 | 仅暴露必要字段 | 通常完整映射数据库表结构 |
生命周期 | 仅在传输过程中存在 | 持久化存储,贯穿业务逻辑生命周期 |
示例: 用户实体类 User 可能包含密码字段,而 UserDTO 仅暴露用户名和邮箱。
对象类型 | 用途 | 示例场景 | 是否含逻辑 |
---|---|---|---|
实体类/Entity | 映射数据库表结构 | ORM 框架中的 User 实体 |
可能包含业务方法 |
VO | 前端展示数据(如格式化日期) | 显示用户名的 UserVO |
无逻辑,仅数据 |
DTO | 跨层数据传输 | API 返回的 UserResponseDTO |
无逻辑,仅数据 |
// 反模式:直接暴露数据库实体
public class UserController : Controller
{
public IActionResult GetUser(int id)
{
var user = _dbContext.Users.Find(id); // 返回包含密码哈希的实体
return Ok(user); // ❌ 敏感数据泄露
}
}
手动编写 DTO 类,并将实体对象的数据拷贝到 DTO。适用于简单场景,代码清晰直接。
// 数据库实体类
public class UserEntity
{
public long Id { get; set; }
public string Username { get; set; }
public string Password { get; set; } // 敏感信息
public string Email { get; set; }
}
// DTO 类
public class UserDTO
{
public long Id { get; set; }
public string Username { get; set; }
public string Email { get; set; }
}
public class DTOConverter
{
// 手动封装
public UserDTO ConvertUserToDTO(UserEntity user)
{
return new UserDTO
{
Id = user.Id,
Username = user.Username,
Email = user.Email
};
}
}
init
关键字(C# 9+)不可变的数据对象,更适合做DTO
public class ProductDto
{
public int Id { get; init; }
public string Name { get; init; }
}
// 使用示例
var dto = new ProductDto { Id = 1, Name = "Laptop" };
C# 9.0 引入了记录类型(record
),它们是不可变的数据容器,并且默认实现了值相等性比较。记录类型非常适合用作 DTO。
public record UserDto(int Id, string Name, string Email);
class Program
{
static void Main()
{
var userDto = new UserDto(1, "Bob", "[email protected]");
Console.WriteLine(userDto); // 输出: UserDto { Id = 1, Name = Bob, Email = [email protected] }
}
}
// 数据库实体
public class UserEntity
{
public int Id { get; set; }
public string Username { get; set; }
public string PasswordHash { get; set; }
public DateTime CreatedAt { get; set; }
}
// DTO设计
public record UserDto(int Id,string Username,DateTime CreatedAt);
记录类型提供了简洁的语法和不可变性,非常适合用于 DTO。
public record OrderDto(int OrderId,string CustomerName,List<OrderItemDto> Items);
public record OrderItemDto(string ProductName,decimal UnitPrice,int Quantity);
方法 | 优点 | 缺点 |
---|---|---|
手动映射 | 完全控制 | 代码冗余 |
AutoMapper | 自动转换 | 配置复杂 |
手动转换实体对象和 DTO 对象可能会导致大量重复代码。为了简化这一过程,可以使用 AutoMapper 库来进行自动映射。详见:C# AutoMapper 框架使用详解。
在 ASP.NET Core 中,DTO 常用于:
// 请求 DTO
public record CreateUserRequest(string Username,string Password,string Email);
// 响应 DTO
public record UserResponse(int Id,string Username,string Email,DateTime CreatedAt);
// 控制器
[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserRequest request)
{
var entity = _mapper.Map<UserEntity>(request);
_repository.Add(entity);
var response = _mapper.Map<UserResponse>(entity);
return CreatedAtAction(nameof(GetUser), new { id = entity.Id }, response);
}
如果你的应用程序需要支持多个版本的 API,考虑为每个版本创建不同的记录类型。这样可以避免破坏现有客户端的兼容性。
public record PersonV1(string Name);
public record PersonV2(string Name, int Age);
微服务间通过 DTO 传输数据,减少网络开销并明确接口契约隐藏实现细节。
// 订单服务DTO
public record OrderCreatedEvent(Guid OrderId,string CustomerId,decimal TotalAmount,DateTimeOffset CreatedAt);
// 消息发布
_bus.Publish(new OrderCreatedEvent(order.Id,order.CustomerId,order.CalculateTotal(),DateTimeOffset.UtcNow));
在分层架构中,如 MVC 或 n 层架构,DTO 可以用于在不同的层之间传递数据。
如从数据访问层(DAO)向服务层传递数据时,避免暴露数据库细节。
在 MVC 架构中,DTO(或 ViewModel)将后端数据适配到前端视图,例如聚合多个实体的字段。
// 错误:在DTO中添加业务逻辑
public class OrderDto
{
public decimal CalculateTotal() => Items.Sum(i => i.Price * i.Quantity); // ❌
}
// 正确:计算逻辑应留在领域层
如果你的应用程序需要支持多个版本的 API,考虑为每个版本创建不同的 DTO 类。这样可以避免破坏现有客户端的兼容性。
// V1 DTO
public class ProductDtoV1
{
public int Id { get; set; }
public string Name { get; set; }
}
// V2 DTO(向后兼容)
public class ProductDtoV2 : ProductDtoV1
{
public string Category { get; set; }
}
DTO 应仅包含前端或目标方所需的字段,避免包含过多的信息,避免包含过多的业务逻辑。这不仅减少了网络传输的数据量,还提高了代码的可读性和维护性。推荐使用不可变record类型。
不要直接暴露数据库实体类,DTO 只做数据传输的“载体”。
简单场景可手动封装,复杂场景推荐使用 AutoMapper。
在某些情况下,你可以使用泛型和接口来提高 DTO 的灵活性和复用性。例如:
public interface IEntityDto<TId>
{
TId Id { get; set; }
}
public class UserDto : IEntityDto<int>
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
将 DTO 定义在独立项目(如 Application.DTOs
)中,便于复用。
回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
Clean Architecture
Microsoft Docs: Design Patterns
AutoMapper Documentation
Best Practices for Using DTOs in C#