代码改变世界

对象映射工具AutoMapper介绍

2014-08-29 09:33 by JustRun, ... 阅读, ... 评论, 收藏, 编辑

AutoMapper是用来解决对象之间映射转换的类库。对于我们开发人员来说,写对象之间互相转换的代码是一件极其浪费生命的事情,AutoMapper能够帮助我们节省不少时间。

一. AutoMapper解决了什么问题?

要问AutoMapper解决了什么问题? 难道不是对象映射转换的问题吗?
当然是,不过我们可以问深入一些,为什么项目中会出现大量的对象映射转换?(以下对于非MVC项目也适用)

在现代的软件开发中,项目的层级更加的细分,而不同层级之间对于对象的需求是有区别的,这就需要在不同层级间传递数据的时候,必须要转换数据。

举一些实际具体的例子:
在持久层(数据访问层), 我们的User对象,可能是一个包含User表中所有字段的数据的对象,甚至包含了用户的Password信息。而在界面层,我们只是需要显示用户的name, email,不需要Password这些额外的信息,同时,它还需要用户的考勤信息,而这个信息来自于另外一张表。
这个例子中,能够发现不同层之间,我们对于数据对象的需求是不同的。
每个层都做了它们职责范围内的事情:
持久层关注数据,所以只提供数据对象,它无需知道外层如何使用这些数据对象,也无法知道。
界面层关注数据的呈现,它只关注它要显示的数据。

那么问题是,谁来弥补它们之间的鸿沟?DTO(Data Transfer Object)——数据传输对象。而AutoMapper就是解决其中涉及到的数据对象转换的工具。

clip_image001

在实际开发中,如果你还可以直接在Business层或者界面层直接使用持久层的对象,因为你认为这个关系不大,整个项目都是你自己控制的,虽然dirty,但是quick. 作为一个有些洁癖的程序员,我还是建议使用DTO在不同层级之间传递数据。因为当你做更高层级开发的时候,比如开发web service,WCF,Web API这些为系统外部提供接口的开发时候,你就回明白这些好的习惯和思维能够帮助你更加好的设计这些外部接口。

二. AutoMapper如何使用?

先来看一个简单的例子,这个例子是定义Order对象到OrderDto对象之间的映射。(我们把Order称呼为源类,OrderDto称呼为目标类)

Mapper.CreateMap<Order, OrderDto>();//创建映射关系Order –> OrderDto
OrderDto dto = Mapper.Map<OrderDto>(order);//使用Map方法,直接将order对象装换成OrderDto对象

智能匹配

AutoMapper能够自动识别和匹配大部分对象属性:

  • 如果源类和目标类的属性名称相同,直接匹配
  • 目标类型的CustomerName可以匹配源类型的Customer.Name
  • 目标类型的Total可以匹配源类型的GetTotal()方法

自定义匹配规则

AutoMapper还支持自定义匹配规则

Mapper.CreateMap<CalendarEvent, CalendarEventForm>()
                        //属性匹配,匹配源类中WorkEvent.Date到EventDate
                        .ForMember(dest => dest.EventDate, opt => opt.MapFrom(src => src.WorkEvent.Date))
                        .ForMember(dest => dest.SomeValue, opt => opt.Ignore())//忽略目标类中的属性
                        .ForMember(dest => dest.TotalAmount, opt => opt.MapFrom(src => src.TotalAmount ?? 0))//复杂的匹配
                        .ForMember(dest => dest.OrderDate, opt => opt.UserValue<DateTime>(DateTime.Now));固定值匹配

测试
当定义完规则后,可以使用下面的代码来验证配置是否正确。不正确抛出异常AutoMapperConfigurationException.

Mapper.AssertConfigurationIsValid();

三. AutoMapper处理多对一映射

我们开篇提到的问题中,说到界面显示User的name, email, 还有用户的考勤信息,而这些信息来自于2张不同的表。这就涉及到了多对一映射的问题,2个持久层对象需要映射到一个界面显示层的对象。

假设我们的持久层对象是这样的:

public class User
{
       public int Id { get; set; }
       public string Name { get; set; }
       public string Email { get; set; }
       public string Passworkd { get; set; }
       public DateTime Birthday { get; set; }
}

public class Evaluation
{
       public int Id { get; set; }
       public int Score { get; set; }
}

在Asp.net MVC中,我的界面显示层的ViewModel是这样的

public class UserViewModel
{
       public int Id { get; set; }
       public string Name { get; set; }
       public string Email { get; set; }
       public int Score { get; set; }
}

接下来,为了达到多对一的映射的目的,我们创建这个EntityMapper类

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

为了实现多个源对象映射一个目标对象,我们使用了AutoMapper的方法,从不同的源对象逐一匹配一个已经存在的目标对象。下面是实际使用在MVC中的代码:

public ActionResult Index()
{
          var userId = 23,
          var user = _userRepository.Get(userId);
          var score = _scoreRepository.GetScore(userId);
          var userViewModel = EntityMapper.Map<UserViewModel>(user, score);
          return this.View(userViewModel);
}

四. 使用Profile在Asp.net MVC项目中配置AutoMapper

ProfileAutoMapper中用来分离类型映射定义的,这样可以让我们的定义AutoMapper类型匹配的代码可以更加分散,合理和易于管理。

利用Profile, 我们可以更加优雅的在MVC项目中使用我们的AutoMapper. 下面是具体的方法:

1.  在不同层中定义Profile,只定义本层中的类型映射

继承AutoMapping的Profile类,重写ProfileName属性和Configure()方法。

public class ViewModelMappingProfile: Profile
{
       public override string ProfileName
       {
           get
           {
               return GetType().Name;
           }
       }

       protected override void Configure()
       {
           Mapper.CreateMap......
       }
}

2. 创建AutoMapperConfiguration, 提供静态方法Configure,一次加载所有层中Profile定义

public class AutoMapperConfiguration
{
       public static void Configure()
       {
           Mapper.Initialize(x => x.AddProfile<ViewModelMappingProfile>());
           Mapper.AssertConfigurationIsValid();
       }
}

3. Global.cs文件中执行

最后,在Global.cs文件中程序启动前,调用该方法

AutoMapperConfiguration.Configuration()