You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 05:35:33