Перетворення AutoMapper з кількох джерел


80

Скажімо, у мене є два модельні класи:

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

Також мати клас телефону:

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

І я хочу перетворити на PeoplePhoneDдо такого:

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

Скажімо, у моєму контролері я маю:

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

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

Я не можу знайти жодного прикладу для цього сценарію. Чи можливо це?

Примітка: Приклад не є реальним, лише для цього питання.


@Andrei, хоча я згоден, це здається схожим, це різниця в проблемі, яку він намагається вирішити. також важко зрозуміти з цього питання, як це стосуватиметься цього.
Барт Каліксто

Чому б не зробити PeoplePhoneDtoa Peopleта Phoneучасником?
розчавити

Тому що це не те, що я хочу викрити.
Барт Каліксто

3
Голосування за повторне відкриття - хоча я і думаю, що stackoverflow.com/questions/12429210/… є дублікатом, він (разом із однією його відповіддю) здається занадто локалізованим, щоб вважати його канонічним. Існує прецедент для дублікатів запитань, не враховуючи, якщо на них не було дано достатньої відповіді, щоб вирішити питання.
Brilliand

Відповіді:


103

Ви не можете безпосередньо зіставити багато джерел з одним пунктом призначення - вам слід застосовувати карти по одному, як описано у відповіді Ендрю Вітакер . Отже, вам потрібно визначити всі відображення:

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

Потім створіть об’єкт призначення за будь-яким із цих зіставлення та застосуйте інші зіставлення до створеного об’єкта. І цей крок можна спростити дуже простим методом розширення:

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

Використання дуже просте:

var dto = Mapper.Map<PeoplePhoneDto>(people)
                .Map(phone);

Існує абстракція IMapper над AutoMapper для відображення кількох джерел в єдине призначення, яке я використовую.
Ілля Палкін

@ Сергій Березовський, я створив зіставлення, додав метод розширення в клас PeoplePhoneDto і скопіював ваше використання (тобто, я скопіював все необхідне), але я отримую помилку "Немає перевантаження для методу Карта займає 1 аргумент". Чого мені не вистачає? Я використовую Automapper 4.2.1.
OfirD

@HeyJude переконайтеся, що ваш Mapметод розширення видно в точці, де ви робите картографію (тобто додано директиву правильного використання)
Сергій Березовський

Це добре, але я не люблю користуватися статичною картою через те, що не можу знущатись над нею, тому спробую абстракцію ilyas Imapper
сенсей

Це створить клас DTO 2 рази для кожної карти або лише один раз?
Anestis Kivranoglou

19

Ви можете використовувати Tupleдля цього:

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

Якщо у вас буде більше вихідних моделей, ви можете використовувати інше подання (Список, Словник чи щось інше), яке буде збирати всі ці моделі разом як джерело.

Вищезазначений код слід переважно розмістити в якомусь файлі AutoMapperConfiguration, встановити один раз і в усьому світі, а потім використовувати, коли це застосовно.

AutoMapper за замовчуванням підтримує лише одне джерело даних. Отже, немає можливості встановити безпосередньо кілька джерел (без загортання в колекцію), бо як тоді ми могли б знати, що у випадку, якщо, наприклад, дві моделі джерел мають властивості з однаковими іменами?

Для досягнення цього є певні шляхи вирішення:

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

І потім:

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

Але, чесно кажучи, хоча я вже кілька років використовую AutoMapper, мені ніколи не було необхідності використовувати картографування з кількох джерел. У випадках, коли, наприклад, мені потрібні були кілька бізнес-моделей у моїй єдиній моделі перегляду, я просто вбудовував ці моделі в клас моделі представлення.

Тож у вашому випадку це буде виглядати так:

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

3
Тому я повинен створити кортеж перед тим, як робити картографування, мені цікаво, які справжні переваги automapper ... звучить трохи надмірно. Чи є спосіб уникнути створення іншого типу (кортеж, дік тощо)?
Барт Каліксто

дякую за вашу відповідь, це змушує мене зрозуміти багато про automapper. Справа в тому, що коли ви виставляєте API, ви ретельно моделюєте дані таким чином, що іноді вбудовані моделі не є бажаними, оскільки ви передаєте свої проблеми, пов'язані з доменом, споживачеві, і я намагаюся полегшити споживання клієнтами без вкладених типів . Якби це було для мого власного внутрішнього використання, я точно піду вперед із вбудованою опцією.
Барт Каліксто

1
Запропоноване PeoplePhoneDtoвами виглядає добре, але я все ще вважаю, що картографування з кількох джерел є корисним, особливо в моделях подання карт. Я думаю, що більшість реальних сценаріїв вимагають декількох джерел для побудови моделі подання. Я припускаю, що ви могли б створити моделі подання, які не сплющені, щоб обійти проблему, але я думаю, що це гарна ідея створювати моделі подання, не дбаючи про те, як виглядає бізнес-схема.
Мафін,

Також автоматизатору важливо, в якому порядку є типи в кортежі? це Tuple<People, Phone>те саме, що Tuple<Phone, People>?
Мафін,

2
@TheMuffinMan Tupleвиставляє аргумент першого типу як Item1, другий як Item2тощо. У цьому сенсі порядок має значення.
Джош М.

1

Я б написав метод розширення, як показано нижче:

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

Тоді використання буде таким:

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

1

можливо, це звучить як стара публікація, але, можливо, деякі хлопці все ще борються з тією ж проблемою, посилаючись на документацію щодо функції AutoMapper IMapper Map , ми можемо використати той самий існуючий об'єкт призначення для зіставлення з нового джерела, за умови, що ви вже створили карту для кожного джерела до пункту призначення у профілі, тоді ви можете використовувати цей простий метод Розширення:

зауважте, що я створив обмеження для типу призначення, яке говорить, що це має бути тип, що підтримує екземпляр. якщо ваш тип не такий, використовуйте default(TDestination)замістьnew TDestination() .

Попередження : цей тип зіставлення іноді буває трохи небезпечним, оскільки властивості відображення цільового призначення можуть бути замінені різними джерелами, і відстеження проблеми у великих програмах може спричинити головний біль. Ви можете застосувати нерішучий спосіб вирішення, як описано нижче, але знову ж це зовсім не тверде рішення:


0

Якщо у вас є сценарій, коли тип призначення повинен бути зіставлений з одного з джерел, і ви хочете використовувати проекції linq, ви можете зробити наступне.

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

Мені це потрібно було здебільшого для таких запитів із перехресним застосуванням.

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

0

Є вже багато варіантів, але жоден з них насправді не відповідає тому, що я хотів. Вчора я засинав і думав:

Припустимо , ви хочете , щоб зіставити два класи, Peopleі PhoneвPeoplePhoneDto

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

Все, що вам дійсно потрібно - це ще один клас обгортки для цілей Automapper.

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

А потім визначте відображення:

CreateMap<PeoplePhone, PeoplePhoneDto>()

І використовувати його

var dto = Map<PeoplePhoneDto>(new PeoplePhone
{
    People = people,
    Phone = phone,
});
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.