C# 对象映射框架(Mapster & mapperly)
Mapster
可能是 .NET 领域性能最好的对象映射框架 —— Mapster - 明志唯新
MapsterMapper/Mapster: A fast, fun and stimulating object to object Mapper
// 自定义转换配置
public class MyMappingConfig
{
public static void ConfigureMappings()
{
// UserViewObject 到 UserEntry 的映射配置
TypeAdapterConfig<UserViewObject, UserEntry>
.NewConfig()
.Map(dest => dest.Gender, src => (int)src.UserGender) // Gender 枚举转换为 int
.Map(dest => dest.Birthday, src => src.Birthday.ToString("yyyy-MM-dd")) // DateTime 转换为字符串
.Map(dest => dest.Address, src => src.HomeAddress); // HomeAddress 映射到 Address
// UserEntry 到 UserViewObject 的映射配置
TypeAdapterConfig<UserEntry, UserViewObject>
.NewConfig()
.Map(dest => dest.UserGender, src => (Gender)src.Gender) // int 转换回 Gender 枚举
.Map(dest => dest.Birthday, src => DateTime.Parse(src.Birthday)); // 字符串转换为 DateTime
}
}
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 = "北京市",
Remark = "这是一个备注",
};
// 将 UserViewObject 转换为 UserEntry
UserEntry entry = viewObject.Adapt<UserEntry>();
Console.WriteLine(
$"UserEntry: {entry.Id}, {entry.Name}, {entry.Gender}, {entry.Birthday}, {entry.Address}"
);
// 创建示例 UserEntry 对象
UserEntry newEntry = new UserEntry
{
Id = "2",
Name = "李四",
Gender = 1, // 男性
Birthday = "1995-05-05",
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
Address = "上海市",
};
// 将 UserEntry 转换为 UserViewObject
UserViewObject newViewObject = newEntry.Adapt<UserViewObject>();
Console.WriteLine(
$"UserViewObject: {newViewObject.Id}, {newViewObject.Name}, {newViewObject.UserGender}, {newViewObject.Birthday}, {newViewObject.HomeAddress}"
);
}
}
public enum Gender
{
Unknown,
Male,
Female,
}
public class UserViewObject
{
public string Id { get; set; }
public string Name { get; set; }
public Gender UserGender { get; set; }
public DateTime Birthday { get; set; }
public string HomeAddress { get; set; }
public string Remark { get; set; }
}
public class UserEntry
{
public string Id { get; set; }
public string Name { get; set; }
public int Gender { get; set; }
public string Birthday { get; set; }
public DateTime CreateTime { get; set; }
public DateTime UpdateTime { get; set; }
public string Address { get; set; }
}
Mapster 和 AutoMapper 等“传统”映射框架的问题
自动的转换,让你失去了对变化的感知和追踪,比如有天某个 Model 的名称修改了,然后忘记了在转换配置中进行自定义处理,就可能留有隐患。
我比较推荐的是使用源码生成的方式,并且充分控制转换方式。Mapster 有 CodeGen 的配置方式,但是感觉有点复杂了。真正的源码生成形式,还在路上。
Source Generators with IMapper Interface · Issue #622 · MapsterMapper/Mapster
发现 mapperly 可以符合需求
mapperly
riok/mapperly: A .NET source generator for generating object mappings. No runtime reflection.
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 = "北京市",
Remark = "这是一个备注",
};
// 将 UserViewObject 转换为 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, // 男性
Birthday = "1995-05-05",
CreateTime = DateTime.Now,
UpdateTime = DateTime.Now,
Address = "上海市",
};
// 将 UserEntry 转换为 UserViewObject
UserViewObject newViewObject = UserViewObjectMapper.ToUserViewObject(newEntry);
Console.WriteLine(
$"UserViewObject: {newViewObject.Id}, {newViewObject.Name}, {newViewObject.UserGender}, {newViewObject.Birthday}, {newViewObject.HomeAddress}"
);
}
}
[Riok.Mapperly.Abstractions.Mapper(
UseDeepCloning = true,
AutoUserMappings = false,
ThrowOnMappingNullMismatch = true,
ThrowOnPropertyMappingNullMismatch = true,
EnabledConversions = MappingConversionType.ExplicitCast | MappingConversionType.ImplicitCast
)]
public partial class UserViewObjectMapper
{
[MapProperty(nameof(UserViewObject.HomeAddress), nameof(UserEntry.Address))] // Map property with a different name in the target type
[MapProperty(
nameof(UserViewObject.UserGender),
nameof(UserEntry.Gender),
Use = nameof(ToIntegerGender)
)]
[MapProperty(
nameof(UserViewObject.Birthday),
nameof(UserEntry.Birthday),
Use = nameof(ToBirthdayString)
)]
[MapperIgnoreSource(nameof(UserViewObject.Remark))]
[MapperIgnoreTarget(nameof(UserEntry.CreateTime))]
[MapperIgnoreTarget(nameof(UserEntry.UpdateTime))]
public static partial UserEntry ToUserEntry(UserViewObject vo);
[MapProperty(nameof(UserEntry.Address), nameof(UserViewObject.HomeAddress))]
[MapProperty(
nameof(UserEntry.Gender),
nameof(UserViewObject.UserGender),
Use = nameof(ToGender)
)]
[MapProperty(
nameof(UserEntry.Birthday),
nameof(UserViewObject.Birthday),
Use = nameof(ToBirthdayDatetime)
)]
[MapperIgnoreSource(nameof(UserEntry.CreateTime))]
[MapperIgnoreSource(nameof(UserEntry.UpdateTime))]
[MapperIgnoreTarget(nameof(UserViewObject.Remark))]
public static partial UserViewObject ToUserViewObject(UserEntry entry);
private static int ToIntegerGender(Gender gender)
{
return (int)gender;
}
private static Gender ToGender(int gender)
{
return gender switch
{
0 => Gender.Unknown,
1 => Gender.Male,
2 => Gender.Female,
_ => throw new ArgumentOutOfRangeException(nameof(gender)),
};
}
private static string ToBirthdayString(DateTime birthday)
{
return birthday.ToString("yyyy-MM-dd");
}
private static DateTime ToBirthdayDatetime(string date)
{
return DateTime.Parse(date);
}
}
<WarningsAsErrors>RMG012;RMG020</WarningsAsErrors>
这里有两个重要的配置,
1 EnabledConversions = MappingConversionType.ExplicitCast | MappingConversionType.ImplicitCast
这个配置是说,处理明确存在显式转换和隐式转换,其它的都不进行自动处理。
比如 Datetime 和 string 之间,如果没有手动设置,就会按照默认的方式进行相互转换。
EnabledConversions 设置为仅支持 ExplicitCast 和 ImplicitCast 之后,则不会存在这种“偷偷摸摸”的默认操作。
2 在 csproj 中,将如下两个警告,设置成 Error。
可能还有其它的警告也最好设置成 Error。
<WarningsAsErrors>RMG012;RMG020</WarningsAsErrors>
这样做的好处是,对于任何两个 Model 之间的差异,都必须手动处理,或者手动声明不处理,不会存在任何的模糊空间。
设想如下场景,本来两个 Model 中,属性名称都是 Name, 类型也都是 string, 则默认转换会处理得很好。
如果哪天其中一个重命名成了 FullName, 则会有上述警告,告知有转换操作没有进行。
但是,警告是容易被忽略的,一不下心就留下了 BUG 隐患。
如果是 Error, 则在修改属性命名之后,编译会无法通过,强力地提示此处存在问题。
总结
在 C# 中做对象映射时,推荐使用 mapperly 这类使用源码生成器来处理转换的工具库。
并结合配置,将其设置为对于任何差异都需要手动确认的配置形式,并使用编译时错误来进行约束。