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

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

举个例子,你数据库里存的是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层也扯进来,测试用例又少又干净。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐