ASP.NET Core 6 Web API清洁架构:配置Mapper与创建DTO及ViewModel
清洁架构下ASP.NET Core 6 Web API:ViewModel、DTO分层与Get方法示例
一、ViewModel与DTO的分层原则(核心结论)
首先明确清洁架构的分层职责边界,绝对不要在领域层放置任何DTO或ViewModel——领域层是核心业务逻辑层,只包含实体、值对象、领域服务等与业务规则强绑定的内容,不能依赖任何跨层传输模型。
1. ViewModel的定位与理解
ViewModel是表现层(Web API项目)专属的模型,它的唯一职责是适配API客户端的需求:
- 只返回客户端需要的字段(比如隐藏实体中的敏感字段如密码);
- 调整字段名以符合客户端习惯(比如把实体的
Name改成ViewModel的UserName); - 格式化数据(比如把
DateTime转成yyyy-MM-dd HH:mm的字符串); - 组合多实体数据(比如把用户信息和关联的订单统计合并到一个ViewModel)。
ViewModel完全与领域实体解耦,只服务于API的响应输出。
2. DTO的定位与存放
DTO(数据传输对象)是层与层之间的"数据容器",主要用于:
- 应用层与表现层之间传递数据(比如应用层查询返回
UserDto,表现层再映射为UserViewModel); - 基础设施层与应用层之间传递数据(比如仓储返回的封装数据)。
DTO建议放在应用层,因为应用层是协调领域层与外部的中间层,跨层传输的模型归属于这里最合适。
二、清洁架构下Get方法完整示例
以下是分层实现的极简示例,使用MediatR处理请求(清洁架构常用的命令查询分离方式)、AutoMapper做映射、EF Core做数据持久化。
1. 领域层(Domain)
仅包含核心实体与业务规则,不依赖任何外部层:
// Domain/Entities/User.cs public class User { public Guid Id { get; private set; } public string Name { get; private set; } public string Email { get; private set; } public DateTime CreatedAt { get; private set; } // 私有构造函数,通过工厂方法保证领域规则 private User() {} public static User Create(string name, string email) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("用户名不能为空"); if (string.IsNullOrWhiteSpace(email)) throw new ArgumentException("邮箱不能为空"); return new User { Id = Guid.NewGuid(), Name = name, Email = email, CreatedAt = DateTime.UtcNow }; } } // Domain/Abstractions/IUserRepository.cs // 仓储接口定义在领域层,实现放在基础设施层 public interface IUserRepository { Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken); }
2. 应用层(Application)
包含查询/命令定义、DTO、业务协调逻辑:
// Application/Dtos/UserDto.cs // 应用层与外部传输的DTO,仅包含需要传递的字段 public class UserDto { public Guid Id { get; set; } public string Name { get; set; } public string Email { get; set; } public DateTime CreatedAt { get; set; } } // Application/Queries/GetUserByIdQuery.cs // 查询请求定义 public class GetUserByIdQuery : IRequest<UserDto> { public Guid UserId { get; set; } } // Application/Queries/GetUserByIdQueryHandler.cs // 查询处理逻辑,协调仓储与映射 public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, UserDto> { private readonly IUserRepository _userRepository; private readonly IMapper _mapper; public GetUserByIdQueryHandler(IUserRepository userRepository, IMapper mapper) { _userRepository = userRepository; _mapper = mapper; } public async Task<UserDto> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) { var user = await _userRepository.GetByIdAsync(request.UserId, cancellationToken); if (user == null) throw new KeyNotFoundException("用户不存在"); return _mapper.Map<UserDto>(user); } }
3. 基础设施层(Infrastructure)
实现仓储、EF Core配置等外部依赖:
// Infrastructure/Persistence/AppDbContext.cs public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) {} public DbSet<User> Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<User>() .HasKey(u => u.Id); modelBuilder.Entity<User>() .Property(u => u.Name) .HasMaxLength(50) .IsRequired(); modelBuilder.Entity<User>() .Property(u => u.Email) .HasMaxLength(100) .IsRequired(); } } // Infrastructure/Persistence/Repositories/UserRepository.cs // 仓储接口的EF实现 public class UserRepository : IUserRepository { private readonly AppDbContext _dbContext; public UserRepository(AppDbContext dbContext) { _dbContext = dbContext; } public async Task<User?> GetByIdAsync(Guid id, CancellationToken cancellationToken) { return await _dbContext.Users.FindAsync(new object[] { id }, cancellationToken); } }
4. 表现层(Web API)
包含ViewModel、控制器、映射配置:
// WebApi/ViewModels/UserViewModel.cs // 面向客户端的响应模型,适配API输出需求 public class UserViewModel { public Guid Id { get; set; } public string UserName { get; set; } // 调整字段名更友好 public string Email { get; set; } public string CreatedAt { get; set; } // 格式化日期为字符串 } // WebApi/MappingProfiles/UserMappingProfile.cs // AutoMapper映射配置,分层次映射 public class UserMappingProfile : Profile { public UserMappingProfile() { // 领域实体 → 应用层DTO CreateMap<User, UserDto>(); // 应用层DTO → 表现层ViewModel CreateMap<UserDto, UserViewModel>() .ForMember(dest => dest.UserName, opt => opt.MapFrom(src => src.Name)) .ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => src.CreatedAt.ToString("yyyy-MM-dd HH:mm"))); } } // WebApi/Controllers/UsersController.cs [ApiController] [Route("api/[controller]")] public class UsersController : ControllerBase { private readonly IMediator _mediator; private readonly IMapper _mapper; public UsersController(IMediator mediator, IMapper mapper) { _mediator = mediator; _mapper = mapper; } [HttpGet("{id:guid}")] public async Task<ActionResult<UserViewModel>> GetUserById(Guid id, CancellationToken cancellationToken) { try { var userDto = await _mediator.Send(new GetUserByIdQuery { UserId = id }, cancellationToken); var viewModel = _mapper.Map<UserViewModel>(userDto); return Ok(viewModel); } catch (KeyNotFoundException ex) { return NotFound(new { Message = ex.Message }); } } }
三、关键注意事项
- 各层依赖方向:表现层→应用层→领域层,基础设施层依赖领域层,绝对反向依赖;
- 可以根据API需求定义多个ViewModel(比如精简版、详细版),但DTO尽量保持稳定,只传递核心数据;
- 避免直接在控制器中操作EF Core,所有数据访问都通过仓储接口,保证领域层的独立性。
内容的提问来源于stack exchange,提问作者Mira
相关产品推荐
相关产品推荐

