Weniger Code dank AutoMapper

AutoMapper ist ein einfach zu verwendender Objekt-Objekt Mapper. Wer mehr als einmal ein Geschäftsobjekt auf ein DTO oder Viewmodel abbilden musste dachte sich wohl: Warum muss ich diesen Code selber schreiben? Mit AutoMapper gibt es genau dafür eine Lösung.

 

Installation

Wie bei fast allen Bibliotheken kann man auch hier wieder NuGet zur Installation verwenden. Wer die Package Manager Konsole der GUI-Anwendung vorzieht bekommt AutoMapper mit diesem Befehl:

PM> Install-Package AutoMapper

 

Ausgangslage mit ganz einfachen Daten

Als Beispiel für den manuellen Weg wie man ein Objekt auf ein anderes überträgt dienen uns die beiden Klassen Basic und BasicDto:

public class Basic
{
    public string A { get; set; }
    public string B { get; set; }
    public string C { get; set; }
}

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

private readonly Basic basic = new Basic() { A = "[A]", B = "[B]", C = "[C]" };

Bisher werden die meisten Entwickler wohl einen Code geschrieben haben der diesem hier sehr ähnlich ist:

[TestMethod]
public void MappingTheOldWay()
{
    BasicDto dto = new BasicDto();
    dto.A = basic.A;
    dto.B = basic.B;

    Assert.AreEqual("[A]", dto.A);
    Assert.AreEqual("[B]", dto.B);
}

Mit AutoMapper muss man erst ein Mapping definieren und danach das gewünschte Ausgangsobjekt übergeben:

[TestMethod]
public void MappingSimpleObjectWithAutomapper()
{
    Mapper.CreateMap<Basic, BasicDto>();

    BasicDto dto = Mapper.Map<Basic, BasicDto>(basic);

    Assert.AreEqual("[A]", dto.A);
    Assert.AreEqual("[B]", dto.B);
}

So lange man nur wenige Werte übertragen muss ist ein Helfer wie AutoMapper zugegebenermassen kein grosser Gewinn. Anders sieht es aus wenn die Objekte komplexer und umfangreicher werden.

 

Nicht alle Werte automatisch übernehmen

Per Konvention werden automatisch alle Werte übernommen bei denen im Ausgangs- und im Zielobjekt ein Feld mit gleichem Namen existiert. Will man dieses Verhalten ändern kann man AutoMapper sehr einfach anpassen:

[TestMethod]
public void MappingNotAllValues()
{
    Mapper.CreateMap<Basic, BasicDto>()
        .ForMember(dest => dest.B, opt => opt.Ignore());

    BasicDto dto = Mapper.Map<Basic, BasicDto>(basic);

    Assert.AreEqual("[A]", dto.A);
    Assert.IsNull(dto.B);
}

Mit dest wir hier das Zielobjekt (Destination) referenziert und für das Property B eine entsprechende Option (opt) gesetzt. Die Bezeichner „dest“ und „opt“ kann man nach Belieben umbenennen. Ich halte mich hier an die im Wiki verwendeten Namen.

 

Werte verändern

AutoMapper lässt einem Werte nicht nur direkt übernehmen, sondern erlaubt einem auch diese nach Belieben zu verändern:

[TestMethod]
public void MappingModifyValues()
{
    Mapper.CreateMap<Basic, BasicDto>()
        .ForMember(dest => dest.A, 
            opt => opt.MapFrom(src => String.Format("{0} {1}", src.A, src.C)));

    BasicDto dto = Mapper.Map<Basic, BasicDto>(basic);

    Assert.AreEqual("[A] [C]", dto.A);
    Assert.AreEqual("[B]", dto.B);
}

Wie viel Funktionalität man in ein Mapping stecken will sollte man sich aber gut überlegen. Bei zu vielen Sonderfällen hat man immer noch die Möglichkeit selber den passenden Code zu schreiben und für diesen Fall auf AutoMapper zu verzichten.

 

Verschachtelte Objekte

Richtig interessant wird es wenn das Ausgangsobjekt aus mehreren Objekten besteht. Benennt man die Felder im Zielobjekt nach dem Muster KlassennameFeldname übernimmt AutoMapper wiederum die ganze Arbeit:

public class Customer
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public int Number { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string Street { get; set; }
    public int Zip { get; set; }
    public string Place { get; set; }
    public string Country { get; set; }
}

public class CustomerDto
{
    public string LastName { get; set; }
    public string FirstName { get; set; }
    public string AddressStreet { get; set; }
    public int AddressZip { get; set; }
    public string AddressPlace { get; set; }
    public string NotMapped { get; set; }
}

[TestMethod]
public void MappingComplexObjects()
{
    Mapper.CreateMap<Customer, CustomerDto>()

    Address address = new Address() { Street = "Main", Country = "CH" };
    Customer customer = new Customer() { LastName = "Graber", Address = address };

    CustomerDto dto = Mapper.Map<Customer, CustomerDto>(customer);

    Assert.AreEqual("Graber", dto.LastName);
    Assert.AreEqual("Main", dto.AddressStreet);
    Assert.IsNull(dto.NotMapped);
}

Gefällt einem dies nicht kann man die Felder per opt.MapFrom wie schon erklärt aus einem beliebigen Feld (oder Wert) übernehmen.

 

Konfiguration überprüfen

Im vorherigen Beispiel wurde das Feld NotMapped nicht gesetzt. Da es im Ausgangsobjekt kein entsprechendes Feld gibt wird es von AutoMapper auch nicht gefüllt. Falls man dies als Fehler ansieht kann man die Konfiguration durch AutoMapper überprüfen lassen:

[TestMethod]
public void ValidateMapping()
{
    Mapper.CreateMap<Customer, CustomerDto>();
    Mapper.AssertConfigurationIsValid();
}

Durch das nicht vorhandene Feld NotMapped wird man beim Ausführen diese Fehlermeldung erhalten:

Test method TestProject1.UnitTest1.ValidateMapping threw exception:
AutoMapper.AutoMapperConfigurationException:
Unmapped members were found. Review the types and members below.
Add a custom mapping expression, ignore, add a custom resolver, or modify the
source/destination type

 

Wohin mit der Konfiguration?

Es genügt AutoMapper beim Start der Applikation über die Methode Mapper.CreateMap() zu konfigurieren. Je nach Anwendungstyp kann man dies einmalig in der Main-Methode oder in der Datei Global.asax machen. So wird der notwendige Code für AutoMapper gegenüber den hier gezeigten Beispielen noch einmal kleiner.

 

Fazit

Mit AutoMapper hat man ein Werkzeug zur Verfügung mit dem man nicht mehr selber den ganz banalen Mapping-Code schreiben muss. Wird es komplexer bietet einem AutoMapper eine Vielzahl von Einstellungsmöglichkeiten. Allerdings gilt es wie bei jedem Werkzeug genau zu prüfen ob und in welchem Umfang der Einsatz sinnvoll ist.