Nick Coad
Nick Coad

Reputation: 3694

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?

Upvotes: 3

Views: 83

Answers (1)

Thorkil Holm-Jacobsen
Thorkil Holm-Jacobsen

Reputation: 7686

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

A Dictionary is the right way to go for this use case, as an arbitrary number of registration can be made and we would like to ensure fast lookup. 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 like so:

public class TypeMapping : IStructuralEquatable
{
    public Type From { get; private set; }
    public Type To { get; private set; }

    public TypeMapping (Type from, Type to)
    {
        From = from;
        To = to;
    }

    public override int GetHashCode()
    {
        return ((IStructuralEquatable) this).GetHashCode(EqualityComparer<Object>.Default);
    }

    int IStructuralEquatable.GetHashCode(IEqualityComparer comparer)
    {
        var hash = 17;
        unchecked
        {
            hash = hash * 31 + From.GetHashCode();
            hash = hash * 31 + To.GetHashCode();
        }
        return hash;
    }

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

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

        var otherMapping = other as TypeMapping;
        if (otherMapping == null) return false;

        return comparer.Equals(From, otherMapping.From) && comparer.Equals(To, otherMapping.To);
    }
}

And then you would have your Dictionary in the Mapping class look like this (with corresponding changes to Registration and Map methods):

private readonly static Dictionary<TypeMapping, Delegate> Mappings = new Dictionary<TypeMapping, Delegate>();

Upvotes: 3

Related Questions