【问题标题】:AutoMapper convert from multiple sourcesAutoMapper 从多个来源转换
【发布时间】:2014-02-20 04:55:00
【问题描述】:

假设我有两个模型类:

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 虽然我同意它看起来很相似,但它试图解决的问题有所不同。从这个问题也很难理解它将如何应用于这个问题。
  • 为什么不让PeoplePhoneDto 拥有PeoplePhone 成员?
  • 因为这不是我想暴露的。
  • 投票重新打开 - 虽然我确实认为 stackoverflow.com/questions/12429210/… 是重复的,但它(连同它的一个答案)似乎有点过于本地化而不能被视为规范。有重复问题的先例,如果没有得到足够好的答案来解决问题,则不算在内。

标签: c# automapper


【解决方案1】:

使用 FluentAPI 样式以获得更好的发现和指导使用。

 public static class MapperExtensions
    {
        public static IMultiMapBuilder<TDestination> StartMultiMap<TDestination>(this IMapper mapper, object source)
        {
            return new MultiMapBuilder<TDestination>(mapper, source);
        }
    }

    public interface IMultiMapBuilder<T>
    {
        IMultiMapBuilder<T> Then<TSource>(TSource source);
        T Map();
    }

    public class MultiMapBuilder<T> : IMultiMapBuilder<T>
    {
        private readonly IMapper _mapper;
        private readonly T _mappedObject;
        public MultiMapBuilder(IMapper mapper, object source)
        {
            _mapper = mapper;
            _mappedObject = mapper.Map<T>(source);
        }
        public IMultiMapBuilder<T> Then<TSource>(TSource source)
        {
            _mapper.Map(source, _mappedObject);
            return this;
        }

        public T Map()
        {
            return _mappedObject;
        }
    }

示例用法:

//-- By IMapper Extension
var mapped = _mapper.StartMultiMap<SomeType>(source1)
     .Then(source2)
     .Then(source3)
     .Map();

or 

//-- new instance of MultiMapBuilder
var mapped = new MultiMapBuilder<SomeType>(_mapper, source1)
     .Then(source2)
     .Then(source3)
     .Map();


【讨论】:

    【解决方案2】:

    AutoMapper 9.0 中有一个重大更改,不再为静态 Mapper 提供 API。所以,我们现在需要使用一个实例。对于那些使用较新版本的人,使用带有/不带有目标对象的推断类型的扩展方法如下:

    public static class AutoMapperExtensions
    {
        public static TDestination Map<TDestination>(this IMapper mapper, params object[] source) where TDestination : class
        {
          TDestination destination = mapper.Map<TDestination>(source.FirstOrDefault());
    
          foreach (var src in source.Skip(1))
            destination = mapper.Map(src, destination);
    
          return destination;
        }
    
        public static TDestination Map<TDestination>(this IMapper mapper, TDestination destination, params object[] source) where TDestination : class
        {
          foreach (var src in source)
            destination = mapper.Map(src, destination);
    
          return destination;
        }
    }
    

    【讨论】:

      【解决方案3】:

      如果您使用的是 C# 7+,请尝试此操作(@Paweł Bejger 的答案略有不同,这将使其更简单):

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

      然后像这样使用它:

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

      是的,您需要在输入的每一侧使用一对括号,这不是错误。其背后的原因是您传递了一个(而不是两个)恰好是 (People, Phone) 元组的单一来源。

      【讨论】:

      • 这对我有帮助。不需要额外的扩展方法。很简单。
      【解决方案4】:

      也许这听起来是一个旧帖子,但可能有些人仍在为同样的问题而苦苦挣扎,参考AutoMapper IMapper Map function documentation,我们可以重用相同的现有目标对象来从新源映射,前提是您已经创建配置文件中每个源到目标的映射,然后您可以使用这个简单的扩展方法:

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

      【讨论】:

      • 您能给我们举个使用例子吗?
      【解决方案5】:

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

      假设您想将PeoplePhone 这两个类映射到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,
      });
      

      【讨论】:

        【解决方案6】:

        我会写一个扩展方法如下:

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

        【讨论】:

          【解决方案7】:

          如果您有一个场景,当目标类型应该从一个源映射并且您想要使用 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();
          

          【讨论】:

            【解决方案8】:

            您可以为此使用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; }
            }
            

            【讨论】:

            • 所以我必须在进行映射之前创建一个元组,我想知道 automapper 的真正好处是什么……听起来有点矫枉过正。有什么办法可以避免创建另一种类型(元组、字典等)?
            • 感谢您的回答,它让我对 automapper 有了很多了解。问题是,当您公开 API 时,您会仔细地以有时不需要嵌入模型的方式对数据进行建模,因为您将“与域相关”的问题转移给了消费者,而我正在努力让客户在没有嵌套类型的情况下轻松消费.如果它是供我自己内部使用的,我肯定会继续使用嵌入式选项。
            • 您建议的PeoplePhoneDto 看起来不错,但我仍然认为从多个来源进行映射很有用,尤其是在映射视图模型中。我认为大多数现实世界的场景都需要多个来源来构建视图模型。我想您可以创建未扁平化的视图模型来解决此问题,但我认为创建视图模型而不关心业务模式的外观是个好主意。
            • automapper 还关心类型在元组中的顺序吗? Tuple&lt;People, Phone&gt;Tuple&lt;Phone, People&gt; 一样吗?
            • @TheMuffinMan Tuple 将第一个类型参数公开为Item1,第二个公开为Item2,依此类推。从这个意义上说,顺序很重要。
            【解决方案9】:

            您不能直接将多个来源映射到单个目的地 - 您应该一一应用映射,如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。
            • @HeyJude 确保您的 Map 扩展方法在您进行映射时可见(即添加了正确的 using 指令)
            • 扩展方法对我不起作用(AutoMapper v9 Mapper.Map 不是静态的)替代方法只是使用您的映射器实例,例如 var foo = mapper.Map(people); mapper.Map(phone, foo);
            • @bluedot 扩展方法可以工作,但方式不同,我也在使用 .net core 和 AutoMapper 10,我遇到了同样的问题,推荐的解决方案都不适合我的情况,所以我创建了自己的方法并与大家分享,如果您仍然遇到同样的问题,请查看我的答案。
            猜你喜欢
            • 2016-12-28
            • 2012-02-15
            • 1970-01-01
            • 2013-11-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-05-10
            相关资源
            最近更新 更多