AutoMapper convierte desde múltiples fuentes


80

Digamos que tengo dos clases modelo:

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

También tenga un teléfono de clase:

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

Y quiero convertirme a un PeoplePhoneD de esta manera:

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

Digamos que en mi controlador tengo:

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

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

Parece que no puedo encontrar ningún ejemplo para este escenario. Es posible ?

Nota: El ejemplo no es real, solo para esta pregunta.


@Andrei, aunque estoy de acuerdo en que parece similar, hay una diferencia en el problema que está tratando de resolver. también es difícil entender a partir de esa pregunta cómo se aplicaría a este.
Bart Calixto

¿Por qué no PeoplePhoneDtotener un miembro Peopley Phone?
aplastar

Porque eso no es lo que quiero exponer.
Bart Calixto

3
Votar para reabrir: aunque creo que stackoverflow.com/questions/12429210/… es un duplicado, (junto con su única respuesta) parece un poco demasiado localizado para ser considerado canónico. Existe un precedente de preguntas duplicadas que no cuentan si no fueron respondidas lo suficientemente bien como para resolver el asunto.
Brilliand

Respuestas:


103

No puede asignar directamente muchas fuentes a un solo destino; debe aplicar los mapas uno por uno, como se describe en la respuesta de Andrew Whitaker . Entonces, debes definir todas las asignaciones:

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

Luego cree el objeto de destino mediante cualquiera de estas asignaciones y aplique otras asignaciones al objeto creado. Y este paso se puede simplificar con un método de extensión muy simple:

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

El uso es muy simple:

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

Hay una abstracción IMapper sobre AutoMapper para mapear varias fuentes en un solo destino que yo uso.
Ilya Palkin

@Sergey Berezovskiy, creé las asignaciones, agregué el método de extensión en la clase PeoplePhoneDto y copié y pegué su uso (es decir, copié y pegué todo lo necesario), pero aparece el error "No hay sobrecarga para el método El mapa toma 1 argumento". ¿Qué me estoy perdiendo? Estoy usando Automapper 4.2.1.
OfirD

@HeyJude asegúrese de que su Mapmétodo de extensión sea visible en el punto en que realiza el mapeo (es decir, se agrega la directiva de uso correcto)
Sergey Berezovskiy

Esto es bueno, pero no me gusta usar el mapa estático debido a que no puedo burlarme de él, así que intentaré la abstracción de ilyas Imapper
sensei

¿Esto creará el DTO Clase 2 veces para cada mapa o solo una vez?
Anestis Kivranoglou

19

Podrías usar un Tuplepara esto:

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

En caso de que tenga más modelos fuente, puede usar una representación diferente (Lista, Diccionario o algo más) que reunirá todos estos modelos como fuente.

El código anterior debe colocarse preferiblemente en algún archivo de configuración de AutoMapper, establecerse una vez y de forma global y luego usarse cuando corresponda.

AutoMapper de forma predeterminada admite solo una fuente de datos. Entonces, no hay posibilidad de configurar directamente múltiples fuentes (sin envolverlo en una colección) porque entonces, ¿cómo sabríamos qué en caso de que, por ejemplo, dos modelos de fuente tengan propiedades con los mismos nombres?

Aunque hay alguna solución para lograr esto:

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

Y entonces:

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

Pero para ser bastante honesto, a pesar de que estoy usando AutoMapper desde hace algunos años, nunca he tenido la necesidad de usar mapas de múltiples fuentes. En los casos en los que, por ejemplo, necesitaba varios modelos de negocio en mi modelo de vista única, simplemente incrustaba estos modelos dentro de la clase de modelo de vista.

Entonces, en su caso, se vería así:

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

3
Así que debo crear una tupla antes de hacer el mapeo, me pregunto cuáles son los beneficios reales de automapper ... suena un poco exagerado. ¿Hay alguna forma de evitar la creación de otro tipo (tupla, dic, etc.)?
Bart Calixto

gracias por tu respuesta, me lleva a entender mucho sobre automapper. La cuestión es que, cuando expone la API, modela cuidadosamente los datos de maneras que a veces no se desea tener modelos integrados porque transfiere sus problemas 'relacionados con el dominio' a un consumidor y estoy tratando de facilitar que los clientes consuman sin tipos anidados. . Si fuera para mi propio uso interno, seguro que seguiré adelante con la opción integrada.
Bart Calixto

1
Lo PeoplePhoneDtoque sugirió se ve bien, pero sigo pensando que el mapeo de múltiples fuentes es útil, sobre todo en modelos de vista de mapeo. Creo que la mayoría de los escenarios del mundo real requieren múltiples fuentes para construir un modelo de vista. Supongo que podría crear modelos de vista que no se aplanan para solucionar el problema, pero creo que es una buena idea crear los modelos de vista sin importar cómo se ve el esquema comercial.
The Muffin Man

¿También le importa a automapper en qué orden están los tipos en la tupla? es Tuple<People, Phone>lo mismo que Tuple<Phone, People>?
The Muffin Man

2
@TheMuffinMan Tupleexpone el primer argumento de tipo como Item1, el segundo como Item2, etc. En ese sentido, el orden importa.
Josh M.

1

Escribiría un método de extensión de la siguiente manera:

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

Entonces el uso sería:

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

1

tal vez parezca una publicación antigua, pero puede haber algunos tipos que todavía luchan con el mismo problema, refiriéndose a la documentación de la función AutoMapper IMapper Map , podemos reutilizar el mismo objeto de destino existente para el mapeo de una nueva fuente, siempre que ya haya creado un mapa para cada origen a destino en el perfil, entonces puede usar este método de extensión simple:

tenga en cuenta que he creado una restricción para el tipo de destino que dice que debe ser un tipo capaz de crear instancias. si tu tipo no es así, usa en default(TDestination)lugar de new TDestination().

Advertencia : este tipo de mapeo es un poco peligroso a veces porque las propiedades del mapeo de destino pueden ser sobrescritas por múltiples fuentes y rastrear el problema en aplicaciones más grandes puede ser un dolor de cabeza, hay una solución suelta que puede aplicar, puede hacer lo siguiente, pero de nuevo, no es una solución sólida en absoluto:


0

Si tiene un escenario en el que el tipo de destino debe mapearse desde una de las fuentes y desea utilizar proyecciones de linq, puede hacer lo siguiente.

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

Necesitaba esto principalmente para consultas de aplicación cruzada como esta.

       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

Ya se ofrecen muchas opciones, pero ninguna se ajusta realmente a lo que quería. Me estaba quedando dormido anoche y tuve el pensamiento:

Digamos que desea asignar sus dos clases, Peopley PhoneparaPeoplePhoneDto

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

Todo lo que realmente necesita es otra clase contenedora para propósitos de Automapper.

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

Y luego defina el mapeo:

CreateMap<PeoplePhone, PeoplePhoneDto>()

Y usarlo

var dto = Map<PeoplePhoneDto>(new PeoplePhone
{
    People = people,
    Phone = phone,
});
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.