可能是 .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; }}
自动的转换,让你失去了对变化的感知和追踪,比如有天某个 Model 的名称修改了,然后忘记了在转换配置中进行自定义处理,就可能留有隐患。
我比较推荐的是使用源码生成的方式,并且充分控制转换方式。Mapster 有 CodeGen 的配置方式,但是感觉有点复杂了。真正的源码生成形式,还在路上。
Source Generators with IMapper Interface · Issue #622 · MapsterMapper/Mapster
发现 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 这类使用源码生成器来处理转换的工具库。 并结合配置,将其设置为对于任何差异都需要手动确认的配置形式,并使用编译时错误来进行约束。
原文链接: https://blog.jgrass.cc/posts/csharp-model-mapper/
本作品采用 「署名 4.0 国际」 许可协议进行许可,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。