Dynamisches Mappen zwischen Typen

Manchmal kommt es vor, dass ein anderer Typ als der gegebene notwendig ist und das, obwohl dieser die gleichen Eigenschaften besitzt. Und mit diesem Satz möchte ich den nächsten Beitrag einleiten, denn diesmal geht es darum Typen zu Mappen. Dies kann erforderlich sein, wenn von einem SOAP-Service Datentypen vorgegeben werden, diese aber in eigene verpackt werden müssen, um diese mit speziellen Eigenschaften zu versehen und anschließend in der Oberfläche, im ViewModel oder im Model verwenden zu können. Im Prinzip bietet C# seit Version 2.0 dafür schon etwas passendes an, nämlich die generics. Aber um den Blogbeitrag sinnvoller zu gestalten, benötigen wir noch etwas – ein eigenes Attribut. Beginnen wir also mit diesem.

[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class IgnoreOnMapAttribute : Attribute 
{ }

Relativ simpel gehalten, soll uns dieses Attribut lediglich die Möglichkeit schaffen bestimmte Eigenschaften beim Mapping zu überspringen. Es muss lediglich existieren. 

Weiter geht es nun mit unseren generischen Methoden, welche in den meisten Fällen für die Anforderung des Typen-Mappings genutzt werden können. Hierfür habe ich drei unterschiedliche generische Methoden gebaut, um möglichst viele Fälle abdecken zu können. Beginnen wir mit einem einfachen 1:1 mappen. Also einen Quelltyp auf einen Zieltypen.

private TResult MapResult<TResult, TSource>(TSource source)
    where TResult : new()
{
    TResult result = new TResult();
    foreach (PropertyInfo resultProperty in result.GetType().GetProperties())
    {
        if (source.GetType().GetProperties().All(item => item.Name != resultProperty.Name))
            continue;

        PropertyInfo sourceProperty = source.GetType().GetProperty(resultProperty.Name);

        if (Attribute.GetCustomAttributes(resultProperty).Any(item => item.GetType() == typeof(IgnoreOnMapAttribute)) ||
            Attribute.GetCustomAttributes(sourceProperty).Any(item => item.GetType() == typeof(IgnoreOnMapAttribute)))
            continue;

        object value = sourceProperty.GetValue(source, new object[0]);
        MethodInfo setMethodInfo = result.GetType().GetProperty(resultProperty.Name).GetSetMethod(true);

        if (setMethodInfo != null)
            setMethodInfo.Invoke(result, new[] { value });
    }

    return result;
}

Dies mag auf den ersten Blick etwas verwirrend aussehen, aber im Prinzip ist dieses als „Reflection“ bezeichnete Vorgehen auch schnell erklärt. In der Definition benutzen wir die eigentlich nicht existierenden Typen „TResult“ sowie „TSource“, diese werden jedoch direkt rechts vom Methodennamen in den <> definiert – zumindest für den Compiler. Anschließend folgt die Zeile „where TResult : new()“. Diese sagt aus, dass es uns möglich sein muss den Konstruktor des eingesetzten Typen aufzurufen. Weiterhin holen wir uns mittels „result.GetType().GetProperties()“ sämtliche Eigenschaften, die in dem Typen enthalten sind (theoretisch müsste hier auch „typeof(TResult).GetProperties()“ gehen). Im Inneren der Schleife prüfen wir zunächst, ob unser Quelltyp auch eine Eigenschaft mit diesem Namen enthält (Hinweis: Hier müsste eigentlich auch noch geprüft werden, ob die Typen der Eigenschaft im Quelltypen und im Zieltypen identisch sind). Im Weiteren Kontext holen wir uns nun die Eigenschaft des Quelltypen und prüfen, ob der Quelltyp oder der Zieltyp ggf. mit unseren „IgnoreOnMap“-Attribut ausgestattet worden sind. Falls dem nicht so ist, holen wir uns den Wert aus der Eigenschaft im Quelltypen und die „Set“-Methode der Eigenschaft in der  Zielmethode, welche wir anschließend mit dem Wert auslösen. Und das war auch schon die ganze Magie.

Kommen wir also als nächstes zu dem Fall, dass wir mehrere Quelltypen besitzen und diese in einem Zieltypen vereinen wollen. Auch hier gilt wieder das wir theoretisch den Typen der Eigenschaft zusätzlich prüfen müssten.

private TResult MapResult<TResult>(params object[] sources) 
    where TResult : new()
{
    TResult result = new TResult();
    foreach (object source in sources)
    {
        foreach (PropertyInfo resultProperty in result.GetType().GetProperties())
        {
            if (source.GetType().GetProperties().All(item => item.Name != resultProperty.Name))
                continue;

            PropertyInfo sourceProperty = source.GetType().GetProperty(resultProperty.Name);

            if (Attribute.GetCustomAttributes(resultProperty).Any(item => item.GetType() == typeof(IgnoreOnMapAttribute)) ||
                Attribute.GetCustomAttributes(sourceProperty).Any(item => item.GetType() == typeof(IgnoreOnMapAttribute)))
                continue;

            object value = sourceProperty.GetValue(source, new object[0]);
            MethodInfo setMethodInfo = result.GetType().GetProperty(resultProperty.Name).GetSetMethod(true);

            if (setMethodInfo != null)
                setMethodInfo.Invoke(result, new[] { value });
        }
    }

    return result;
}

Die einzige Änderung im Vergleich zur ersten Methode besteht in der zusätzlichen Schleife. Hier sollte jedoch auch beachtet werden, dass immer die Eigenschaften des letzten Eintrages gültig sind. Möchte man einen bereits existierenden Wert nicht überschreiben müsste dies zusätzlich geprüft werden. Was hier nur unter Umständen auffällt, ist der verwendete Parameter. Dieser wird genauso auch z.B. bei „string.Format“ verwendet und macht nichts anderes als das wir die Methode mit beliebig vielen Parametern aufrufen können. Außerdem können unsere Parameter auch beliebige Typen haben (ansonsten wäre das zusammenfügen ja auch ggf. überflüssig, wenn immer alles überschrieben wird).

Zum Abschluss noch ein letzter Fall. Ein 1:1 Mapping von einem Array auf ein Array.

private TResult[] MapArrayResult<TResult, TSource>(IEnumerable<TSource> sources)
    where TResult : new()
{
    IList<TResult> results = new List<TResult>();
    foreach (TSource source in sources)
    {
        TResult result = new TResult();
        foreach (PropertyInfo resultProperty in result.GetType().GetProperties())
        {
            if (source.GetType().GetProperties().All(item => item.Name != resultProperty.Name))
                continue;

            PropertyInfo sourceProperty = source.GetType().GetProperty(resultProperty.Name);

            if (Attribute.GetCustomAttributes(resultProperty).Any(item => item.GetType() == typeof(IgnoreOnMapAttribute)) ||
                Attribute.GetCustomAttributes(sourceProperty).Any(item => item.GetType() == typeof(IgnoreOnMapAttribute)))
                continue;

            object value = sourceProperty.GetValue(source, new object[0]);
            MethodInfo setMethodInfo = result.GetType().GetProperty(resultProperty.Name).GetSetMethod(true);

            if (setMethodInfo != null)
                setMethodInfo.Invoke(result, new[] { value });
        }

        results.Add(result);
    }

    return results.ToArray();
}

Auch hier ist wieder nichts Besonderes sondern alles wie zuvor. Da dieser Beitrag aus einer Mail-Anfrage stammt, hoffe ich, dass ich der entsprechenden Person und vielleicht auch dem ein oder anderen damit helfen konnte.