对象映射工具 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);
}