1. 项目背景与核心痛点在C#后端开发中我们经常遇到两种截然不同的数据类定义方式一种是开发人员手动编写的传统数据类Hand-written Data Class另一种是通过Protocol Buffersprotobuf的.proto文件自动生成的类Protoc-generated Class。这两种数据类虽然本质上都是用来承载数据的但在实际项目中混用时却会带来诸多不便。我最近在一个微服务改造项目中就深刻体会到了这种痛苦。旧系统使用的是典型的三层架构数据访问层返回的都是手工编写的POCO类而新引入的gRPC服务则全部采用protobuf协议自动生成的类充斥着各种GeneratedCode特性。当我们需要在两种数据类之间转换时不得不编写大量重复的赋值代码// 手工数据类 public class User { public int Id { get; set; } public string Name { get; set; } public DateTime CreateTime { get; set; } } // Protobuf生成类 public partial class UserProto { [global::ProtoBuf.ProtoMember(1)] public int Id { get; set; } [global::ProtoBuf.ProtoMember(2)] public string Name { get; set; } [global::ProtoBuf.ProtoMember(3)] public global::Google.Protobuf.WellKnownTypes.Timestamp CreateTime { get; set; } } // 转换代码 var user new User(); var proto new UserProto { Id user.Id, Name user.Name, CreateTime Timestamp.FromDateTime(user.CreateTime) // 更多字段... };这种手动转换不仅枯燥乏味而且当字段数量达到几十个时极容易出错。更麻烦的是protobuf的特殊类型如Timestamp、ByteString等与.NET原生类型之间的转换还需要特殊处理。2. 解决方案设计思路2.1 技术选型对比针对这类问题社区常见的解决方案主要有以下几种手动映射直接编写转换代码优点完全可控性能最佳缺点维护成本高字段变更时需要同步修改反射缓存运行时动态映射属性优点自动化程度高缺点首次运行性能较差复杂类型处理不便表达式树编译动态生成转换委托优点接近手写代码的性能缺点实现复杂度高Source Generator编译时生成转换代码优点无运行时开销类型安全缺点需要.NET 5调试较困难经过评估我选择了表达式树编译方案因为项目使用的是.NET Core 3.1无法使用Source Generator需要处理大量DTO转换性能是关键考量支持复杂类型的特殊转换逻辑2.2 核心架构设计解决方案的核心组件包括类型发现系统自动识别可转换的类型对表达式树编译器生成高效的转换委托特殊类型处理器处理DateTime/Timestamp等特殊转换缓存机制避免重复编译的开销graph TD A[输入类型对] -- B{是否已缓存?} B --|是| C[返回缓存委托] B --|否| D[分析成员映射] D -- E[构建表达式树] E -- F[编译为委托] F -- G[存入缓存] G -- C注意实际实现时应考虑线程安全问题特别是在并发场景下构建和访问缓存。3. 核心实现细节3.1 表达式树构建表达式树的核心思想是将转换逻辑表示为数据结构然后编译为可执行的委托。以下是一个简化版的实现public static FuncTSource, TTarget CreateMapperTSource, TTarget() { var sourceParam Expression.Parameter(typeof(TSource), source); var variableExprs new ListExpression(); var variableMap new Dictionarystring, ParameterExpression(); // 创建目标实例 var targetVar Expression.Variable(typeof(TTarget), target); variableExprs.Add(Expression.Assign(targetVar, Expression.New(typeof(TTarget)))); // 遍历属性进行映射 foreach (var srcProp in typeof(TSource).GetProperties()) { var targetProp typeof(TTarget).GetProperty(srcProp.Name); if (targetProp null || !targetProp.CanWrite) continue; // 获取源值 var srcValue Expression.Property(sourceParam, srcProp); // 类型转换处理 Expression valueExpr ConvertExpression(srcValue, targetProp.PropertyType); // 赋值操作 variableExprs.Add(Expression.Assign( Expression.Property(targetVar, targetProp), valueExpr )); } // 构建最终表达式 variableExprs.Add(targetVar); var body Expression.Block(new[] { targetVar }, variableExprs); return Expression.LambdaFuncTSource, TTarget(body, sourceParam).Compile(); } private static Expression ConvertExpression(Expression srcExpr, Type targetType) { // 处理DateTime - Timestamp if (srcExpr.Type typeof(DateTime) targetType typeof(Timestamp)) { var method typeof(Timestamp).GetMethod(FromDateTime); return Expression.Call(method, srcExpr); } // 其他类型转换... return Expression.Convert(srcExpr, targetType); }3.2 性能优化技巧缓存编译结果使用ConcurrentDictionary存储已编译的委托private static readonly ConcurrentDictionary(Type, Type), Delegate _cache new ConcurrentDictionary(Type, Type), Delegate();避免重复反射缓存PropertyInfo等元数据private static readonly ConcurrentDictionaryType, PropertyInfo[] _propertyCache new ConcurrentDictionaryType, PropertyInfo[]();支持批量转换为集合类型提供专门优化public static ListTTarget MapListTSource, TTarget(IEnumerableTSource sources) { var mapper GetOrCreateMapperTSource, TTarget(); var result new ListTTarget(sources.Count()); foreach (var item in sources) result.Add(mapper(item)); return result; }4. 高级功能实现4.1 自定义映射规则有时我们需要覆盖默认的映射行为可以通过特性标注来实现[AttributeUsage(AttributeTargets.Property)] public class MapWithAttribute : Attribute { public Type ConverterType { get; } public MapWithAttribute(Type converterType) { ConverterType converterType; } } // 使用示例 public class UserDto { [MapWith(typeof(PhoneNumberConverter))] public string Phone { get; set; } } // 在表达式构建时检查该特性 var mapWithAttr targetProp.GetCustomAttributeMapWithAttribute(); if (mapWithAttr ! null) { var converter Activator.CreateInstance(mapWithAttr.ConverterType); var convertMethod mapWithAttr.ConverterType.GetMethod(Convert); valueExpr Expression.Call(Expression.Constant(converter), convertMethod, srcValue); }4.2 嵌套对象处理对于复杂对象图我们需要递归处理嵌套属性private static Expression HandleComplexType(Expression srcExpr, Type targetType) { // 如果是值类型或字符串直接返回 if (targetType.IsValueType || targetType typeof(string)) return srcExpr; // 如果是集合类型处理元素类型 if (typeof(IEnumerable).IsAssignableFrom(targetType)) { // 处理集合转换... } // 递归处理对象属性 var mapper GetOrCreateMapper(srcExpr.Type, targetType); return Expression.Invoke(Expression.Constant(mapper), srcExpr); }5. 性能对比测试为了验证方案的性能我使用BenchmarkDotNet进行了测试方法平均值误差分配内存手动映射19.2ns0.5ns0 B表达式树22.7ns0.3ns0 B反射(带缓存)45.6ns1.2ns0 B反射(无缓存)320.5ns8.7ns104 B测试结果表明表达式树方案性能接近手写代码比反射方案快2-15倍无内存分配适合高性能场景6. 实际应用中的经验6.1 常见问题排查字段不匹配当遇到映射失败时首先检查属性名称是否一致包括大小写是否为可写属性setter可访问复杂类型的嵌套属性是否需要特殊处理循环引用问题当对象图存在循环引用时需要// 在构建映射器时检测循环引用 var visited new HashSetType(); CheckCircularReference(typeof(TSource), visited); private static void CheckCircularReference(Type type, HashSetType visited) { if (visited.Contains(type)) throw new InvalidOperationException(循环引用); visited.Add(type); foreach (var prop in type.GetProperties()) { if (prop.PropertyType.IsClass prop.PropertyType ! typeof(string)) CheckCircularReference(prop.PropertyType, new HashSetType(visited)); } }6.2 性能调优技巧预热缓存在应用启动时预编译常用类型的映射器public static void WarmUp(params (Type, Type)[] typePairs) { Parallel.ForEach(typePairs, pair { GetOrCreateMapper(pair.Item1, pair.Item2); }); }选择性映射对于大型对象可以指定只映射部分属性public static FuncTSource, TTarget CreateSelectiveMapperTSource, TTarget( params string[] propertyNames) { // 只处理指定的属性... }AOT兼容性如果项目需要Native AOT编译考虑使用预生成技术如Source Generator提供fallback的反射实现7. 替代方案比较虽然表达式树方案在大多数场景表现良好但根据项目需求其他方案可能更合适AutoMapper优点功能全面社区支持好缺点运行时反射性能较差适用场景快速开发对性能要求不高Mapster优点支持编译时映射生成缺点配置较复杂适用场景需要平衡性能和开发效率Protobuf-net优点原生支持protobuf类型缺点仅限于protobuf相关场景适用场景纯protobuf项目在我的实际项目中最终采用了混合策略核心路径使用表达式树实现边缘场景使用Mapster作为fallback特殊协议处理使用Protobuf-net的扩展方法这种分层架构在保证性能的同时也提供了足够的灵活性。
阅读完成 · 觉得有帮助?