Nick Coad Nick Coad - 2 months ago 7
C# Question

Correct datatype to use for Type mappings?

I'd like to implement a basic mapping system similar to AutoMapper, but all explicit mappings, no convention-based mappings.

To do this I've written a class that is supposed to maintain a register of "mapping" functions and look them up on demand in order to map one type to another. The usage I'm envisaging is like this:

Somewhere in startup:

Mapper.Register<TypeA, TypeB>(typeA =>
{
var typeB = new TypeB()
{
Property1 = typeA.Property1
};

return typeB;
}


And then when I want to perform the mapping...

TypeA typeA = new TypeA();
TypeB typeB = Mapper.Map<TypeA, TypeB>(typeA);


Currently I've used a
Dictionary<Tuple<Type, Type>, Delegate>
to store this mapping register, but it's not working as I had hoped...

public static class Mapper
{
private readonly static Dictionary<Tuple<Type, Type>, Delegate> Mappings = new Dictionary<Tuple<Type, Type>, Delegate>();

public static void Register<TSource, TDestination>(Func<TSource, TDestination> mappingFunction)
where TSource : class
where TDestination : class
{
Delegate mappingFunc;

if (Mappings.TryGetValue(new Tuple<Type, Type>(typeof (TSource), typeof (TDestination)), out mappingFunc))
Mappings[new Tuple<Type, Type>(typeof (TSource), typeof (TDestination))] = mappingFunction;
else
Mappings.Add(new Tuple<Type, Type>(typeof (TSource), typeof (TDestination)), mappingFunction);
}

public static TDestination Map<TSource, TDestination>(TSource src)
where TSource : class
where TDestination : class
{
Delegate mappingFunc;

if (!Mappings.TryGetValue(new Tuple<Type, Type>(typeof (TSource), typeof (TDestination)), out mappingFunc))
throw new Exception("Invalid mapping: no mapping found for requested types.");

var func = mappingFunc as Func<TSource, TDestination>;

if (func == null)
throw new Exception("Invalid mapping: no mapping found for requested types.");

return func.Invoke(src);
}
}


When this code is used, registering the mappings works fine, but retrieving them from the dictionary fails, and I think it's because of the third line in the
Map
method:

Mappings.TryGetValue(new Tuple<Type, Type>(typeof (TSource), typeof (TDestination)), out mappingFunc)


This line always fails to pass the test, and I think if I understand correctly, it's because Tuple is a reference type, so a new instance of
Tuple<Type, Type>
, regardless of what
Item1
and
Item2
are, will never match any key in the
Dictionary
.

So,
Dictionary<Tuple<Type, Type>, Delegate>
is not appropriate for storing this mapping register. In that case, what data type is?

Answer

When I try to run your above code, I get the result I expect:

Mapper.Register<string, Regex>(s => new Regex("not using the given string"));
Mapper.Register<string, Regex>(s => new Regex(s));
var regex = Mapper.Map<string, Regex>(@"\w*");
// regex is now the Regex object instantiated with @"\w*"

In other words, your code seems to be functioning correctly.

This line always fails to pass the test, and I think if I understand correctly, it's because Tuple is a reference type, so a new instance of Tuple<Type, Type>, regardless of what Item1 and Item2 are, will never match any key in the Dictionary.

Actually, the implementation of Tuple does support what you are trying to do. The Dictionary uses GetHashCode and Equals methods under the hood to make lookups. The Equals method usually checks reference equality for objects, but the Tuple source code shows that it is specifically programmed to use structural equality:

    public override Boolean Equals(Object obj) {
        return ((IStructuralEquatable) this).Equals(obj, EqualityComparer<Object>.Default);;
    }

    Boolean IStructuralEquatable.Equals(Object other, IEqualityComparer comparer) {
        if (other == null) return false;

        Tuple<T1, T2> objTuple = other as Tuple<T1, T2>;

        if (objTuple == null) {
            return false;
        }

        return comparer.Equals(m_Item1, objTuple.m_Item1) && comparer.Equals(m_Item2, objTuple.m_Item2);
    }

I think, however, that I would probably still create my own type TypeMapping to use for the Dictionary instead of Tuple<Type, Type>, because I think tuples do not express the intend/usage. Just remember to override GetHashCode and Equals so it functions correctly with Dictionary.

If you are still having trouble making it work, you will have to explain how you think it fails.