AutoMapper学习
在两个不同的类型对象之间传输数据,通常我们会用DTOs(数据传输对象),AutoMapper就是将一个对象自动转换为另一个对象的技术
背景
一些orm框架,在用到Entity的时候有一些开源代码用到了automapper(如:nopcommence),将数据对象转成DTO。比如在ORM中,与数据库交互用的Model模型是具有很多属性变量方法神马的。而当我们与其它系统(或系统中的其它结构)进行数据交互时,出于耦合性考虑或者安全性考虑或者性能考虑(总之就是各种考虑),我们不希望直接将这个Model模型传递给它们,这时我们会创建一个贫血模型来保存数据并传递。什么是贫血模型?贫血模型(DTO,Data Transfer Object)就是说只包含属性什么的,只能保存必须的数据,没有其它任何的多余的方法数据什么的,专门用于数据传递用的类型对象。在这个创建的过程中,如果我们手动来进行,就会看到这样的代码:
A a=new A();
a.X1=b.X1;
a.X2=b.X2;
...
...
...
return a; 太麻烦
此时,AutoMapper可以发挥的作用就是根据A的模型和B的模型中的定义,自动将A模型映射为一个全新的B模型。(不用一个属性一个属性的赋值)
好处:
1、 db或者模型 增加字段时,只需在DTO内部增加映射,赋值代码无需修改
2、隔离,前端收集各参数,不用管后端定义的模型。前后端用AutoMapper来做转换。
使用
Nuget引用:AutoMapper 版本不一样,里面的很多方法有些不一样
AutoMapper是基于约定的,因此在使用映射之前,我们需要先进行映射规则的配置。
我们要做的只是将要映射的两个类型告诉AutoMapper(调用Mapper类的Static方法CreateMap并传入要映射的类型):
Mapper.Initialize(cfg => { cfg.CreateMap<StudentEntity, StudentOutput>(); });
也可以将实体类 放在配置文件MapperProfile中
Mapper.Initialize(cfg => { cfg.AddProfile<MapperProfile>(); cfg.AddProfile<ProxyAdapterProfile>(); //可增加多个 });
注意:多次调用 Mapper.Initialize() 只有最后一次生效。所以只能用一个Mapper.Initialize。
【AutoMapper.7.0.1】
class MapperProfile : Profile { public MapperProfile() { CreateMap<StudentEntity, StudentOutput>(); var map = CreateMap<UploadResponseBase, UploadResult>(); //字段名称不一致,一次直接定义好所有字段的映射规则 map.ConvertUsing(s => new UploadResult { IsSuccess = s.success, FileUrl = s.clientUrl, ErrorMessage = s.rawFileName , datetimeStr = (s.datetime).ToString(), }); } }
【AutoMapper 4.2.1.0】
AutoMapper使用ForMember来指定每一个字段的映射规则:
protected override void Configure() { var mapResponst = CreateMap<Response, CResponse>(); mapResponst.ForMember(dest => dest.departure_date, opt => opt.MapFrom(src => src.DepartureDate.ToString("yyyy-MM-dd HH:mm:ss"))) .ForMember(dest => dest.ticket_price, opt => opt.MapFrom(src => src.TicketPrice)); var mapContacts = CreateMap<CContacts, PassengerInputEntity>(); mapContacts.ForMember(dest => dest.FirstName, opt => opt.MapFrom(src => src.First_Name)) .ForMember(dest => dest.LastName, opt => opt.MapFrom(src => src.Last_Name)) .ForMember(dest => dest.AreaCode, opt => opt.MapFrom(src => src.Area_Code)); }
然后就可以交给AutoMapper帮我们搞定一切了:
//实例化实体List List<StudentEntity> StudentList = new List<StudentEntity>(); //模拟数据 StudentList.Add(new StudentEntity { Id = 1, Age = 12, Gander = "boy", Name = "WangZeLing", Say = "Only the paranoid survive", Score = 99M }); //AuotMapper具体使用方法 将List<StudentEntity>转换为List<StudentOutput> List<StudentOutput> Output = Mapper.Map<List<StudentOutput>>(StudentList); Output.ForEach(output => Console.WriteLine(string.Format("name:{0},say:{1},score:{2}", output.Name, output.Say, output.Score)));
注:从 9.0 开始 Mapper.Initialize
方法就不可用了。
而是这么使用:
private MapperHelper() { var config = new MapperConfiguration(cfg => { //源,目标 cfg.CreateMap<MailAccountModel, MailAccountServiceModel>(); }); MapperInstance = config.CreateMapper(); }
解释
1、 AutoMapper给我们提供的Convention或Configuration方式并不是“异或的”,我们可以结合使用两种方式,为名称不同的字段配置映射规则,而对于名称相同的字段则忽略配置。
2、 在映射具有相同字段名的类型时,会自动转换
3、 不相同名称的属性则需要 指定映射字段,设置ConvertUsing或者ForMember..
4、 值为空的属性,AutoMapper在映射的时候会把相应属性也置为空
5、 如果传入一个空的AddressDto,AutoMapper也会帮我们得到一个空的Address对象。
Address address = Mapper.Map<AddressDto,Address>(null);
6、不需要映射的属性可以用Ignore忽略。【if有验证 目标类中的所有属性是否都被映射 时】
使用Ignore方法:
Mapper.CreateMap<Entity.Source, Entity.Destination>()
.ForMember(dest => dest.SomeValuefff, opt =>
{
opt.Ignore();
});
最佳实践
这段内容将讨论AutoMapper的规则写在什么地方的问题。
在上一段中,我们已经知道了如何使用AutoMapper进行简单的对象映射,但是,在实际的项目中,我们会有很多类进行映射(从Entity转换为Dto,或者从Entity转换为ViewModel等),这么多的映射如何组织将成为一个问题。
首先我们需要定义一个Configuration.cs的类,该类提供AutoMapper规则配置的入口,它只提供一个静态的方法,在程序第一次运行的时候调用该方法完成配置。
当有多个Profile的时候,我们可以这样添加:
public class Configuration { public static void Configure() { Mapper.Initialize(cfg => { cfg.AddProfile<Profiles.SourceProfile>(); cfg.AddProfile<Profiles.OrderProfile>(); cfg.AddProfile<Profiles.CalendarEventProfile>(); }); } }
在程序运行的时候,只需要调用Configure方法即可。
了解了这些实现以后,我们可以再项目中添加AutoMapper文件夹。
Configuration为我们的静态配置入口类;Profiles文件夹为我们所有Profile类的文件夹。如果是MVC,我们需要在Global中调用:
AutoMapper.Configuration.Configure();
关于List类型的转换
错误:
正确的:
//第一步初始化 Mapper.Initialize(cfg => { cfg.CreateMap<WorldA, WorldB>(); }); //声明一个集合 List<WorldA> worldAs = new List<WorldA>(); //声明一个对象,并赋值 WorldA world = new WorldA() { id = 1, wewe = 1.ToString() }; //将对象加入集合内部 worldAs.Add(world); //开始搬运 var b = Mapper.Map<List<WorldA>, List<WorldB>>(worldAs); //输出搬运后的值 Console.WriteLine(b[0].id);
问题:Missing type map configuration or unsupported mapping
重现:本地调试直接打开出错的页面,调试发现是ok的;然后先打开用到了mapper所在控制器对应的页面,再去打开出错的页面,是报错的。
从 GitHub 上签出 AutoMapper 的源代码一看 Mapper.Initialize() 的实现,恍然大悟。
public static void Initialize(Action<IMapperConfigurationExpression> config)
{
Configuration = new MapperConfiguration(config);
Instance = new Mapper(Configuration);
}
原来每次调用 Mapper.Initialize() 都会创建新的 Mapper 实例,也就是多次调用 Mapper.Initialize() 只有最后一次生效。
切记不要多处调用Mapper.Initialize()。
优化方法:【写一个工具类】
需程序集:AutoMapper
/// <summary> /// 优化AutoMap映射工具,解决AutoMap只能Initialize一次的问题 /// </summary> public class AutoMapperManager { /// <summary> /// 存储所有的profile /// </summary> static ConcurrentBag<Profile> Profiles; static AutoMapperManager() { Profiles = new ConcurrentBag<Profile>(); } /// <summary> /// 新增Profile,必須放在靜態構造函數里 /// </summary> /// <param name="profile"></param> public static void AddProfile(Profile profile) { Profiles.Add(profile); } /// <summary> /// 初始化,可以多次调用,同时之前的Profile也会生效 /// </summary> public static void Initialize() { Mapper.Initialize(config => { Profiles.ToList().ForEach(file => { config.AddProfile(file); }); }); } }
其他地方需要用mapper的地方 调用方式:
AutoMapperManager.AddProfile(new Profile1());
AutoMapperManager.AddProfile(new Profile2());
AutoMapperManager.Initialize();
参考:
https://www.cnblogs.com/jobs2/p/3503990.html