AutoMapper从多个来源转换


79

假设我有两个模型类:

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

还有一个班级的电话:

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

我想这样转换为PeoplePhoneDto:

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虽然我确实同意它看起来很相似,但是它试图解决的问题有所不同。从这个问题也很难理解它如何适用于这个问题。
Bart Calixto 2014年

为什么不让PeoplePhoneDto一个PeopleandPhone成员呢?
暗恋

因为那不是我想要公开的。
Bart Calixto 2014年

3
投票重新打开-虽然我确实认为stackoverflow.com/questions/12429210/…是重复的,但它(以及它的一个答案)似乎过于局限,无法被认为是规范的。有重复问题的先例,如果问题回答得不够好,就无法解决问题。
Brilliand 2014年

Answers:


102

您不能直接将多个源映射到单个目标-您应按照安德鲁·惠特克Andrew Whitaker)的回答中的说明一张一张地应用地图。因此,您必须定义所有映射:

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

在AutoMapper上有一个抽象的IMapper,可以将多个源映射到我使用的单个目标中。
伊利亚·帕金

@Sergey Berezovskiy,我创建了映射,在PeoplePhoneDto类中添加了扩展方法,并复制粘贴了您的用法(即,我复制粘贴了所需的所有内容),但是出现了“方法Map不需要重载1个参数”的错误。我想念什么?我正在使用Automapper 4.2.1。
OfirD

@HeyJude确保Map在执行映射时扩展方法可见(即添加了正确的using指令)
Sergey Berezovskiy

这很好,但是由于无法模拟静态地图,我不喜欢使用静态地图,因此我将尝试使用ilyas Imapper抽象
sensei

这会为每个地图创建2次DTO类还是一次?
Anestis Kivranoglou19年

18

您可以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的真正好处是什么……听起来有些矫kill过正。有什么方法可以避免创建其他类型(元组,dic等)?
Bart Calixto 2014年

感谢您的回答,这使我对自动映射器有了很多了解。事实是,当您公开API时,您会以有时不希望使用嵌入式模型的方式对数据进行仔细建模,因为您将与“域相关”的问题转移给了使用者,而我正在努力使客户端易于使用而无需嵌套类型。如果是供我自己内部使用,我肯定会继续使用嵌入式选项。
Bart Calixto 2014年

1
PeoplePhoneDto你的建议很好看,但我仍然认为,从多个来源映射在映射视图模型有用,最显着的。我认为大多数现实场景都需要多个源来构建视图模型。我想您可以创建视图模型,这些视图模型不会展平以解决问题,但是我认为创建视图模型而不关心业务模式的外观是一个好主意。
松饼人

自动映射器还会关心元组中类型的顺序吗?是Tuple<People, Phone>一样的Tuple<Phone, People>
松饼人

2
@TheMuffinManTuple将第一个类型参数公开为Item1,将第二个参数公开为,依此类推Item2。顺序很重要。
Josh M.

1

也许听起来像是一篇老文章,但也许​​有些人仍在为同一问题而苦苦挣扎,请参阅AutoMapper IMapper Map函数文档,只要您已经创建了一个将每个源映射到配置文件中的目标,然后可以使用以下简单的扩展方法:

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

请注意,我已经为目标类型创建了一个约束,该约束说它必须是可实例化的类型。如果您的类型不是这样,请使用default(TDestination)代替new TDestination()

警告:这种类型的映射有时会有些危险,因为目标映射属性可能会被多个源覆盖,并且在较大的应用程序中跟踪问题可能令人头疼,您可以采用宽松的解决方法,但是可以按照以下步骤进行操作,但是同样,这根本不是一个可靠的解决方案:

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

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

我将编写一个扩展方法,如下所示:

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

0

已经提供了很多选项,但是没有一个选项真正适合我的需求。昨晚我睡着了,心想:

比方说,你想你的两个类映射,PeoplePhonePeoplePhoneDto

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,
});
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.