惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
Apple Machine Learning Research
Apple Machine Learning Research
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
雷峰网
雷峰网
S
SegmentFault 最新的问题
量子位
有赞技术团队
有赞技术团队
V
V2EX
宝玉的分享
宝玉的分享
Hugging Face - Blog
Hugging Face - Blog
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Jina AI
Jina AI
C
Check Point Blog
G
Google Developers Blog
博客园 - 叶小钗
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园_首页
T
Tailwind CSS Blog
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
酷 壳 – CoolShell
酷 壳 – CoolShell
U
Unit 42

博客园 - 邓磊Lei

我可以双击 shift 打开 visual studio 的代码搜索 我决定把 gittoy 更名为 git blame for vs 为了可以重新拥抱 Visual Studio, 我做了一个 vs 版的 gittoolbox ABP 应用服务源码解读:ApplicationService 基类内置了哪些能力 ABP 仓储模式源码解读:自动过滤是如何实现的 ABP DDD 实体源码解读:7 层基类的设计逻辑和适用场景 ABP 模块系统源码学习:启动时模块是如何加载和排序的 为什么 Go 一个 HTTP 服务可以同时处理数万连接 gin怎么处理一次http请求的 我的经验:git提交信息分别用什么icon C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用 .NET 10 使用 Microsoft.AspNetCore.OpenApi 实现 API 版本管理 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 Microsoft Agent Framework + Kimi API 实战:控制台应用跑通单次与多轮 Agent 对话 开发实战:asp.net core + ef core 实现动态可扩展的分页方案 聊聊 ASP.NET Core 中间件和过滤器的区别 Python 入门:从“其他语言”到 Pythonic 思维的完整迁移手册 .NET 进阶之路:异步、并发与内存管理的系统性认知 Redis:延迟双删的适用边界与落地细节 Serilog:从结构化日志认知到 .NET 工程落地 EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界 EF Core 拦截器实战:SaveChangesInterceptor、CommandInterceptor 与审计落地 ASP.NET Core 外部依赖调用治理实战:HttpClientFactory、Polly 与幂等边界 .NET .Result 避坑指南:不同框架下的死锁与线程池饥饿 EF Core 慢查询排查实战:TagWith、OpenTelemetry、执行计划,30 分钟定位性能瓶颈 EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理 EF Core 写入链路深拆:从 ChangeTracker 到 SQL Batch 的性能诊断与优化 ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地 C# 异步编程深水区:Task、ValueTask、线程池饥饿与背压设计 EF Core 查询性能黑洞:Include、投影与跟踪策略的边界
ABP DTO 与对象映射:AutoMapper Profile 配置与 Mapperly 源...
云深见海 · 2026-08-06 · via 博客园 - 邓磊Lei

DTO 基类的设计

ABP 提供了标准的 DTO 基类,看其源码继承关系:

// framework/src/Volo.Abp.Ddd.Application.Contracts/Volo/Abp/Application/Dtos/

// 单个实体输出
public abstract class EntityDto<TKey> : EntityDto, IEntityDto<TKey>
{
    public TKey Id { get; set; }
}

// 分页响应
public class PagedResultDto<T> : ListResultDto<T>, IPagedResult<T>
{
    public long TotalCount { get; set; }

    public PagedResultDto(long totalCount, IReadOnlyList<T> items) : base(items)
    {
        TotalCount = totalCount;
    }
}

// 分页请求参数
public class PagedAndSortedResultRequestDto : PagedResultRequestDto, IPagedAndSortedResultRequest
{
    public virtual string? Sorting { get; set; }
}

public class LimitedResultRequestDto : ILimitedResultRequest, IValidatableObject
{
    public static int DefaultMaxResultCount { get; set; } = 10;   // 默认每页 10 条
    public static int MaxMaxResultCount { get; set; } = 1000;     // 最大 1000 条

    [Range(1, int.MaxValue)]
    public virtual int MaxResultCount { get; set; } = DefaultMaxResultCount;
}

LimitedResultRequestDto 自带验证逻辑,当 MaxResultCount > 1000 时会返回验证错误。这个限制可以有效防止恶意请求拉取全量数据。

AutoMapper 集成源码

ABP 的 AutoMapper 集成在 Volo.Abp.AutoMapper 包中。核心是 AbpAutoMapperModule

[DependsOn(typeof(AbpObjectMappingModule), typeof(AbpObjectExtendingModule))]
public class AbpAutoMapperModule : AbpModule
{
    public override void ConfigureServices(ServiceConfigurationContext context)
    {
        context.Services.AddAutoMapperObjectMapper();

        context.Services.AddSingleton<IConfigurationProvider>(sp =>
        {
            var options = sp.GetRequiredService<IOptions<AbpAutoMapperOptions>>().Value;
            // 执行所有配置器
            foreach (var configurator in options.Configurators)
            {
                configurator(autoMapperConfigurationContext);
            }
            return new MapperConfiguration(mapperConfigurationExpression);
        });
    }
}

AbpAutoMapperOptions 支持两种注册 Profile 的方式:

// 方式 1:通过 AbpAutoMapperOptions 配置
Configure<AbpAutoMapperOptions>(options =>
{
    options.AddProfile<BookProfile>();
    options.AddProfile<UserProfile>();
});

// 方式 2:通过 ASP.NET Core 的 AddAutoMapper 扩展
context.Services.AddAutoMapper(typeof(BookProfile), typeof(UserProfile));

Mapperly 方案

Mapperly 是 ABP 提供的另一种映射方案,通过源生成器在编译时生成映射代码:

// 定义映射
[Mapper]
public static partial class BookMapper
{
    public static partial BookDto MapToDto(Book book);
}

// 注入 ObjectMapper 后使用
ObjectMapper.Map<Book, BookDto>(book);

与 AutoMapper 的关键区别:

AutoMapper Mapperly
映射时机 运行时(反射) 编译时(源生成器)
性能 首次需创建映射配置 零反射,直接方法调用
使用方式 Profile 配置 静态方法标注
适用场景 复杂映射、现有项目 性能敏感、新项目

实战:Profile 配置的最佳实践

// 1. 命名实体和 DTO 一一对应
CreateMap<Book, BookDto>();  // 按属性名自动匹配

// 2. 明确忽略不应由客户端传入的字段
CreateMap<CreateBookDto, Book>()
    .ForMember(x => x.Id, opt => opt.Ignore())              // ID 由服务端生成
    .ForMember(x => x.CreationTime, opt => opt.Ignore())    // 时间由服务端设置
    .ForMember(x => x.TenantId, opt => opt.Ignore());       // 租户由上下文注入

// 3. 敏感字段不出现在 DTO 中
public class UserDto : EntityDto<Guid>
{
    public string UserName { get; set; }
    // PasswordHash 不在这里,自动忽略
}

使用注意事项

// 一般不建议把实体直接返回
[HttpGet("{id}")]
public async Task<User> GetUser(Guid id)    // ❌ 直接暴露实体
{
    return await _userRepo.GetAsync(id);
}

// 使用 DTO
[HttpGet("{id}")]
public async Task<UserDto> GetUser(Guid id) // ✅ 使用 DTO
{
    var user = await _userRepo.GetAsync(id);
    return ObjectMapper.Map<User, UserDto>(user);
}