在日常开发中经常会遇到对象之间的映射场景(比如 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学到的很多东西,欢迎一起学习。