3

I have the following extension method which fills in missing values of a class member from a source object of the same class. The missing part of this method is when a member of the class is also a class object of another class that also has members of its own, so I need a recursion here, but I didn't find any way to get the inner members of the class member and being able to pass it back to the FillValues method...

For example, I have a class called User and a class called UserLogin like this:

public class User
{
    public string FirstName;
    public string LastName;
    public UserLogin Login;
}

public class UserLogin
{
    public string Email;

    public string Password;
}

When I call FillValues on a member of class User if fills in the missing fields from FirstName and LastName but not from Login because its a class member also.

How can I pass the member Login recursively so that nested members will also be filled with values?

public static void FillValues<T>(this T target, T source)
{
    FillMissingProperties(target, source);
    FillMissingFields(target, source);
}

private static void FillMissingProperties<T>(T target, T source)
{
    var properties = typeof(T).GetProperties().Where(prop => prop.CanRead && prop.CanWrite);
    foreach (var prop in properties)
    {
        var targetValue = prop.GetValue(target, null);
        var defaultValue = prop.PropertyType.GetTypeInfo().IsValueType ? Activator.CreateInstance(prop.PropertyType) : null;

        if (targetValue == null || targetValue.Equals(defaultValue))
        {
            var sourceValue = prop.GetValue(source, null);
            prop.SetValue(target, sourceValue, null);
        }
    }
}

private static void FillMissingFields<T>(T target, T source)
{
    var fields = typeof(T).GetFields();
    foreach (var field in fields)
    {
        var targetValue = field.GetValue(target);
        var sourceValue = field.GetValue(source);
        var defaultValue = field.FieldType.GetTypeInfo().IsValueType ? Activator.CreateInstance(field.FieldType) : null;

        if (targetValue == null || targetValue.Equals(defaultValue))
            field.SetValue(target, sourceValue);
    }
}
Liran Friedman
  • 4,027
  • 13
  • 53
  • 96
  • The answer to [this question](https://stackoverflow.com/questions/37844147/c-sharp-loop-iterate-through-object-to-get-property-values-with-complex-property/37844503#37844503) might help you in this situation. – Matt Rowland Feb 12 '18 at 15:37
  • From what I can tell, this will work, it will set `Login` on the target with the same instance as on source. Do you want to have a new instance ? – Titian Cernicova-Dragomir Feb 12 '18 at 15:39
  • `Login` is still a field of `User`, so the code to recursively enumerate it would go into `FillMissingFields`, calling `FillValues` with a source/target pair pointing to the field that is a reference type ("class"). If you keep it like you have it, you could as well just remove the check for value types, and assign the reference (or do you need a "deep copy")? – Cee McSharpface Feb 12 '18 at 15:40
  • @TitianCernicova-Dragomir, If `Login` is not null it won't work – Liran Friedman Feb 12 '18 at 15:41
  • So the desired behavior is that if it is not null it should be filled in, and if it is null a new instance should be created ? – Titian Cernicova-Dragomir Feb 12 '18 at 15:42
  • @dlatikay, I don't want deep copy, I want to only fill in missing values. – Liran Friedman Feb 12 '18 at 15:42
  • Is this a contradiction? "I don't want deep copy, I want to only fill in missing values." But the title of the question is "How to get properties of class member which is also a class?" As soon as you start getting into the properties of reference types which themselves are used as properties, that sounds more like a deep copy. I recommend Automapper. – Scott Hannen Feb 12 '18 at 15:59
  • @ScottHannen - this is not a deep copy since I'm not cloning the object. I'm looping over the properties and only sets value to missing fields. – Liran Friedman Feb 12 '18 at 16:04
  • But what happens if the missing field is itself a reference type? – Scott Hannen Feb 12 '18 at 16:11
  • So I check for null. The issue I found now is for ValueTypes which always has a default value. So that is indeed a bug. – Liran Friedman Feb 12 '18 at 16:13

1 Answers1

2

You need to recursively call FillValues for the class fields/properties that are of a class type. To do this you need to have a non generic version of this method:

public static void FillValues<T>(this T target, T source)
{
    FillValues(typeof(T), target, source);
}

private static void FillValues(Type type, object target, object source)
{
    FillMissingProperties(type, target, source);
    FillMissingFields(type, target, source);
}
private static void FillMissingProperties(Type type, object target, object source)
{
    var properties = type.GetProperties().Where(prop => prop.CanRead && prop.CanWrite);
    foreach (var prop in properties)
    {
        var targetValue = prop.GetValue(target, null);
        var defaultValue = prop.PropertyType.GetTypeInfo().IsValueType ? Activator.CreateInstance(prop.PropertyType) : null;

        if (targetValue == null || targetValue.Equals(defaultValue))
        {
            var sourceValue = prop.GetValue(source, null);
            prop.SetValue(target, sourceValue, null);
        }
        else if (targetValue != null && prop.PropertyType != typeof(string) && prop.PropertyType.GetTypeInfo().IsClass)
        {
            var sourceValue = prop.GetValue(source, null);
            FillValues(prop.PropertyType, targetValue, sourceValue);
        }
    }
}

private static void FillMissingFields(Type type, object target, object source)
{
    var fields = type.GetFields();
    foreach (var field in fields)
    {
        var targetValue = field.GetValue(target);
        var sourceValue = field.GetValue(source);
        var defaultValue = field.FieldType.GetTypeInfo().IsValueType ? Activator.CreateInstance(field.FieldType) : null;

        if (targetValue == null || targetValue.Equals(defaultValue))
        {
            field.SetValue(target, sourceValue);
        }
        else if(targetValue != null && field.FieldType  != typeof(string) && field.FieldType.GetTypeInfo().IsClass)
        {
            FillValues(field.FieldType, targetValue, sourceValue);
        }
    }
}
Titian Cernicova-Dragomir
  • 230,986
  • 31
  • 415
  • 357
  • It works, but can you please explain why is this workaround needed? – Liran Friedman Feb 12 '18 at 15:58
  • @LiranFriedman I wouldn't necessarily call it a work around. The code only copies properties if they are null on the target. If login is not null, nothing happened to the property. The code did not drill down into the properties of complex properties. The generic method had to be changed because it is more difficult to call the generic version for complex properties, as you have the type at runtime not compile time. – Titian Cernicova-Dragomir Feb 12 '18 at 16:29