AutoMapper-Konvertierung aus mehreren Quellen

79

Angenommen, ich habe zwei Modellklassen:

public class People {
   public string FirstName {get;set;}
   public string LastName {get;set;}
}

Haben Sie auch eine Klasse Telefon:

public class Phone {
   public string Number {get;set;}
}

Und ich möchte wie folgt in ein PeoplePhoneD konvertieren:

public class PeoplePhoneDto {
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public string PhoneNumber {get;set;}
}

Sagen wir in meinem Controller, den ich habe:

var people = repository.GetPeople(1);
var phone = repository.GetPhone(4);

// normally, without automapper I would made
return new PeoplePhoneDto(people, phone) ;

Ich kann anscheinend kein Beispiel für dieses Szenario finden. Ist das möglich ?

Hinweis: Das Beispiel ist nur für diese Frage nicht real.

Bart Calixto
quelle
@Andrei Ich stimme zwar zu, dass es ähnlich scheint, aber es ist ein Unterschied in dem Problem, das es zu lösen versucht. Aus dieser Frage ist auch schwer zu verstehen, wie sie auf diese Frage zutreffen würde.
Bart Calixto
Warum nicht PeoplePhoneDtoein Peopleund PhoneMitglied haben?
Crush
Weil ich das nicht aussetzen möchte.
Bart Calixto
3
Abstimmung zur Wiedereröffnung - obwohl ich denke, dass stackoverflow.com/questions/12429210/… ein Duplikat ist, scheint es (zusammen mit seiner einen Antwort) etwas zu lokalisiert zu sein, um als kanonisch angesehen zu werden. Es gibt Präzedenzfälle für doppelte Fragen, die nicht zählen, wenn sie nicht gut genug beantwortet wurden, um die Angelegenheit zu klären.
Brilliand

Antworten:

102

Sie können nicht viele Quellen direkt einem einzelnen Ziel zuordnen. Sie sollten die Karten einzeln anwenden, wie in der Antwort von Andrew Whitaker beschrieben . Sie müssen also alle Zuordnungen definieren:

Mapper.CreateMap<People, PeoplePhoneDto>();
Mapper.CreateMap<Phone, PeoplePhoneDto>()
        .ForMember(d => d.PhoneNumber, a => a.MapFrom(s => s.Number));

Erstellen Sie dann das Zielobjekt mit einer dieser Zuordnungen und wenden Sie andere Zuordnungen auf das erstellte Objekt an. Und dieser Schritt kann mit einer sehr einfachen Erweiterungsmethode vereinfacht werden:

public static TDestination Map<TSource, TDestination>(
    this TDestination destination, TSource source)
{
    return Mapper.Map(source, destination);
}

Die Verwendung ist sehr einfach:

var dto = Mapper.Map<PeoplePhoneDto>(people)
                .Map(phone);
Sergey Berezovskiy
quelle
Es gibt einen Abstraktions- IMapper über AutoMapper, um mehrere Quellen einem einzigen Ziel zuzuordnen, das ich verwende.
Ilya Palkin
@Sergey Berezovskiy, ich habe die Zuordnungen erstellt, die Erweiterungsmethode in der PeoplePhoneDto-Klasse hinzugefügt und Ihre Verwendung kopiert (dh ich habe alles Notwendige kopiert), aber es wird der Fehler "Keine Überladung für Methode Map nimmt 1 Argument" angezeigt. Was vermisse ich? Ich benutze Automapper 4.2.1.
OfirD
@ HeyJude stellen Sie sicher, dass Ihre MapErweiterungsmethode an dem Punkt sichtbar ist, an dem Sie das Mapping durchführen (dh die korrekte Verwendung der Direktive wird hinzugefügt)
Sergey Berezovskiy
Das ist gut, aber ich mag es nicht, statische Map zu verwenden, weil ich sie nicht verspotten kann, also werde ich ilyas Imapper-Abstraktion versuchen
sensei
Wird dadurch die DTO-Klasse 2 Mal für jede Karte oder nur einmal erstellt?
Anestis Kivranoglou
18

Sie könnten ein Tupledafür verwenden:

Mapper.CreateMap<Tuple<People, Phone>, PeoplePhoneDto>()
    .ForMember(d => d.FirstName, opt => opt.MapFrom(s => s.Item1.FirstName))
    .ForMember(d => d.LastName, opt => opt.MapFrom(s => s.Item1.LastName))
    .ForMember(d => d.Number, opt => opt.MapFrom(s => s.Item2.Number ));

Falls Sie mehr Quellmodelle hätten, können Sie eine andere Darstellung (Liste, Wörterbuch oder etwas anderes) verwenden, die alle diese Modelle als Quelle zusammenfasst.

Der obige Code sollte vorzugsweise in einer AutoMapperConfiguration-Datei abgelegt, einmal und global festgelegt und dann gegebenenfalls verwendet werden.

AutoMapper unterstützt standardmäßig nur eine einzige Datenquelle. Es gibt also keine Möglichkeit, mehrere Quellen direkt festzulegen (ohne sie in eine Sammlung einzuschließen), denn wie würden wir dann wissen, was passiert, wenn beispielsweise zwei Quellmodelle Eigenschaften mit demselben Namen haben?

Es gibt jedoch einige Problemumgehungen, um dies zu erreichen:

public static class EntityMapper
{
    public static T Map<T>(params object[] sources) where T : class
    {
        if (!sources.Any())
        {
            return default(T);
        }

        var initialSource = sources[0];

        var mappingResult = Map<T>(initialSource);

        // Now map the remaining source objects
        if (sources.Count() > 1)
        {
            Map(mappingResult, sources.Skip(1).ToArray());
        }

        return mappingResult;
    }

    private static void Map(object destination, params object[] sources)
    {
        if (!sources.Any())
        {
            return;
        }

        var destinationType = destination.GetType();

        foreach (var source in sources)
        {
            var sourceType = source.GetType();
            Mapper.Map(source, destination, sourceType, destinationType);
        }
    }

    private static T Map<T>(object source) where T : class
    {
        var destinationType = typeof(T);
        var sourceType = source.GetType();

        var mappingResult = Mapper.Map(source, sourceType, destinationType);

        return mappingResult as T;
    }
}

Und dann:

var peoplePhoneDto = EntityMapper.Map<PeoplePhoneDto>(people, phone);

Aber um ganz ehrlich zu sein, obwohl ich AutoMapper bereits seit einigen Jahren verwende, musste ich nie Mapping aus mehreren Quellen verwenden. In Fällen, in denen ich beispielsweise mehrere Geschäftsmodelle in meinem Einzelansichtsmodell benötigte, habe ich diese Modelle einfach in die Ansichtsmodellklasse eingebettet.

In Ihrem Fall würde es also so aussehen:

public class PeoplePhoneDto {
    public People People { get; set; }
    public Phone Phone { get; set; }
}
Paweł Bejger
quelle
3
Also muss ich ein Tupel erstellen, bevor ich das Mapping mache. Ich frage mich, was die wirklichen Vorteile von Automapper sind ... klingt ein wenig übertrieben. Gibt es eine Möglichkeit, das Erstellen eines anderen Typs (Tupel, Dic usw.) zu vermeiden?
Bart Calixto
danke für deine antwort, es bringt mich dazu, viel über automapper zu verstehen. Wenn Sie die API verfügbar machen, modellieren Sie Daten sorgfältig auf eine Weise, die manchmal nicht mit eingebetteten Modellen erwünscht ist, da Sie Ihre "domänenbezogenen" Probleme an einen Verbraucher übertragen und ich versuche, Clients das Konsumieren ohne verschachtelte Typen zu erleichtern . Wenn es für meinen eigenen internen Gebrauch wäre, würde ich mit der eingebetteten Option sicher weitermachen.
Bart Calixto
1
Das, was PeoplePhoneDtoSie vorgeschlagen haben, sieht gut aus, aber ich denke immer noch, dass die Zuordnung aus mehreren Quellen nützlich ist, insbesondere bei der Zuordnung von Ansichtsmodellen. Ich denke, die meisten realen Szenarien erfordern mehrere Quellen, um ein Ansichtsmodell zu erstellen. Ich nehme an, Sie könnten Ansichtsmodelle erstellen, die nicht abgeflacht sind, um das Problem zu umgehen, aber ich denke, es ist eine gute Idee, die Ansichtsmodelle zu erstellen, ohne sich darum zu kümmern, wie das Geschäftsschema aussieht.
Der Muffin-Mann
Interessiert es Automapper auch, in welcher Reihenfolge sich die Typen im Tupel befinden? ist Tuple<People, Phone>das gleiche wie Tuple<Phone, People>?
Der Muffin-Mann
2
@TheMuffinMan Tuplemacht das erste Typargument als Item1, das zweite als Item2usw. verfügbar . In diesem Sinne ist die Reihenfolge wichtig.
Josh M.
1

Vielleicht klingt es nach einem alten Beitrag, aber es gibt möglicherweise einige Leute, die immer noch mit demselben Problem zu kämpfen haben. In der Dokumentation zur AutoMapper IMapper Map-Funktion können wir dasselbe vorhandene Zielobjekt für die Zuordnung aus einer neuen Quelle wiederverwenden, vorausgesetzt, Sie haben bereits einen erstellt Wenn Sie für jede Quelle ein Ziel im Profil zuordnen, können Sie diese einfache Erweiterungsmethode verwenden:

 public static class MappingExtentions
    {
        public static TDestination Map<TDestination>(this IMapper mapper, params object[] sources) where TDestination : new()
        {
            return Map(mapper, new TDestination(), sources);
        }

        public static TDestination Map<TDestination>(this IMapper mapper, TDestination destination, params object[] sources) where TDestination : new()
        {
            if (!sources.Any())
                return destination;

            foreach (var src in sources)
                destination = mapper.Map(src, destination);

            return destination;
        }
    }

Bitte beachten Sie, dass ich eine Einschränkung für den Zieltyp erstellt habe, die besagt, dass es sich um einen instanziierbaren Typ handeln muss. Wenn Ihr Typ nicht so ist, verwenden Sie default(TDestination)stattdessen new TDestination().

Warnung : Diese Art der Zuordnung ist manchmal etwas gefährlich, da die Eigenschaften der Zielzuordnung möglicherweise von mehreren Quellen überschrieben werden und das Nachverfolgen des Problems in größeren Apps Kopfschmerzen bereiten kann. Es gibt eine lose Problemumgehung, die Sie anwenden können. Sie können dies jedoch wie folgt tun wieder ist es überhaupt keine feste Lösung:

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string PhoneNumber { get; set; }
    }

    public class Contact
    {
        public string Address { get; set; }
        public string PhoneNumber { get; set; }
        public string Other{ get; set; }
    }


    public class PersonContact
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Address{ get; set; }
        public string PhoneNumber { get; set; }
    }

    public class PersonMappingProfile : MappingProfile
    {
        public PersonMappingProfile()
        {
            this.CreateMap<Person, PersonContact>();

            
            this.CreateMap<Phone, PersonContact>()
                .ForMember(dest => dest.PhoneNumber, opt => opt.MapFrom((src, dest) => dest.PhoneNumber ?? src.PhoneNumber)) // apply mapping from source only if the phone number is not already there, this way you prevent overwriting already initialized props
                .ForAllOtherMembers(o => o.Ignore());
        }
    }
Mohammad Khodabandeh
quelle
0

Wenn Sie ein Szenario haben, in dem der Zieltyp aus einer der Quellen zugeordnet werden soll und Sie Linq-Projektionen verwenden möchten, können Sie Folgendes tun.

    Mapper.CreateMap<People, PeoplePhoneDto>(MemberList.Source);
    Mapper.CreateMap<Phone, PeoplePhoneDto>(MemberList.Source)
          .ForMember(d => d.PhoneNumber, a => a.MapFrom(s => s.Number));

    CreateMap<PeoplePhoneDto,(People,Phone)>(MemberList.Destination)
           .ForMember(x => x.Item1, opts => opts.MapFrom(x => x))
           .ForMember(x => x.Item2, opts => opts.MapFrom(x => x.PhoneNumber))
           .ReverseMap();

Ich brauchte dies hauptsächlich für Cross-Apply-Abfragen wie diese.

       var dbQuery =
          from p in _context.People
          from ph in _context.Phones
             .Where(x => ...).Take(1)
          select ValueTuple.Create(p, ph);
       var list = await dbQuery
          .ProjectTo<PeoplePhoneDto>(_mapper.ConfigurationProvider)
          .ToListAsync();
Gevorg Narimanyan
quelle
0

Ich würde eine Erweiterungsmethode wie folgt schreiben:

    public static TDestination Map<TSource1, TSource2, TDestination>(
        this IMapper mapper, TSource1 source1, TSource2 source2)
    {
        var destination = mapper.Map<TSource1, TDestination>(source1);
        return mapper.Map(source2, destination);
    }

Dann wäre die Verwendung:

    mapper.Map<People, Phone, PeoplePhoneDto>(people, phone);
Ranga
quelle
0

Es gibt bereits viele Optionen, aber keine passt wirklich zu dem, was ich wollte. Ich bin letzte Nacht eingeschlafen und hatte den Gedanken:

Nehmen wir an, Sie möchten Ihre beiden Klassen zuordnen Peopleund PhoneaufPeoplePhoneDto

public class People {
   public string FirstName {get;set;}
   public string LastName {get;set;}
}

+

public class Phone {
   public string Number {get;set;}
}

=

public class PeoplePhoneDto {
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public string PhoneNumber {get;set;}
}

Alles, was Sie wirklich brauchen, ist eine weitere Wrapper-Klasse für Automapper-Zwecke.

public class PeoplePhone {
    public People People {get;set;}
    public Phone Phone {get;set;}
}

Und dann definieren Sie das Mapping:

CreateMap<PeoplePhone, PeoplePhoneDto>()

Und benutze es

var dto = Map<PeoplePhoneDto>(new PeoplePhone
{
    People = people,
    Phone = phone,
});
Jancarius
quelle