Loading

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 的名称修改了,然后忘记了在转换配置中进行自定义处理,就可能留有隐患。

程序员千万不要用AutoMapper或者Mapster_哔哩哔哩_bilibili

我比较推荐的是使用源码生成的方式,并且充分控制转换方式。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.

Introduction | 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 = "北京市",
            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 这类使用源码生成器来处理转换的工具库。
并结合配置,将其设置为对于任何差异都需要手动确认的配置形式,并使用编译时错误来进行约束。

posted @ 2025-03-02 14:53  J.晒太阳的猫  阅读(31)  评论(0编辑  收藏  举报