![](/img/trans.png)
[英]Same Variable Names - 2 Different Classes - How To Copy Values From One To Another - Reflection - C#
[英]how to copy properties from one object to another with different values C#
我想将给定 object ClassA
中的 Properties 值复制到另一个名为ClassB
的 object 实例中,这些类可能是也可能不是同一类型。
如果ClassB
中的属性有值,而ClassA
中对应的属性值为 null,则不要复制该值,因此仅复制ClassB
中的当前属性为 null 的位置。
这不是克隆练习,目标 object (
ClassB
) 已经用部分定义的值实例化,我正在寻找一种可重用的方法来复制尚未设置的值的 rest。想想我们有一个通用或默认测试数据值的测试场景,对于特定的测试我想设置一些特定的字段,然后从通用测试数据 object 中完成设置其他属性。
我想我正在寻找一个基于反射的解决方案,因为这样我们就不需要知道要复制的特定类型,这将使它可以在许多不同的场景中重用。
例如。
public class Employee
{
public int EmployeeID { get; set; }
public string EmployeeName { get; set; }
public Address ContactAddress { get; set; }
}
public class Address
{
public string Address1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
测试例如。
public void TestMethod1()
{
Employee employee = new Employee();
employee.EmployeeID = 100;
employee.EmployeeName = "John";
employee.ContactAddress = new Address();
employee.ContactAddress.Address1 = "Park Ave";
employee.ContactAddress.City = "New York";
employee.ContactAddress.State = "NewYork";
employee.ContactAddress.ZipCode = "10002";
Employee employeeCopy = new Employee();
employeeCopy.EmployeeID = 101;
employeeCopy.EmployeeName = "Tom";
employeeCopy.ContactAddress = new Address();
CopyPropertiesTo(employee, employeeCopy);
}
我想得到结果
员工复制员工ID = 101;
员工姓名="汤姆";
ContactAddress.Address1 = "公园大道";
ContactAddress.City = "纽约";
ContactAddress.State = "纽约";
ContactAddress.ZipCode = "10002"
因此,在这种情况下,由于没有设置employeeCopy.ContactAddress
中的任何字段,因此只应复制原始employee
object 中的那些字段。
我不知道如何编写方法:
CopyPropertiesTo(object sourceObject, object targetObject)
一种方法是简单地检查“to” Employee
中的每个属性,如果它是null
或0
,则为其分配来自“from” Employee
的值:
/// <summary>
/// Copies values in 'from' to 'to' if they are null in 'to'
/// </summary>
public static void CopyProperties(Employee from, Employee to)
{
if (from == null) return;
if (to == null) to = new Employee();
if (to.EmployeeID == 0) to.EmployeeID = from.EmployeeID;
if (to.EmployeeName == null) to.EmployeeName = from.EmployeeName;
if (from.ContactAddress == null) return;
if (to.ContactAddress == null) to.ContactAddress = new Address();
if (to.ContactAddress.Address1 == null)
to.ContactAddress.Address1 = from.ContactAddress.Address1;
if (to.ContactAddress.City == null)
to.ContactAddress.City = from.ContactAddress.City;
if (to.ContactAddress.State == null)
to.ContactAddress.State = from.ContactAddress.State;
if (to.ContactAddress.ZipCode == null)
to.ContactAddress.ZipCode = from.ContactAddress.ZipCode;
}
public static void CopyPropertiesTo(Employee EP1, Employee EP2){
Type eType=typeof(Employee);
PropertyInfo[] eProps = eType.GetProperties();
foreach(var p in eProps){
if(p.PropertyType != typeof(String) && p.PropertyType != typeof(Int32)){
//Merging Contact Address
Type cType=p.PropertyType;
PropertyInfo[] cProps = cType.GetProperties();
foreach(var c in cProps){
//Check if value is null
if (String.IsNullOrEmpty((EP2.ContactAddress.GetType().GetProperty(c.Name).GetValue(EP2.ContactAddress) as string))){
//Assign Source to Target
EP2.ContactAddress.GetType().GetProperty(c.Name).SetValue(EP2.ContactAddress, (EP1.ContactAddress.GetType().GetProperty(c.Name).GetValue(EP1.ContactAddress)));
}
}
}
else{
//Check if value is null or empty
if (String.IsNullOrEmpty((EP2.GetType().GetProperty(p.Name).GetValue(EP2) as string))){
//Assign Source to Target
EP2.GetType().GetProperty(p.Name).SetValue(EP2, (EP1.GetType().GetProperty(p.Name).GetValue(EP1)));
}
}
}
}
不是最漂亮的,但这应该可以让您更改 class 中的属性名称/数量。 我从来没有真正尝试过这样做,所以如果有人有一些反馈,我将不胜感激
查看以下链接以获取更多信息和示例PropertyInfo GetType GetProperty
如果还不算太晚,这也是我的建议,但也许会有所帮助。
public class Source
{
[DefaultValueAttribute(-1)]
public int Property { get; set; }
public int AnotherProperty { get; set; }
}
public class Dedstination
{
public int Property { get; set; }
[DefaultValueAttribute(42)]
public int AnotherProperty { get; set; }
}
public void Main()
{
var source = new Source { Property = 10, AnotherProperty = 76 };
var destination = new Dedstination();
MapValues(source, destination);
}
public static void MapValues<TS, TD>(TS source, TD destination)
{
var srcPropsWithValues = typeof(TS)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(x => x.Name, y => y.GetValue(source));
var dstProps = typeof(TD)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(key => key, value => value.GetCustomAttribute<DefaultValueAttribute>()?.Value
?? (value.PropertyType.IsValueType
? Activator.CreateInstance(value.PropertyType, null)
: null));
foreach (var prop in dstProps)
{
var destProperty = prop.Key;
if (srcPropsWithValues.ContainsKey(destProperty.Name))
{
var defaultValue = prop.Value;
var currentValue = destProperty.GetValue(destination);
var sourceValue = srcPropsWithValues[destProperty.Name];
if (currentValue.Equals(defaultValue) && !sourceValue.Equals(defaultValue))
{
destProperty.SetValue(destination, sourceValue);
}
}
}
}
编辑:我编辑了我的解决方案,以消除对使用 DefaultValueAttribute 的依赖。 现在,您可以从指定的属性或类型默认值中获取默认值。
以前的解决方案如下:
// This solution do not needs DefaultValueAttributes
var dstProps = typeof(TD)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(x => x, x => x.PropertyType.IsValueType ? Activator.CreateInstance(x.PropertyType, null) : null);
// This solution needs DefaultValueAttributes
var dstProps = typeof(TD)
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(x => x, x => x.GetCustomAttribute<DefaultValueAttribute>()?.Value ?? null);
在复制完成和/或实现 ICloneable 接口后对新实例进行更改。 https://docs.microsoft.com/en-us/dotnet/api/system.icloneable?view=netcore-3.1
深度克隆可以通过序列化轻松实现,但是仅跨非空字段复制需要更多条件逻辑,在这种情况下,我将其称为Coalesce ,因此我将我的方法命名为CoalesceTo
。 如果您愿意,您可以将其重构为扩展方法,但我不推荐它,而是将其放在 static 助手 class 中。 尽管这可能很有用,但我不鼓励将其作为生产业务运行时的“goto”。
对这些类型的解决方案使用反射通常是效率最低的机制,但它为我们提供了很大的灵活性,并且非常适合 mocking、原型设计和快速单元测试表达式。
- 尽管不在此示例中,但可以轻松添加检查以排除高级场景的
[Obsolete]
属性
以下示例使用属性名称比较,因此您不必传入相同类型的对象。 请注意,已创建IsNull
和IsValueType
方法来封装这些概念,从而简化您可能希望对此方法进行的调整。
/// <summary>
/// Deep Copy the top level properties from this object only if the corresponding property on the target object IS NULL.
/// </summary>
/// <param name="source">the source object to copy from</param>
/// <param name="target">the target object to update</param>
/// <returns>A reference to the Target instance for chaining, no changes to this instance.</returns>
public static void CoalesceTo(object source, object target, StringComparison propertyComparison = StringComparison.OrdinalIgnoreCase)
{
var sourceType = source.GetType();
var targetType = target.GetType();
var targetProperties = targetType.GetProperties();
foreach(var sourceProp in sourceType.GetProperties())
{
if(sourceProp.CanRead)
{
var sourceValue = sourceProp.GetValue(source);
// Don't copy across nulls or defaults
if (!IsNull(sourceValue, sourceProp.PropertyType))
{
var targetProp = targetProperties.FirstOrDefault(x => x.Name.Equals(sourceProp.Name, propertyComparison));
if (targetProp != null && targetProp.CanWrite)
{
if (!targetProp.CanRead)
continue; // special case, if we cannot verify the destination, assume it has a value.
else if (targetProp.PropertyType.IsArray || targetProp.PropertyType.IsGenericType // It is ICollection<T> or IEnumerable<T>
&& targetProp.PropertyType.GenericTypeArguments.Any()
&& targetProp.PropertyType.GetGenericTypeDefinition() != typeof(Nullable<>) // because that will also resolve GetElementType!
)
continue; // special case, skip arrays and collections...
else
{
// You can do better than this, for now if conversion fails, just skip it
try
{
var existingValue = targetProp.GetValue(target);
if (IsValueType(targetProp.PropertyType))
{
// check that the destination is NOT already set.
if (IsNull(existingValue, targetProp.PropertyType))
{
// we do not overwrite a non-null destination value
object targetValue = sourceValue;
if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
{
// TODO: handle specific types that don't go across.... or try some brute force type conversions if neccessary
if (targetProp.PropertyType == typeof(string))
targetValue = targetValue.ToString();
else
targetValue = Convert.ChangeType(targetValue, targetProp.PropertyType);
}
targetProp.SetValue(target, targetValue);
}
}
else if (!IsValueType(sourceProp.PropertyType))
{
// deep clone
if (existingValue == null)
existingValue = Activator.CreateInstance(targetProp.PropertyType);
CoalesceTo(sourceValue, existingValue);
}
}
catch (Exception)
{
// suppress exceptions, don't set a field that we can't set
}
}
}
}
}
}
}
/// <summary>
/// Check if a boxed value is null or not
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of null in here.
/// </remarks>
/// <param name="value">Value to inspect</param>
/// <param name="valueType">Type of the value, pass it in if you have it, otherwise it will be resolved through reflection</param>
/// <returns>True if the value is null or primitive default, otherwise False</returns>
public static bool IsNull(object value, Type valueType = null)
{
if (value is null)
return true;
if (valueType == null) valueType = value.GetType();
if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType)
{
// Handle nullable types like float? or Nullable<Int>
if (valueType.IsGenericType)
return value is null;
else
return Activator.CreateInstance(valueType).Equals(value);
}
// treat empty string as null!
if (value is string s)
return String.IsNullOrWhiteSpace(s);
return false;
}
/// <summary>
/// Check if a type should be copied by value or if it is a complexe type that should be deep cloned
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of Object vs Value/Primitive here.
/// </remarks>
/// <param name="valueType">Type of the value to check</param>
/// <returns>True if values of this type can be straight copied, false if they should be deep cloned</returns>
public static bool IsValueType(Type valueType)
{
// TODO: any specific business types that you want to treat as value types?
// Standard .Net Types that can be treated as value types
if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType || valueType == typeof(string))
return true;
// Support Nullable Types as Value types (Type.IsValueType) should deal with this, but just in case
if (valueType.HasElementType // It is array/enumerable/nullable
&& valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
return true;
return false;
}
因为我们在这里使用反射,所以我们无法利用Generics可以为我们提供的优化。 如果您想将其应用于生产环境,请考虑使用 T4 模板来编写此逻辑的通用类型版本作为您的业务类型的扩展方法。
你会注意到我特别跳过了 arrays 和其他 IEnumerable 结构......有一大堆蠕虫在支持它们,最好不要让一种方法尝试深度复制,所以把嵌套调用CoalesceTo
拿出来,然后在树中的每个 object 上调用 clone 方法。
数组/集合/列表的问题在于,在克隆之前,您需要确定一种将源中的集合与目标中的集合同步的方法,您可以根据 Id 字段或某种形式进行约定像
[KeyAttribute]
这样的属性,但这种实现需要高度特定于您的业务逻辑,并且不在这个已经很可怕的帖子的 scope 之外;)
像Decimal
和DateTime
这样的类型在这些类型的场景中是有问题的,它们不应该与 null 进行比较,而是我们必须将它们与它们的默认类型状态进行比较,同样我们不能在这种情况下使用通用的default
运算符或值,因为类型只能在运行时解析。
因此,我更改了您的类,以包含一个示例,说明此逻辑如何处理 DateTimeOffset:
public class Employee
{
public int EmployeeID { get; set; }
public string EmployeeName { get; set; }
public DateTimeOffset Date { get; set; }
public float? Capacity { get; set; }
Nullable<int> MaxShift { get; set; }
public Address ContactAddress { get; set; }
}
public class Address
{
public string Address1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
public static void TestMethod1()
{
Employee employee = new Employee();
employee.EmployeeID = 100;
employee.EmployeeName = "John";
employee.Capacity = 26.2f;
employee.MaxShift = 8;
employee.Date = new DateTime(2020,1,22);
employee.ContactAddress = new Address();
employee.ContactAddress.Address1 = "Park Ave";
employee.ContactAddress.City = "New York";
employee.ContactAddress.State = "NewYork";
employee.ContactAddress.ZipCode = "10002";
Employee employeeCopy = new Employee();
employeeCopy.EmployeeID = 101;
employeeCopy.EmployeeName = "Tom";
employeeCopy.ContactAddress = new Address();
CoalesceTo(employee, employeeCopy);
}
这会产生以下 object 图:
{
"EmployeeID": 101,
"EmployeeName": "Tom",
"Date": "2020-01-22T00:00:00+11:00",
"Capacity":26.2,
"MaxShift":8,
"ContactAddress": {
"Address1": "Park Ave",
"City": "New York",
"State": "NewYork",
"ZipCode": "10002"
}
}
如果您首先进行完整的深度克隆,然后设置您的值,那么这些类型的问题通常会更容易且资源消耗更少,而不是尝试进行深度复制。
SO上有很多关于深度克隆的帖子,我的偏好只是使用 JSON.Net 进行序列化然后反序列化。
public static T Clone<T>(T value, Newtonsoft.Json.JsonSerializerSettings settings = null) { var objectType = value.GetType(); var cereal = Newtonsoft.Json.JsonConvert.SerializeObject(value, settings); return (T)Newtonsoft.Json.JsonConvert.DeserializeObject(cereal, objectType, settings); }
但是,此代码需要Newtonsoft.Json nuget package 参考。
克隆 object首先设置所有通用/默认值,然后我们只修改此特定测试或代码块所需的那些属性。
public void TestMethod1()
{
Employee employee = new Employee();
employee.EmployeeID = 100;
employee.EmployeeName = "John";
employee.ContactAddress = new Address();
employee.ContactAddress.Address1 = "Park Ave";
employee.ContactAddress.City = "New York";
employee.ContactAddress.State = "NewYork";
employee.ContactAddress.ZipCode = "10002";
// Create a deep clone of employee
Employee employeeCopy = Clone(employee);
// set the specific fields that we want to change
employeeCopy.EmployeeID = 101;
employeeCopy.EmployeeName = "Tom";
}
如果我们愿意改变我们的方法,我们通常可以找到更简单的解决方案,这个解决方案将具有相同的 output,就好像我们有条件地复制了属性值一样,但没有进行任何比较。
如果您有条件副本的其他原因,在这篇文章的其他解决方案中称为Merge或Coalesce ,那么我使用反射的其他答案将完成这项工作,但它不如这个强大。
[TestClass]
public class UnitTest11
{
[TestMethod]
public void TestMethod1()
{
Employee employee = new Employee();
employee.EmployeeID = 100;
employee.EmployeeName = "John";
employee.Date = DateTime.Now;
employee.ContactAddress = new Address();
employee.ContactAddress.Address1 = "Park Ave";
employee.ContactAddress.City = "New York";
employee.ContactAddress.State = "NewYork";
employee.ContactAddress.ZipCode = "10002";
Employee employeeCopy = new Employee();
employeeCopy.EmployeeID = 101;
employeeCopy.EmployeeName = "Tom";
employeeCopy.ContactAddress = new Address();
employeeCopy.ContactAddress.City = "Bei Jing";
//copy all properties from employee to employeeCopy
CoalesceTo(employee, employeeCopy);
Console.ReadLine();
}
/// Deep Copy the top level properties from this object only if the corresponding property on the target object IS NULL.
/// </summary>
/// <param name="source">the source object to copy from</param>
/// <param name="target">the target object to update</param>
/// <returns>A reference to the Target instance for chaining, no changes to this instance.</returns>
public static void CoalesceTo(object source, object target, StringComparison propertyComparison = StringComparison.OrdinalIgnoreCase)
{
var sourceType = source.GetType();
var targetType = target.GetType();
var targetProperties = targetType.GetProperties();
foreach (var sourceProp in sourceType.GetProperties())
{
if (sourceProp.CanRead)
{
var sourceValue = sourceProp.GetValue(source);
// Don't copy across nulls or defaults
if (!IsNull(sourceValue, sourceProp.PropertyType))
{
var targetProp = targetProperties.FirstOrDefault(x => x.Name.Equals(sourceProp.Name, propertyComparison));
if (targetProp != null && targetProp.CanWrite)
{
if (!targetProp.CanRead)
continue; // special case, if we cannot verify the destination, assume it has a value.
else if (targetProp.PropertyType.IsArray || targetProp.PropertyType.IsGenericType // It is ICollection<T> or IEnumerable<T>
&& targetProp.PropertyType.GenericTypeArguments.Any()
&& targetProp.PropertyType.GetGenericTypeDefinition() != typeof(Nullable<>) // because that will also resolve GetElementType!
)
continue; // special case, skip arrays and collections...
else
{
// You can do better than this, for now if conversion fails, just skip it
try
{
var existingValue = targetProp.GetValue(target);
if (IsValueType(targetProp.PropertyType))
{
// check that the destination is NOT already set.
if (IsNull(existingValue, targetProp.PropertyType))
{
// we do not overwrite a non-null destination value
object targetValue = sourceValue;
if (!targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
{
// TODO: handle specific types that don't go across.... or try some brute force type conversions if neccessary
if (targetProp.PropertyType == typeof(string))
targetValue = targetValue.ToString();
else
targetValue = Convert.ChangeType(targetValue, targetProp.PropertyType);
}
targetProp.SetValue(target, targetValue);
}
}
else if (!IsValueType(sourceProp.PropertyType))
{
// deep clone
if (existingValue == null)
existingValue = Activator.CreateInstance(targetProp.PropertyType);
CoalesceTo(sourceValue, existingValue);
}
}
catch (Exception)
{
// suppress exceptions, don't set a field that we can't set
}
}
}
}
}
}
}
/// <summary>
/// Check if a boxed value is null or not
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of null in here.
/// </remarks>
/// <param name="value">Value to inspect</param>
/// <param name="valueType">Type of the value, pass it in if you have it, otherwise it will be resolved through reflection</param>
/// <returns>True if the value is null or primitive default, otherwise False</returns>
public static bool IsNull(object value, Type valueType = null)
{
if (value is null)
return true;
if (valueType == null) valueType = value.GetType();
if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType)
return value.Equals(Activator.CreateInstance(valueType));
// treat empty string as null!
if (value is string s)
return String.IsNullOrWhiteSpace(s);
return false;
}
/// <summary>
/// Check if a type should be copied by value or if it is a complexe type that should be deep cloned
/// </summary>
/// <remarks>
/// Evaluate your own logic or definition of Object vs Value/Primitive here.
/// </remarks>
/// <param name="valueType">Type of the value to check</param>
/// <returns>True if values of this type can be straight copied, false if they should be deep cloned</returns>
public static bool IsValueType(Type valueType)
{
// TODO: any specific business types that you want to treat as value types?
// Standard .Net Types that can be treated as value types
if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType || valueType == typeof(string))
return true;
// Support Nullable Types as Value types (Type.IsValueType) should deal with this, but just in case
if (valueType.HasElementType // It is array/enumerable/nullable
&& valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
return true;
return false;
}
}
public class Employee
{
public int EmployeeID { get; set; }
public string EmployeeName { get; set; }
public DateTimeOffset Date { get; set; }
public float? check { get; set; }
public Address ContactAddress { get; set; }
}
public class Address
{
public string Address1 { get; set; }
public string City { get; set; }
public string State { get; set; }
public string ZipCode { get; set; }
}
非常感谢大家,尤其是@Chris Schaller,我在上面发布了代码
private Employee Check(Employee employee,Employee employeeCopy)
{
if (employeeCopy.EmployeeID==0 && employee.EmployeeID !=0)
{
employeeCopy.EmployeeID = employee.EmployeeID;
}
if (employeeCopy.EmployeeName == null && employee.EmployeeName != null)
{
employeeCopy.EmployeeName = employee.EmployeeName;
}
if (employeeCopy.ContactAddress == null)
{
if (employeeCopy.ContactAddress.Address1 == null && employee.ContactAddress.Address1 != null)
{
employeeCopy.ContactAddress.Address1 = employee.ContactAddress.Address1;
}
if (employeeCopy.ContactAddress.City == null && employee.ContactAddress.City != null)
{
employeeCopy.ContactAddress.City = employee.ContactAddress.City;
}
if (employeeCopy.ContactAddress.State == null && employee.ContactAddress.State != null)
{
employeeCopy.ContactAddress.State = employee.ContactAddress.State;
}
if (employeeCopy.ContactAddress.ZipCode == null && employee.ContactAddress.ZipCode != null)
{
employeeCopy.ContactAddress.ZipCode = employee.ContactAddress.ZipCode;
}
}
return employeeCopy;
}
这是你想要的?
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.