Ari Roth
Ari Roth

Reputation: 5534

Mapping similar objects using reflection: Object does not match target type

I am at a complete loss here, despite looking at multiple SO posts and anything else I can think of.

My goal here is to make a really, really simple mapper. Something I can basically use as a tool in some unit tests. It doesn't need to be sophisticated or anything -- just map high-level primitive and string values of one object to another. So the basic algorithm is:

  1. Get all properties from TFrom
  2. Get all properties from TTo
  3. Get all properties that are in both, matched by name.
    • I know this could be a bug in that they could have the same name but a different type, but let's set that aside. It's not what I'm running into here -- the properties and types match between classes.
  4. Create an instance of TTo that we can copy to.
  5. For each property that was mapped between the objects:
    1. Get the value off of the from object
    2. Convert the value to the type of the property
    3. Set the value on the to object

The problem is that no matter what I do, and no matter what the type of the property is (int or string, for example) I get the following:

Object does not match the target type.

Here is the code I'm using:

public TTo Map<TFrom, TTo>(TFrom from)
{
    if (from == null) return default;

    var fromProps = GetProperties(typeof(TFrom));
    var toProps = GetProperties(typeof(TTo));

    // Props that can be mapped from one to the other
    var propsToCopy = fromProps.Intersect(toProps, new PropertyComparer()).ToList();

    var returnObject = (TTo)Activator.CreateInstance(typeof(TTo));

    foreach (var prop in propsToCopy)
    {
        // Copy the values
        var fromValue = prop.GetValue(from, null);
        var convertedValue = Convert.ChangeType(fromValue, prop.PropertyType);
        prop.SetValue(returnObject, convertedValue, null);
    }

    return returnObject;
}

public PropertyInfo[] GetProperties(Type objectType)
{
    var allProps = objectType.GetProperties(
        BindingFlags.Public | BindingFlags.Instance);

    return allProps.Where(p => p.PropertyType.IsPrimitive ||
        p.PropertyType == typeof(string)).ToArray();
}

private class PropertyComparer : IEqualityComparer<PropertyInfo>
{
    public bool Equals(PropertyInfo x, PropertyInfo y)
    {
        return x.Name.Equals(y.Name);
    }

    public int GetHashCode(PropertyInfo obj)
    {
        return obj.Name.GetHashCode();
    }
}

And here's an example of a way I would call it, with sample classes:

public class Foo 
{
    public string StringProp { get; set; }
    public int IntProp { get; set; }
}

public class FooOther
{
    public string StringProp { get; set; }
    public int IntProp { get; set; }
}

var foo = new Foo { IntProp = 1, StringProp = "foo" };
var mappedFoo = Map<Foo, FooOther>(foo);

About the only hint I've gotten out of Visual Studio is from the watch window: if the property type is a string, the watch window reports the type of convertedValue as object. If the property type is an int, the watch window reports object {int}.

Upvotes: 3

Views: 1752

Answers (1)

Jonathon Chase
Jonathon Chase

Reputation: 9704

The PropertyInfo you are using is still coupled to the type the property it is representing is a member of, so you aren't able to use it to set the value of an object of another type without the error you are getting.

Here's a shortened example of the behavior:

public class A {
    public string Id {get;set;}
}
public class B {
    public string Id {get;set;}
}

void Main()
{
    var test = new A() { Id = "Test"};
    var prop = test.GetType().GetProperty("Id");

    var b = (B)Activator.CreateInstance(typeof(B));

    var fromValue = prop.GetValue(test);
    var converted = Convert.ChangeType(fromValue, prop.PropertyType);
    prop.SetValue(b, converted, null); // Exception
}

This makes sense if you think of the PropertyInfo as a member of A. To fix this, you'll want to get a property info that's specific to your type. I can fix up my example with the following:

var propTo = typeof(B).GetProperty(prop.Name);
propTo.SetValue(b, converted, null);
Console.WriteLine(b.Id); // Output: Test

Bringing that together, if you change the contents of your foreach to the following you should be in the clear:

foreach (var prop in propsToCopy)
{
    // Copy the values
    var fromValue = prop.GetValue(from, null);
    var convertedValue = Convert.ChangeType(fromValue, prop.PropertyType);
    var propTo = typeof(TTO).GetProperty(prop.Name);
    propTo.SetValue(returnObject, convertedValue, null);
}

Upvotes: 5

Related Questions