Felipe Deguchi Felipe Deguchi - 19 days ago 7
C# Question

Subclass Reflection type error

I'm currently having some issues with a method I made. I use reflection to run through my class and get all it's properties. I use this to cast my models to DTO and vice-versa.

The problem I am encountering is that, whenever my class has another class as an attribute, I get an error.


Object of type 'UserTypeProxy' cannot be converted to type 'MyNamespace.DTO.UserTypeDto'.


This is my code:

public static T Cast<T>(object myobj)
{
Type _objectType = myobj.GetType();
Type target = typeof(T);

var x = Activator.CreateInstance(target, false);

var z = from source in _objectType.GetMembers().ToList()
where source.MemberType == MemberTypes.Property
select source;

var d = from source in target.GetMembers().ToList()
where source.MemberType == MemberTypes.Property
select source;

List<MemberInfo> members = d.Where(memberInfo => d.Select(c => c.Name)
.ToList().Contains(memberInfo.Name)).ToList();

PropertyInfo propertyInfo;
object value;

foreach (var memberInfo in members)
{
propertyInfo = typeof(T).GetProperty(memberInfo.Name);
var propy = myobj.GetType().GetProperty(memberInfo.Name);
value = propy.GetValue(myobj, null);

propertyInfo.SetValue(x, value, null); //<-- this is the line that gives the error
}
return (T)x;
}

Answer

As a previous commenter states, this is not the kind of code you should be writing/maintaining yourself. Frameworks like AutoMapper were built specifically to solve the problem you are attacking - converting model objects to DTOs. The right long-term choice would be to start leveraging such a framework instead of reinventing the wheel.

In the meanwhile the following code is a short-term solution for your issue. Keep in mind that while this may solve the specific case you mention in your question, object mapping has many corner cases and eventually you will run into another. I would recommend only using this as a temporary fix until you migrate to using AutoMapper or a similar framework.

Based on your description and your code, here is an example which models your failure:

static void Main(string[] args)
{
    var user = new UserModel
    {
        Name = "User McUserson",
        Age = 30,
        Buddy = new UserModel
        {
            Name = "Buddy McFriendly",
            Age = 28
        }
    };

    // This fails saying that UserModel cannot be converted to UserDto
    var userDto = Cast<UserDto>(user);  
}

class UserModel
{
    public String Name { get; set; }
    public int Age { get; set; }
    public UserModel Buddy { get; set; }
}

class UserDto
{
    public String Name { get; set; }
    public int Age { get; set; }
    public UserDto Buddy { get; set; }
}

The problem is that the Buddy property, unlike all the others, has a different type in the model and DTO classes. A UserModel is simply not assignable to a UserDto. The only exception to this is if the value is null.

For properties which are class types, instead of setting the target equal to the source you need to map the source type to the target type: UserModel -> UserDto. This can be done with a recursive call.

Before I show you the code which solves this issue, let's talk about naming for a minute. Calling your function Cast() is very misleading. The operation we are really doing here is taking some source object and mapping its property values onto some target object of a specific type (with possible recursive mappings for properties which are class types).

Given this terminology, here is some updated code which solves this specific issue:

public static T MapProperties<T>(object source)
{
    return (T)MapProperties(source, typeof(T));
}

public static object MapProperties(object source, Type targetType)
{
    object target = Activator.CreateInstance(targetType, nonPublic: false);
    Type sourceType = source.GetType();

    var sourcePropertyLookup = sourceType.GetProperties().ToDictionary(p => p.Name);
    var targetPropertyLookup = targetType.GetProperties().ToDictionary(p => p.Name);

    var commonProperties = targetPropertyLookup.Keys.Intersect(sourcePropertyLookup.Keys);
    foreach (var commonProp in commonProperties)
    {
        PropertyInfo sourceProp = sourcePropertyLookup[commonProp];
        PropertyInfo targetProp = targetPropertyLookup[commonProp];

        object sourcePropValue = sourceProp.GetValue(source);

        if(sourcePropValue == null || targetProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
        {
            targetProp.SetValue(target, sourceProp.GetValue(source));
        }
        else
        {
            object mappedValue = MapProperties(sourceProp.GetValue(source), targetProp.PropertyType);
            targetProp.SetValue(target, mappedValue);
        }
    }

    return target;
}

You can use this in the same way you've used your previous code:

static void Main(string[] args)
{
    var user = new UserModel
    {
        Name = "User McUserson",
        Age = 30,
        Buddy = new UserModel
        {
            Name = "Buddy McFriendly",
            Age = 28
        }
    };

    // This works!
    var userDto = MapProperties<UserDto>(user);  
}

Aside from some optimizations the key differences from your code is in the if-else block. There we check if we can assign the source value to the target directly, in which case we do what your code was doing so far. Otherwise it assumes we need to recursively map the value over. This new section is what solves the issue of converting a source property of a model class type to a target property of a DTO class type.