对象映射工具 Mapperly

对象映射工具 Mapperly

Mapperly 是一个用于生成对象映射的 .NET 源生成器
以前常用的 AutoMapper 现在开始收费了

dotnet add package Riok.Mapperly

https://www.cnblogs.com/jasongrass/p/18746203#mapperly

在 csproj 中,建议将如下两个警告,设置成 Error。 可能还有其它的警告也最好设置成 Error。它会提示有哪些属性没有显示进行映射。

<WarningsAsErrors>RMG012;RMG020</WarningsAsErrors>

使用例子

网上找的例子让 AI 做了注释

// 定义一个测试类,用于演示 Mapperly 自动生成映射代码的功能
public class AutoMapTest
{
    public static void DoTest()
    {
        // 创建一个 UserViewObject 实例作为源对象(通常用于视图层/前端交互)
        UserViewObject viewObject = new UserViewObject
        {
            Id = "1",
            Name = "张三",
            UserGender = Gender.Male,        // 枚举类型
            Birthday = new DateTime(1990, 1, 1),
            HomeAddress = "北京市",           // 源属性名为 HomeAddress
            Remark = "这是一个备注",          // 此字段在目标类型中不存在,将被忽略
        };

        // ✅ 关键:调用 Mapperly 自动生成的映射方法
        // Mapperly 在编译时为 `ToUserEntry` 方法生成具体实现,
        // 将 viewObject 映射为 UserEntry 实例(通常用于数据持久层/数据库实体)
        UserEntry entry = UserViewObjectMapper.ToUserEntry(viewObject);

        // 输出映射结果,验证字段是否正确转换
        Console.WriteLine(
            $"UserEntry: {entry.Id}, {entry.Name}, {entry.Gender}, {entry.Birthday}, {entry.Address}"
        );

        // 创建一个 UserEntry 实例(反向映射的源对象)
        UserEntry newEntry = new UserEntry
        {
            Id = "2",
            Name = "李四",
            Gender = 1, // 对应 Gender.Male(int → enum 需转换)
            Birthday = "1995-05-05", // 字符串格式日期
            CreateTime = DateTime.Now,
            UpdateTime = DateTime.Now,
            Address = "上海市", // 目标属性名为 HomeAddress
        };

        // ✅ 关键:调用反向映射方法(UserEntry → UserViewObject)
        // Mapperly 同样为 `ToUserViewObject` 生成实现,支持双向映射
        UserViewObject newViewObject = UserViewObjectMapper.ToUserViewObject(newEntry);
        Console.WriteLine(
            $"UserViewObject: {newViewObject.Id}, {newViewObject.Name}, {newViewObject.UserGender}, {newViewObject.Birthday}, {newViewObject.HomeAddress}"
        );
    }
}

// 🧠【Mapperly 核心配置】
// 使用 `[Mapper]` 特性标记该类为 Mapperly 的映射器(Mapper)
// Mapperly 会在编译时扫描该类中的 `partial` 方法,并根据特性生成具体实现
[Riok.Mapperly.Abstractions.Mapper(
    UseDeepCloning = true,                         // 启用深拷贝(对复杂引用类型递归复制,避免共享引用)
    AutoUserMappings = false,                      // 禁用自动用户定义类型映射(强制显式配置,提高可控性)
    ThrowOnMappingNullMismatch = true,             // 源对象为 null 但目标非 nullable 时抛异常(增强健壮性)
    ThrowOnPropertyMappingNullMismatch = true,     // 源属性为 null 但目标属性非 nullable 时抛异常
    EnabledConversions =                           // 允许的隐式/显式类型转换
        MappingConversionType.ExplicitCast |       // 如 (int)enum
        MappingConversionType.ImplicitCast         // 如 string → object
)]
public partial class UserViewObjectMapper
{
    // 🔁 【正向映射:UserViewObject → UserEntry】
    // ⚠️ 注意:所有映射方法必须声明为 `partial`,由 Mapperly 在编译时生成具体实现

    // [MapProperty]:显式指定属性映射关系(当源与目标属性名不一致时必需)
    [MapProperty(nameof(UserViewObject.HomeAddress), nameof(UserEntry.Address))] // HomeAddress → Address

    // 自定义转换:UserGender(enum)→ Gender(int),通过 `Use = nameof(...)` 指定转换函数
    [MapProperty(
        nameof(UserViewObject.UserGender),
        nameof(UserEntry.Gender),
        Use = nameof(ToIntegerGender)   // 调用下方定义的 ToIntegerGender 方法
    )]

    // 自定义转换:Birthday(DateTime)→ Birthday(string),格式化为 "yyyy-MM-dd"
    [MapProperty(
        nameof(UserViewObject.Birthday),
        nameof(UserEntry.Birthday),
        Use = nameof(ToBirthdayString)  // 调用 ToBirthdayString
    )]

    // [MapperIgnoreSource]:忽略源对象的某个属性(即使目标有同名字段也不映射)
    [MapperIgnoreSource(nameof(UserViewObject.Remark))] // Remark 不参与映射(UserEntry 无此字段)

    // [MapperIgnoreTarget]:忽略目标对象的某些属性(即使源有值也不赋值)
    [MapperIgnoreTarget(nameof(UserEntry.CreateTime))]  // 不设置 CreateTime(通常由数据库生成)
    [MapperIgnoreTarget(nameof(UserEntry.UpdateTime))]  // 同上

    // ✅ 声明映射方法(无实现),Mapperly 自动生成:
    // - 复制 Id、Name(同名,自动映射)
    // - 调用 ToIntegerGender 处理 Gender
    // - 调用 ToBirthdayString 处理 Birthday
    // - 忽略 Remark / CreateTime / UpdateTime
    public static partial UserEntry ToUserEntry(UserViewObject vo);

    # 自动支持数组
    # 注: 实测数据不行,反编译出来的代码中它没有自动进行映射
    public static partial List<UserEntry> ToUserEntries(List<UserViewObject> vos);

    // 🔁 【反向映射:UserEntry → UserViewObject】

    [MapProperty(nameof(UserEntry.Address), nameof(UserViewObject.HomeAddress))] // Address → HomeAddress

    // 自定义转换:Gender(int)→ UserGender(enum)
    [MapProperty(
        nameof(UserEntry.Gender),
        nameof(UserViewObject.UserGender),
        Use = nameof(ToGender)  // 调用 ToGender 方法
    )]

    // 自定义转换:Birthday(string)→ Birthday(DateTime)
    [MapProperty(
        nameof(UserEntry.Birthday),
        nameof(UserViewObject.Birthday),
        Use = nameof(ToBirthdayDatetime)  // 调用 ToBirthdayDatetime
    )]

    // 忽略 UserEntry 中的审计字段(CreateTime/UpdateTime),它们在 ViewObject 中无对应字段
    [MapperIgnoreSource(nameof(UserEntry.CreateTime))]
    [MapperIgnoreSource(nameof(UserEntry.UpdateTime))]

    // 忽略目标对象 UserViewObject 的 Remark 字段(不从源赋值——虽然源本就没有)
    [MapperIgnoreTarget(nameof(UserViewObject.Remark))]

    // ✅ 反向映射方法(Mapperly 生成实现)
    public static partial UserViewObject ToUserViewObject(UserEntry entry);

// 定义一个后置处理函数
[AfterMapping]
private void AfterMapUser(UserViewObject source, UserEntry target)
{
        // 这里的逻辑就是你想要的 "When"
        if (IsAdult(source))
        {
            target.Birthday = source.Birthday;
        }
        else
        {
            // 如果需要,可以设为默认值
            target.Birthday = default; 
        }
    }

/// <summary>
    /// 【前置处理函数】: 在 Mapperly 自动映射开始之前运行
    /// </summary>
    /// <param name="source">源对象 (UserViewObject)</param>
    /// <param name="target">目标对象 (UserEntry)</param>
    [BeforeMapping]
    private void PreProcessUserEntry(UserViewObject source, UserEntry target)
    {
        // 1. 在映射开始前,设置一个默认值或初始化目标对象
        target.CreateTime = DateTime.UtcNow;

        // 2. 清理目标字段,确保它不会从源对象的某个同名字段意外映射过来
        target.Description = string.Empty; 

        // 3. 可以在这里进行全局的空值检查或日志记录
        if (source == null)
        {
            throw new ArgumentNullException(nameof(source));
        }
    }
}

## 填充, 将 source 值填充至 target
public static partial void ApplyUpdate(UserViewObject source, UserEntry target);

## 同填充,会编译为 UserViewObject 的扩展函数
public static partial void ApplyUpdate([MappingTarget] this UserViewObject source, UserEntry target);

    // 🛠️ 【自定义转换函数】—— Mapperly 在生成代码时会内联调用这些方法

    // 将 Gender 枚举转为整数(用于存入数据库或传输)
    private static int ToIntegerGender(Gender gender)
    {
        return (int)gender;  // 简单 cast,也可加校验
    }

    // 将整数转回 Gender 枚举(带范围校验,防止非法值)
    private static Gender ToGender(int gender)
    {
        return gender switch
        {
            0 => Gender.Unknown,
            1 => Gender.Male,
            2 => Gender.Female,
            _ => throw new ArgumentOutOfRangeException(nameof(gender), $"Invalid gender value: {gender}"),
        };
    }

    // DateTime → string(标准化格式,避免时区/精度问题)
    private static string ToBirthdayString(DateTime birthday)
    {
        return birthday.ToString("yyyy-MM-dd"); // 推荐使用 CultureInfo.InvariantCulture 更健壮
    }

    // string → DateTime(需注意异常处理;生产环境建议用 TryParse)
    private static DateTime ToBirthdayDatetime(string date)
    {
        // ⚠️ 注意:DateTime.Parse 在无效格式时会抛 FormatException
        // 建议改进为:
        // if (!DateTime.TryParseExact(date, "yyyy-MM-dd", null, DateTimeStyles.None, out var dt))
        //     throw new ArgumentException($"Invalid date format: {date}", nameof(date));
        // return dt;
        return DateTime.Parse(date);
    }
}

其它


## 禁止属性对象中循环引用
UseReferenceHandling
开销会有点大

构造参数名称匹配

直接使用字符串名称

(待测试)

## 其实就是将 nameof 取名称直接使用字符串

public class Source { public int Age { get; set; } }

public class Target
{
    public int Val { get; }
    // 构造函数参数名叫 "val",源属性叫 "Age"
    public Target(int val) => Val = val;
}

[Mapper]
public partial class MyMapper
{
    // 告诉 Mapperly:把 Source.Age 映射给 Target 的构造函数参数 "val"
    [MapProperty(nameof(Source.Age), "val")] 
    public partial Target ToTarget(Source s);
}

扁平化映射

映射源属性为对象中的某个值到目标对象的属性。 使用字符串,直接写全路径

(待测试)

1. 使用点号路径 "Config.PrimaryAddress"
1. 安全一点可以使用 BeforeMapping 将属性先初始化一下,防止出现 null 引用的情况

[Mapper]
public partial class UserMapper
{
    // 关键语法:使用点号路径 "Config.PrimaryAddress"
    [MapProperty(
        nameof(UserViewObject.HomeAddress), // 源属性 (string)
        "Config.PrimaryAddress"             // 目标路径 (string)
    )]
    public partial UserEntry ToUserEntry(UserViewObject vo);
}
上一篇
下一篇