0

I want to copy the values of Properties in a given object ClassA to another object instance called ClassB, these classes may or may not be the same type.

if a property in ClassB has a value and in ClassA the corresponding property value is null, then do not copy that value, so only copy across where the current property in ClassB is null.

This is NOT a clone exercise, the target object (ClassB) is already instantiated with partially defined values, I'm looking for a reusable way to copy across the rest of the values that were not already set.

Think of testing scenarios where we have a common or default test data value, for specific tests I want to set some specific fields, then finish setting the other properties from the common test data object.

I think I am looking for a Reflection based solution, as that way we would not need to know the specific types to copy, which would make it reusable for many different scenarios.

eg.

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; }
}

test eg.

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);
}

I want to get the result

employeeCopy EmployeeID=101;
EmployeeName="Tom";
ContactAddress.Address1 = "Park Ave";
ContactAddress.City = "New York";
ContactAddress.State = "NewYork";
ContactAddress.ZipCode = "10002"

So in this case, because none of the fields in employeeCopy.ContactAddress have been set, only those fields from the original employee object should be copied across.

I can not figure out how to write the method:
CopyPropertiesTo(object sourceObject, object targetObject)

Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
qiuqp
  • 3
  • 1
  • 5
  • 2
    What you are trying to do is something called *Deep Copy*. You should lookup about it and can be performed in multiple ways. – Sai Gummaluri Aug 21 '20 at 05:41
  • Anyway, you could just use if then else. Whats the problem? – TheGeneral Aug 21 '20 at 05:47
  • Are you trying to do this through reflection? (because of the tag in the question). Is Employee a sample class or is it the class related to the question? – Ivan Vargas Aug 21 '20 at 05:50
  • I mian if ClassB has value,I will use it,but if ClassB is null and ClassA has value ,copy ClassA's value,I can not write the method "CopyPropertiesTo(employee, employeeCopy)", some body who can help me? thank you! – qiuqp Aug 21 '20 at 06:03

8 Answers8

1

One way to do this is to simply check each property in the "to" Employee, and if it's null or 0, assign it the value from the "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;
}
Rufus L
  • 36,127
  • 5
  • 30
  • 43
  • Whilst this works, to do this so specifically for these types is more lines of code than implementing it in the test direct. It also does not satisfy OPs request for a reflection based solution, presumably OP has many or other types to apply this logic to – Chris Schaller Aug 21 '20 at 09:14
  • @ChrisSchaller Yeah, I've tried getting that clarification from OP but haven't heard from him yet. A reflection-based solution would be far more flexible, but also at the expense of performance. Also, reflection-based deep copy can easily become error-prone for some types. I envision this method as belonging to the `Employee` class. – Rufus L Aug 21 '20 at 14:27
  • @ChrisSchaller Also, if that's what they're looking for, then this question becomes a duplicate of [this](https://stackoverflow.com/questions/78536/deep-cloning-objects) or [this](https://stackoverflow.com/questions/129389/how-do-you-do-a-deep-copy-of-an-object-in-net) (with the exception of checking if the first object's property is set to the default for the type). – Rufus L Aug 21 '20 at 14:33
  • Not really, those are specifically about _clone_ but this is conditionally setting properties, OPs example about Employee was just one class, they are searching for a reflection based solution so they do not have to hardcode all these property comparisons. No one is suggesting its a great idea, but it can certainly be done. – Chris Schaller Aug 21 '20 at 15:14
  • @ChrisSchaller As I said, *"with the exception of checking if the first object's property is set to the default for the type"*, which is simple enough to add. Where do they state that they're *"searching for a reflection based solution so they do not have to hardcode all these property comparisons"*? That may be the case, but at the moment it's only an assumption (unless I missed something in the question). – Rufus L Aug 21 '20 at 15:19
  • Post is tagged with _reflection_, but also in OPs comments, and later edits they use a more generic example: _if ClassB has value,I will use it,but if ClassB is null and ClassA has value ,copy ClassA's value_ OP is really looking for a single method that we can pass in any `ClassA` or `ClassB`. – Chris Schaller Aug 21 '20 at 15:25
1

Here is my suggestions too if not too late, but mayby helps.

    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);
                }
            }
        }
    }

EDIT: I edited my solution in order to remove the dependency on using DefaultValueAttribute. Now you can take a default value either from the attributes if specified or the type defaults.

Previous solution was as follows:

        // 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);
spzvtbg
  • 964
  • 5
  • 13
  • I like the use of `DefaultValueAttribute` its an interesting fail-safe. I try to set _Default Values_ as part of the property defintion (or the _constructor_) whenever I use the `DefaultValueAttribute`, but in a generic purposed method we can't make that assumption. I'm going to use this in my base logic from now on :) – Chris Schaller Aug 24 '20 at 08:11
  • Two quick pointers though, when using attributes, the _attribute_ suffix on the class name is redundant, the code is often clearer to read if you leave it off like `[DefaultValue(0)]` and secondly, `0` _is_ the default value of an `int` so it's redundant for use in OPs scenario. It has its uses for `WinForms` and other design surface frameworks but if the value was already zero (_default_) then we wouldn't see the effect of setting it to a value of `0`. - Just saying it's not a great example to use, but I like the idea. – Chris Schaller Aug 24 '20 at 08:17
  • I agree that the Attribute sufix is redundand but the Attribute itself has not a empty constructor and you need to specify the defailt value that you can then access trought the reflection too – spzvtbg Aug 24 '20 at 09:04
  • Yeah but the _default_ value of int _is_ `0` so the behavior is the same if the attribute was there or if we just skipped the property because it did not have the attribute at all. – Chris Schaller Aug 24 '20 at 13:07
  • @Chris Schaller - The use of the DefaultValueAttribute with values ​​like 0, false or null does not make much sense but if you want to use them with reflection to take default values ​​from the properties, you have to. On the other hand, if you only build the default values ​​with reflection then it makes no sense to use the attributes. – spzvtbg Aug 24 '20 at 14:40
  • @Chris Schaller - But hey there is a solution in between by checking if the property is decorated with the attributes take its value if not then take the default with reflection. I added as edited the LINQ for this. This is all to answer the OP's question how to map values between two objects, where else the attributes are used and whether it makes sense is another topic. – spzvtbg Aug 24 '20 at 14:41
0

Make your changes to the new instance after the copy completes and/or implement ICloneable interface. https://learn.microsoft.com/en-us/dotnet/api/system.icloneable?view=netcore-3.1

Austin G
  • 136
  • 1
  • 7
  • Surely not modifiying the non-`null` properites in the first place would be better? What if one of the properties you changed was a child object graph? That would get tricky –  Aug 21 '20 at 05:53
  • Not entirely sure what you mean by child object graph, but their structure isn't that complex. If it needs to be reusable for generic types, then I'm not sure if there will be good solution for that *shrugs*. – Austin G Aug 21 '20 at 06:00
  • `Address` is not a simple value type, it is a _reference_ type so `Employee` represents an object graph albeit only 2 levels deep. The OP is essentially describing a _merge_ opertation so skipping non-null properties in the target would be better. I'm not saying you are wrong, just that it might require extra effort. –  Aug 21 '20 at 06:05
0
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;

}

Is this what you are looking for ?

Mohammed
  • 151
  • 1
  • 12
0
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)));
            }
        }
    }
}

Not the prettiest, but this should do it and allow you to change the names/amount of properties in the class. I haven't ever actually tried doing it like this so if someone out there has some feedback, I would appreciate it

Check out the following links for more info and examples PropertyInfo GetType GetProperty

  • As a best practice you want to avoid excessive non-LINQ _method chaining_ because 1) if it fails it's hard to know where 2) hard to read 3) difficult to debug because you can't see the return values –  Aug 21 '20 at 06:57
  • Thanks for the feedback! ill see if i can re-write it with LINQ Queries – lvlegabyte Aug 21 '20 at 07:11
  • 1
    Not a problem. By the way, I didn't mean for you to re-write it using LINQ, rather that in general when using LINQ method chaining is acceptible. But definately, you could well do the above in LINQ. –  Aug 21 '20 at 07:18
  • if we can judge the porperty is class,i think this is an very good answer,but i donot know how to judge – qiuqp Aug 21 '20 at 07:25
  • 1
    i am not quite sure what you mean by "judge"? can you please elaborate. – lvlegabyte Aug 21 '20 at 07:30
  • Oh, See the code "if(p.Name == "ContactAddress")",if we can confirm p's property is Address (Class), it will be common use ,this is a demo, in fact,it has many class in one – qiuqp Aug 21 '20 at 07:37
  • ah i see what you mean. Changing "if(p.Name == "ContactAddress")" to "if(p.PropertyType == typeof(Address))" would do it, ill update the answer – lvlegabyte Aug 21 '20 at 07:51
  • yeah you could but would have to have a check in there for it either to be a particular type or type you are expecting or to not be the others that are in there like 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(); – lvlegabyte Aug 21 '20 at 08:18
  • hmm cant figure out how to get code into the comments ill update the answer with what i mean – lvlegabyte Aug 21 '20 at 08:18
  • @qiuqp It sounds like you're looking for a generic way to deep copy a class; in other words, a method that will work for *any* type. Is that correct? If so, you should update your question with that requirement. Reflection would be the way to go in that case, but it's expensive for performance compared to dealing with a known type directly, which is what your question is currently asking about. Also, [this question](https://stackoverflow.com/questions/129389/how-do-you-do-a-deep-copy-of-an-object-in-net) already has some good answers for that. – Rufus L Aug 21 '20 at 08:51
0

Deep cloning can be achieved easily through serialization, however to only copy across non-null fields needs more conditional logic, In this case I call this a Coalesce so I've named my Method CoalesceTo. You could refactor this into an extension method if you wanted to, but I wouldn't recommend it, instead put this inside a static helper class. As useful as this might be, I don't encourage it as your "goto" for a production business runtime.

Using Reflection for these types of solutions is usually the most inefficient mechanism, but it gives us a lot of flexibility and is great for mocking, prototyping and quick unit test expressions.

  • Although not in this example, it would be easy to add in checks to exclude [Obsolete] properties for advanced scenarios

The following example uses Property Name comparison, so you don't have to pass in objects of the same type. Notice that IsNull and IsValueType methods have been created to encapsulate those concepts, simplifying tweaks you might want to make to this method.

  • This method also checks that properties can be read/written before proceeding, which allows us to support readonly properties on the source object, and of course we don't try to write to readonly properties.
  • The final value parse and write is wrapped in a try catch statement that is suppressing any errors, It takes a bit of tweaking to get code like this to work universally, but it should work fine for simple type definitions.
/// <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;
}

Because we are using reflection here, we cant take advantage of optimisations that Generics could offer us. If you wanted to adapt this to a production environment, consider using T4 templates to script out a Generic typed version of this logic as extension methods to your business types.

Deep Cloning -

You'll notice I specifically skip arrays and other IEnumerable structures... There's a whole can of worms in supporting them, it might be better to not let the one method attempt a Deep copy, so take the nested call to CoalesceTo out, then call the clone method on each object in the tree.

The problem with arrays/collections/lists is that before you could clone, you would need to identify a way to synchronise the collection in the source with the collection in the target, you could make a convention based on an Id field or some kind of attribute like [KeyAttribute] but that sort of implementation needs to be highly specific to your business logic and is outside of the scope of this already monstrous post ;)

Types like Decimal and DateTime are problematic in these types of scenarios, they should not be compared to null, instead we have to compare them to their default type states, again we can't use the generic default operator or value in this case because the type can only be resolved at runtime.

So I've changed your classes to include an example of how DateTimeOffset is handled by this logic:

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);
}

This results in the following object graph:

{
  "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"
  }
}
Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
  • I need to re-iterate a major hurdle with this type of _coalesce_ in _DEEP_ object graphs that have arrays or collection properties, If you want to support 1:m properties you will need to identify a way to synchronize the collection in the source with the collection in the target, so how to map the right objects before you copy them. You could make a convention based on an Id field or some kind of attribute like [KeyAttribute] but that sort of implementation will become highly specific to your business logic. If you can just avoid enumerables, or replace them as if they were VALUES – Chris Schaller Aug 21 '20 at 09:26
  • As I Test,If I have Nullable,it stills report error, my code: float? check – qiuqp Aug 24 '20 at 07:18
  • Thanks @qiuqp for picking that up, see the change to the `IsNull` method, that is the primary reason for extracting the logic for `IsNull` out to its own method, the core logic is still the same, we just improved the definition of _what is null_. You may find other scenarios that cause issues, `IsValueType` is the other switch logic that may need to be improved in the future. – Chris Schaller Aug 24 '20 at 08:06
  • Thanks, I modify the IsNull Method, Here is my code: if (valueType.IsPrimitive || valueType.IsEnum || valueType.IsValueType) return value.Equals(Activator.CreateInstance(valueType)); – qiuqp Aug 24 '20 at 08:50
  • Thats the previous logic, but I have changed that in my solution to match your request, it has a specific comment about nullable types. – Chris Schaller Aug 24 '20 at 13:09
0

Instead of trying to do a deep copy at all, these types of issues are generally easier and less resource intensive if you do a full deep clone first, and then set your values.

There are many posts on SO regarding deep clone, my preference is just to use JSON.Net to serialize and then deserialize.

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);
}

However this code requires the Newtonsoft.Json nuget package reference.

Cloning the object sets all the common/default values first, and then we only modify those properties that we need for this specific test, or code block.

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";

}

Often we can find simpler solutions if we are open to changing our approach, this solution will have the same output as if we had conditionally copied across the property values, but without comparing anything.

If you have other reasons for a conditional copy, referred to in other solutions to this post as a Merge or Coalesce, then my other answer using reflection will do the job, but its not as robust as this one.

Chris Schaller
  • 13,704
  • 3
  • 43
  • 81
0
[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; }
}

Thank you very much for every one,especially for @Chris Schaller I post the code above

qiuqp
  • 3
  • 1
  • 5