C#使用反射,特性,表达式树仿写AutoMapper

在日常开发中经常会遇到对象之间的映射场景(比如 DTO 与实体类、ViewModel 与 Model 的转换),市面上有着非常成熟的 AutoMapper作为映射工具。在了解了AutoMapper的构成后,准备根据我自己最近学习的内容仿写一个AutoMapper作为近期深入学习的成功。我从零开始实现了一个轻量级的对象映射库 :MyMapper。文章下面简单介绍一下我开发这个库的思路、核心实现,以及过程中对反射、自定义特性、表达式树(编译树) 等核心知识点的理解与实践。欢迎一起学习。

GitHub地址:https://github.com/2825077535/MyMapper.git

一、前言

在项目中频繁使用 AutoMapper 做对象映射时,我慢慢开始取了解对象映射的底层的实现形式? 比如如何通过反射匹配属性、如何处理类型转换、如何自定义忽略某些字段?了解完后,打算动手造一个 "轻量级轮子",刚好也复习一下最近深入学习的一些知识,例如反射、泛型、自定义特性、表达式树等

MyMapper 的核心目标很明确:

支持基础的对象属性映射(同名称、兼容类型的属性自动赋值);

支持自定义忽略指定属性(通过特性);

支持自定义类型转换(比如 string 转 int、DateTime 转 string);

支持条件映射(满足指定条件才映射属性);

兼顾易用性与性能(通过表达式树优化反射性能);

二、MyMapper 的核心架构

先贴一下我的仓库目录结构,每个文件都有明确的职责,这也是我学习 "单一职责原则" 的实践:

MyMapper/

├── DefaultsRegion.cs // 全局默认配置(比如默认类型转换器)

├── Interfaces.cs // 抽象接口(比如类型转换器接口、映射条件接口)

├── MapperIgnoreAttribute.cs// 自定义特性:标记需要忽略的属性

├── MappingConfiguration.cs // 映射配置类:管理源类型→目标类型的映射规则

├── MiniMapper.cs // 核心入口类:对外提供Map方法(含反射/表达式树实现)

├── MiniMapperException.cs // 自定义异常:统一处理映射过程中的异常

├── Model.cs // 测试用的示例模型(也可作为用户使用的参考)

├── PropertyMapCondition.cs // 条件映射:定义属性映射的判断条件

├── TypeConverterRegistry.cs// 类型转换器注册中心:管理自定义类型转换逻辑

├── TypeMappingConfig.cs // 单个类型对的映射配置(比如源Type→目标Type的规则)

三、主要内容点

1. 反射:对象映射的基础

(1)反射是什么?

反射是 C# 提供的一项能力,允许程序在运行时获取类型(Type)的信息(比如属性、方法、构造函数),并动态操作这些成员(比如读取属性值、调用方法、创建实例)。简单来说,反射让代码能够 "自我审视" 和 "动态操作",这也是对象映射库的底层核心 ------ 因为映射需要在运行时适配任意的源类型和目标类型,而非硬编码。当然反射最大的问题还是性能比较差,每一次反射的应用都是执行动态解析的过程,所有在这里还引用表达式树的概念来优化计算逻辑。

(2)反射在 MyMapper 中的核心应用

在 MyMapper 中,反射主要解决 3 个问题:

获取源类型 / 目标类型的属性列表;

读取源对象的属性值、给目标对象的属性赋值;

动态创建目标对象的实例(Activator.CreateInstance本质也是基于反射)。

(3)核心代码与解析

以 遍历属性并赋值 为例,结合代码解释反射的关键 API:

csharp 复制代码
// 核心映射逻辑(反射版)
public static TTarget Map<TSource, TTarget>(TSource source)
{
    if (source == null)
        throw new MiniMapperException("源对象不能为空");

    Type sourceType = typeof(TSource); // 获取源类型的Type对象(反射入口)
    Type targetType = typeof(TTarget);

    // 反射创建目标对象实例(无参构造函数)
    TTarget target = (TTarget)Activator.CreateInstance(targetType);

    // 反射获取目标类型的所有公共实例属性
    PropertyInfo[] targetProps = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance);
    foreach (var targetProp in targetProps)
    {
        // 反射检查属性是否标记了忽略特性(后续讲自定义特性)
        if (targetProp.IsDefined(typeof(MapperIgnoreAttribute), false))
            continue;

        //  反射查找源类型中同名的属性
        PropertyInfo sourceProp = sourceType.GetProperty(
            targetProp.Name, 
            BindingFlags.Public | BindingFlags.Instance // 指定查找范围:公共实例属性
        );
        if (sourceProp == null) continue; // 源类型无同名属性,跳过

        //  反射读取源属性值
        object sourceValue = sourceProp.GetValue(source); 
        //  反射给目标属性赋值(先处理类型转换)
        object targetValue = TypeConverterRegistry.Convert(sourceValue, sourceProp.PropertyType, targetProp.PropertyType);
        targetProp.SetValue(target, targetValue);
    }

    return target;
}

(4)反射的优缺点

优点:灵活、通用,能适配任意类型的映射,无需提前硬编码;

缺点:性能较差(因为反射绕开了编译期优化,且每次都要重新获取 Type/PropertyInfo),可读性一般。

2. 自定义特性

(1)自定义特性是什么?

特性(Attribute)是 C# 的一种 "元数据" 机制,允许我们给类、属性、方法等成员添加 "附加信息",并在运行时通过反射读取这些信息。自定义特性则是自己根据业务需求定义的、专属的元数据标记 ------ 比如 MyMapper 中的[MapperIgnore],就是用来标记 "映射时需要忽略的属性"。

特性的本质是继承自Attribute基类的类,通过AttributeUsage特性可以指定其适用范围(比如只能标记在属性上)。

(2)自定义特性的实现与解析

第一步:定义自定义特性

csharp 复制代码
// 标记该特性只能用于属性,且一个属性只能标记一次
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class MapperIgnoreAttribute : Attribute
{
    // 特性可以无逻辑,仅作为"标记";也可以添加参数(比如自定义忽略原因)
    // 示例:带参数的特性
    // public string Reason { get; }
    // public MapperIgnoreAttribute(string reason) => Reason = reason;
}

第二步:在模型中标记特性

csharp 复制代码
public class UserEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    [MapperIgnore] // 标记该属性需要忽略
    // [MapperIgnore("密码不参与DTO映射")] // 带参数的用法
    public string Password { get; set; }
}

第三步:运行时通过反射解析特性

csharp 复制代码
// 遍历目标属性时,解析是否标记了MapperIgnoreAttribute
foreach (var targetProp in targetProps)
{
    //反射检查属性是否标记了指定特性
    bool isIgnored = targetProp.IsDefined(typeof(MapperIgnoreAttribute), false);
    if (isIgnored) continue; // 忽略该属性的映射
    
    // 扩展:读取特性的参数
    // var ignoreAttr = (MapperIgnoreAttribute)targetProp.GetCustomAttribute(typeof(MapperIgnoreAttribute), false);
    // Console.WriteLine($"忽略{targetProp.Name}的原因:{ignoreAttr.Reason}");
}

(3)特性的核心价值

自定义特性让映射规则从 "硬编码" 变成 "声明式配置",开发者只需在模型上添加标记,无需修改映射库的核心逻辑,契合 "开闭原则"

3. 表达式树(Expression Tree):优化反射的性能瓶颈

(1)表达式树是什么?

表达式树(是 C# 中表示 "代码逻辑" 的数据结构,它将代码(比如 lambda 表达式)转换成一棵可遍历、可修改、可编译的 "树状结构",而非直接执行。简单来说:

普通 lambda:x => x.Id + 1 → 直接编译成 IL 代码执行;

表达式树:Expression<Func<T, int>> expr = x => x.Id + 1 → 表示 "取 x 的 Id 加 1" 的逻辑结构,而非直接执行。

表达式树的核心价值:可以将 "动态逻辑"(比如反射的属性赋值)转换成 "编译后的委托",执行效率接近硬编码,从而解决反射的性能问题。

(2)为什么用表达式树优化 MyMapper?

反射的性能瓶颈在于 "每次映射都要重新获取 PropertyInfo、每次赋值都要调用GetValue/SetValue",而表达式树的思路是:

第一次映射某类型对(比如UserEntity→UserDTO)时,通过表达式树动态构建 "属性赋值的委托";

缓存这个委托,后续映射该类型对时直接调用委托,无需再走反射;

委托是编译后的代码,执行效率远高于反射。

本质上就是编写一个字典委托,将执行过的函数内容保存在字典中,通过O(1)的查询速度去执行函数,跳过动态解析的过程。

(3)核心实现:表达式树优化映射逻辑

以下是简化版的表达式树实现:

csharp 复制代码
// 缓存编译后的映射委托:Key=源类型+目标类型,Value=映射委托
private static readonly Dictionary<(Type, Type), Delegate> _mapDelegateCache = new Dictionary<(Type, Type), Delegate>();

public static TTarget Map<TSource, TTarget>(TSource source)
{
    if (source == null)
        throw new MiniMapperException("源对象不能为空");

    var key = (typeof(TSource), typeof(TTarget));
    // 优先从缓存获取编译后的委托
    if (!_mapDelegateCache.TryGetValue(key, out var del))
    {
        // 缓存未命中,构建表达式树并编译
        del = BuildMapDelegate<TSource, TTarget>();
        _mapDelegateCache[key] = del;
    }

    // 执行委托完成映射
    return ((Func<TSource, TTarget>)del)(source);
}

// 构建映射委托的核心方法(表达式树)
private static Func<TSource, TTarget> BuildMapDelegate<TSource, TTarget>()
{
    // 定义参数:source(TSource类型)
    ParameterExpression sourceParam = Expression.Parameter(typeof(TSource), "source");

    // 定义目标对象的变量:target = new TTarget()
    ParameterExpression targetVar = Expression.Variable(typeof(TTarget), "target");
    Expression createTarget = Expression.Assign(
        targetVar, 
        Expression.New(typeof(TTarget)) // 表达式树创建TTarget实例(new TTarget())
    );

    //构建所有属性赋值的表达式
    List<Expression> assignExpressions = new List<Expression>();
    assignExpressions.Add(createTarget);

    Type sourceType = typeof(TSource);
    Type targetType = typeof(TTarget);

    foreach (var targetProp in targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
    {
        // 跳过忽略的属性(逻辑同反射版)
        if (targetProp.IsDefined(typeof(MapperIgnoreAttribute), false))
            continue;

        var sourceProp = sourceType.GetProperty(targetProp.Name);
        if (sourceProp == null) continue;

        // 构建:target.TargetProp = (TargetType)source.SourceProp
        //读取源属性值:source.SourceProp
        MemberExpression sourceValueExpr = Expression.Property(sourceParam, sourceProp);
        // 类型转换(适配不同类型,比如string→int)
        UnaryExpression convertExpr = Expression.Convert(sourceValueExpr, targetProp.PropertyType);
        //  给目标属性赋值:target.TargetProp = 转换后的值
        BinaryExpression assignExpr = Expression.Assign(
            Expression.Property(targetVar, targetProp), 
            convertExpr
        );

        assignExpressions.Add(assignExpr);
    }

    // 构建返回语句:return target
    assignExpressions.Add(targetVar);

    // 整合所有表达式,编译成委托
    BlockExpression block = Expression.Block(
        new[] { targetVar }, // 声明变量
        assignExpressions.ToArray() // 执行赋值+返回
    );

    // 将表达式树编译成委托(Func<TSource, TTarget>)
    return Expression.Lambda<Func<TSource, TTarget>>(block, sourceParam).Compile();
}

四、MyMapper 的完整使用示例

封装完核心逻辑后,使用方式非常简单,同时能体现反射、特性、表达式树的结合:

csharp 复制代码
// 1. 定义模型(使用自定义特性)
public class UserEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    [MapperIgnore] // 映射时忽略
    public string Password { get; set; }
    
    public DateTime CreateTime { get; set; } // DateTime类型
}

public class UserDTO
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string CreateTime { get; set; } // string类型(需要类型转换)
}

// 2. 注册映射配置(含条件映射)
MiniMapper.CreateMap<UserEntity, UserDTO>(config =>
{
    // 条件映射:只有Id>0时才映射Name属性
    config.MapPropertyWhen(nameof(UserDTO.Name), source => source.Id > 0);
});

// 3. 注册自定义类型转换器(DateTime→string)
TypeConverterRegistry.RegisterConverter<DateTime, string>(dt => dt.ToString("yyyy-MM-dd"));

// 4. 执行映射(自动使用表达式树缓存优化)
var userEntity = new UserEntity
{
    Id = 1,
    Name = "张三",
    Password = "123456",
    CreateTime = DateTime.Now
};

UserDTO userDTO = MiniMapper.Map<UserEntity, UserDTO>(userEntity);
// 结果:
// userDTO.Id = 1
// userDTO.Name = "张三"(满足Id>0条件)
// userDTO.CreateTime = "2024-05-20"(类型转换)
// Password被忽略,未映射

五、总结

中间项目还提到关于依赖注入的使用,可以参考最近的另一篇博客,有讲解依赖注入的内容:

https://blog.csdn.net/m0_51559565/article/details/159018476?spm=1011.2415.3001.5331

现实中的AutoMapper会比我变得的Mapper要处理更多的内容。例如我在编写代码的时候,在处理循环嵌套引用时,我发现我自己的能力几乎无法正常的处理。另外表达式树其实在我们的日常使用中很少会使用到,主要集中到LINQ SQL,高性能对象映射也就是AutoMapper,动态构建,动态配置规则这些区域。这些内容在WPF的开发中触及并不多。

我自己编写一个简单的MyMapper学到的很多东西,欢迎一起学习。

相关推荐
快乐柠檬不快乐2 小时前
C++中的享元模式高级应用
开发语言·c++·算法
oem1102 小时前
C++与Docker集成开发
开发语言·c++·算法
似水明俊德2 小时前
11-C#
开发语言·c#
xushichao19892 小时前
C++中的享元模式
开发语言·c++·算法
fareast_mzh2 小时前
Mistral AI本地部署 C++无需Nvidiad独立显卡也能运行(CPU推理)
开发语言·c++·人工智能
Jackey_Song_Odd2 小时前
Part 1:Python语言核心 - Control Flow 控制流
开发语言·windows·python
m0_716667072 小时前
C++中的访问者模式高级应用
开发语言·c++·算法
大鹏说大话2 小时前
构建高并发缓存系统:架构设计、Redis策略与灾难防御
开发语言
Oueii2 小时前
C++中的访问者模式变体
开发语言·c++·算法