C#中如何基于Trainee record生成Manager record对象?
问题描述
现有C# record定义如下:
public abstract record Employee { public int EmployeeId { get; init; } } public record Manager : Employee { public string AdminCode { get; init; } } public record Trainee : Employee { public int ManagerId { get; init; } }
需要基于已有的Trainee对象创建Manager对象,无需手动复制基类Employee中的EmployeeId等字段。尝试过with表达式结合类型转换的方式无效(二次转换返回null)。
实际场景是用带init属性的不可变record作为JSON契约DTO,大量DTO共享基类字段,希望复用Trainee的初始化逻辑生成Manager,避免重复代码。
解决方案
方法1:封装基类字段映射逻辑
利用基类统一处理共享属性的复制,派生类只需补充自身独有的字段:
public abstract record Employee { public int EmployeeId { get; init; } // 封装基类属性的复制逻辑,供派生类调用 protected Employee CopyBaseProperties(Employee target) { return target with { EmployeeId = this.EmployeeId }; } } public record Trainee : Employee { public int ManagerId { get; init; } // 直接复用基类逻辑生成Manager public Manager ToManager(string adminCode) { var manager = new Manager { AdminCode = adminCode }; return (Manager)CopyBaseProperties(manager); } }
调用示例:
var trainee = new Trainee { EmployeeId = 123, ManagerId = 456 }; var manager = trainee.ToManager("ADMIN001");
这种方式类型安全,适合基类字段不多的场景,完全避免重复写属性复制代码。
方法2:反射批量复制属性
如果基类/派生类字段较多,手动映射太繁琐,可以用反射批量匹配同名同类型的可初始化属性:
public static class RecordConverter { public static TTarget Convert<TSource, TTarget>(TSource source) where TSource : Employee where TTarget : Employee, new() { var target = new TTarget(); // 获取源对象的可读公共属性 var sourceProps = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.GetIndexParameters().Length == 0); // 获取目标对象的可初始化公共属性 var targetProps = typeof(TTarget).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanWrite && p.GetIndexParameters().Length == 0 && p.SetMethod?.IsInitOnly == true); foreach (var sourceProp in sourceProps) { var targetProp = targetProps.FirstOrDefault(p => p.Name == sourceProp.Name && p.PropertyType == sourceProp.PropertyType); if (targetProp != null) { targetProp.SetValue(target, sourceProp.GetValue(source)); } } return target; } }
调用示例:
var trainee = new Trainee { EmployeeId = 123, ManagerId = 456 }; // 复制基类属性后补充Manager独有的字段 var manager = RecordConverter.Convert<Trainee, Manager>(trainee) with { AdminCode = "ADMIN001" };
一次封装通用逻辑,所有同基类的record都能复用,适合字段多的场景。
方法3:序列化反序列化中转
如果本来就在处理JSON序列化流程,可以直接用序列化库中转,自动复制所有可序列化的属性:
using System.Text.Json; public static class RecordConverter { public static TTarget ConvertViaJson<TSource, TTarget>(TSource source) where TSource : Employee where TTarget : Employee { var json = JsonSerializer.Serialize(source); return JsonSerializer.Deserialize<TTarget>(json)!; } }
调用示例:
var trainee = new Trainee { EmployeeId = 123, ManagerId = 456 }; var manager = RecordConverter.ConvertViaJson<Trainee, Manager>(trainee) with { AdminCode = "ADMIN001" };
优点是不用关心字段变化,缺点是有序列化性能开销,适合对性能要求不高的场景。
方法4:.NET 8+ 表达式树高性能映射
如果需要极致性能,用表达式树提前编译映射委托,避免反射的性能损耗:
using System.Linq.Expressions; public static class RecordConverter { private static readonly Dictionary<Type, Dictionary<Type, Delegate>> _cachedConverters = new(); public static TTarget Convert<TSource, TTarget>(TSource source) where TSource : Employee where TTarget : Employee, new() { var sourceType = typeof(TSource); var targetType = typeof(TTarget); if (!_cachedConverters.TryGetValue(sourceType, out var targetMap) || !targetMap.TryGetValue(targetType, out var converter)) { converter = BuildConverter<TSource, TTarget>(); _cachedConverters.TryAdd(sourceType, new Dictionary<Type, Delegate>()); _cachedConverters[sourceType][targetType] = converter; } return ((Func<TSource, TTarget>)converter)(source); } private static Func<TSource, TTarget> BuildConverter<TSource, TTarget>() where TSource : Employee where TTarget : Employee, new() { var sourceParam = Expression.Parameter(typeof(TSource), "source"); var targetCtor = Expression.New(typeof(TTarget)); var bindings = new List<MemberBinding>(); var sourceProps = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanRead && p.GetIndexParameters().Length == 0); var targetProps = typeof(TTarget).GetProperties(BindingFlags.Public | BindingFlags.Instance) .Where(p => p.CanWrite && p.GetIndexParameters().Length == 0 && p.SetMethod?.IsInitOnly == true); foreach (var sourceProp in sourceProps) { var targetProp = targetProps.FirstOrDefault(p => p.Name == sourceProp.Name && p.PropertyType == sourceProp.PropertyType); if (targetProp != null) { bindings.Add(Expression.Bind(targetProp, Expression.Property(sourceParam, sourceProp))); } } var initExpr = Expression.MemberInit(targetCtor, bindings); return Expression.Lambda<Func<TSource, TTarget>>(initExpr, sourceParam).Compile(); } }
第一次调用会编译映射委托,后续调用性能接近手动写的映射,适合高性能场景。
内容的提问来源于stack exchange,提问作者AceGambit
相关产品推荐
相关产品推荐

