大家好,我是刚子。

写了几年.NET代码,AutoMapper这玩意儿没少用。

刚开始接触的时候就觉得——“哎呦,这东西太爽了!”不用再一行一行手动赋值,一个Map全搞定。但用着用着就发现,光会CreateMap可不够。遇到复杂情况,简单配置根本搞不定,要么报错,要么映射出来的东西不是你要的。

今天刚子就跟你聊聊,我这些年踩坑踩出来的3个高级配置技巧。保证小白也能看懂。


技巧一:值转换器——搞定“类型对不上”

啥时候用这个?就是源属性和目标属性类型不一样的时候。

举个例子,你数据库里存的是decimal类型的金额,比如123.45。但前端要展示带美元符号的字符串,比如"$123.45"。你要是不用转换器,就得在DTO里单独搞个字符串属性,或者在映射之后手动格式化。麻烦不麻烦?

用值转换器就简单了:

public class CurrencyFormatter : IValueConverter<decimal, string>
{
    public string Convert(decimal source, ResolutionContext context)
    {
        return source.ToString("C2");  // 输出 $123.45
    }
}

// 配置
cfg.CreateMap<Order, OrderDto>()
    .ForMember(dest => dest.Amount, 
        opt => opt.ConvertUsing(new CurrencyFormatter()));

每次碰到Order里的Amount要映射到OrderDtoAmount,AutoMapper就会自动走这个转换逻辑。

划重点:值转换器的好处是能重复用。你可以在好几个地方用同一个转换器,不用每个地方都写一遍格式化代码。但有个小坑:值转换器只在普通映射时生效,如果你用EF Core直接从数据库投影(ProjectTo),它不干活。这个记一下就行。


技巧二:条件映射——想清楚再动手

这个技巧特别实用。简单说就是:满足条件才映射,不满足就跳过

举个例子,你有一个用户实体,年龄是int类型,但目标DTO里年龄是uint(无符号整数,就是不能为负数)。负数不能转成无符号整数,对吧?这时候就可以加个条件:

cfg.CreateMap<User, UserDto>()
    .ForMember(dest => dest.Age, opt => opt.Condition(src => src.Age >= 0));

只有源年龄大于等于0时,才映射。

条件映射里还有一对孪生兄弟:ConditionPreCondition,它俩的区别就是谁先跑。PreCondition跑得更早,在源值被拿出来之前就执行。PreCondition主要是为了省时间——如果你的源值解析非常耗时(比如要查数据库),可以在PreCondition里先判断是否满足条件,不满足就直接跳过,不用浪费时间。

划重点:条件映射别到处用。偶尔用一两个没问题,但如果你发现到处都在写Condition,多半是你的模型设计本身有问题,回去改模型比补条件更省事。


技巧三:自定义值解析器——复杂逻辑的归宿

值转换器适合解决“类型对不上”这种一对一的问题。但有些场景更复杂——比如你需要从源对象的好几个地方拿信息,拼成一个目标属性;或者逻辑太复杂,一行代码写不下。

这时候就要上自定义值解析器了。

举个例子,你要把Person里的FirstName(名)和LastName(姓)拼成FullName(全名):

public class FullNameResolver : IValueResolver<Person, PersonDto, string>
{
    public string Resolve(Person source, PersonDto destination, 
        string destMember, ResolutionContext context)
    {
        return $"{source.FirstName} {source.LastName}";
    }
}

// 配置
cfg.CreateMap<Person, PersonDto>()
    .ForMember(dest => dest.FullName, 
        opt => opt.MapFrom<FullNameResolver>());

解析器的Resolve方法能拿到源对象、目标对象、目标成员名和上下文信息。这意味着你可以访问目标对象的其他属性来做复杂判断。

划重点:自定义解析器还有个隐藏好处——支持依赖注入。如果你的解析器里需要用到数据库、缓存啥的,可以直接通过构造函数传进来。但注意要把Mapper配置成单例模式,不然每个请求都建个新的,服务器扛不住。


一个真实案例:三个技巧一起上

说了这么多理论,刚子给你来个真实的例子。

有这样一个需求:订单列表页面,后端返回的Order实体里存的是decimal金额和DateTime时间,但前端要展示带货币符号的字符串和格式化的日期。而且如果订单是取消状态,金额那一栏直接显示“已取消”而不是金额数字,金额超过1万的还要加个特殊标识。

如果不用高级配置,只能在DTO里单独搞字符串属性,然后在Service层手写逻辑。代码又多又难维护,每次改需求都得翻来覆去改好几个地方。

用AutoMapper的高级技巧,一个配置文件全搞定:

public class OrderProfile : Profile
{
    public OrderProfile()
    {
        CreateMap<Order, OrderDto>()
            // 金额映射:走转换器 + 条件判断 + 解析器组合
            .ForMember(dest => dest.FormattedAmount, opt =>
            {
                // 条件:订单状态是取消的话,跳过金额转换逻辑
                opt.Condition(src => src.Status != OrderStatus.Cancelled);
                // 再用转换器把 decimal 变成 $123.45 格式
                opt.ConvertUsing(new CurrencyFormatter());
            })
            // 日期映射:用值转换器搞定格式
            .ForMember(dest => dest.FormattedCreateTime, 
                opt => opt.ConvertUsing(new DateTimeFormatter("yyyy-MM-dd HH:mm")))
            // 超万金额标识:用自定义解析器,内部判断金额>10000时追加标识
            .ForMember(dest => dest.AmountDisplay, 
                opt => opt.MapFrom<AmountDisplayResolver>());
    }
}

你看,原来要在Service层写一大坨逻辑才能搞定的事儿,现在全收进Profile里了。业务层代码干干净净,就一行_mapper.Map<OrderDto>(order)

划重点:把映射逻辑收进Profile里,还有一个额外的好处——单元测试特别好写。你只需要测试Profile的配置对不对,不用把Service层也扯进来,测试用例又少又干净。


最后刚子想说

AutoMapper这个工具,入门简单,想玩溜真得花点功夫。

今天讲的这三个技巧——值转换器、条件映射、自定义值解析器——都是我这些年实战总结出来的。学会了,你就不用再被那些复杂的映射场景折磨了。

记住:能用配置解决的问题,就别写到业务代码里。

如果你觉得这篇文章有用,点个赞、转给还在手写对象映射的兄弟

我是刚子,一个写了六年代码的.NET程序员。咱们下回见!