目录
引言
WPF应用程序的三层架构:Model->ViewModel->View。个人理解就是将后台的运行配置文件或则结果Model转换为可以与窗体交互的ViewModel,ViewModel 再与窗体进行深度绑定,实现后台与前端的解耦。ViewModel作为中间介质包含Model的开放属性,同时又有和View交互的衍生属性或则命令Command。Mapster提供了一种Model<->ViewModel的映射方法,通过Adapt扩展方法实现属性相互映射。
高性能对象映射框架 Mapster
Mapster的高性能可由以下对比列表得出:
Method | Mean | StdDev | Error | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|
'Mapster 6.0.0' | 108.59 ms | 1.198 ms | 1.811 ms | 31000.0000 | - | - | 124.36 MB |
'Mapster 6.0.0 (Roslyn)' | 38.45 ms | 0.494 ms | 0.830 ms | 31142.8571 | - | - | 124.36 MB |
'Mapster 6.0.0 (FEC)' | 37.03 ms | 0.281 ms | 0.472 ms | 29642.8571 | - | - | 118.26 MB |
'Mapster 6.0.0 (Codegen)' | 34.16 ms | 0.209 ms | 0.316 ms | 31133.3333 | - | - | 124.36 MB |
'ExpressMapper 1.9.1' | 205.78 ms | 5.357 ms | 8.098 ms | 59000.0000 | - | - | 236.51 MB |
'AutoMapper 10.0.0' | 420.97 ms | 23.266 ms | 35.174 ms | 87000.0000 | - | - | 350.95 MB |
对比其他框架占用更少的内存, 拥有更好的性能。
1、简单映射
默认会将两个类型中相同名称的属性进行映射
//映射到新对象
var destObject = sourceObject.Adapt<Destination>();
//映射到现有对象
sourceObject.Adapt(destObject);
扩展方法 Adapt,为避免装箱/拆箱 请使用第一个版本指定类型
var dest = src.Adapt<TSource, TDestination>();
var dest = src.Adapt<TDestination>();
2、数据类型
2.1、基本类型
基本类型的转换 ,例如: int/bool/dobule/decimal
,包括可空的基本类型。
只要C#支持类型转换的类型,那么在 Mapster 中也同样支持转换。
decimal i = 123.Adapt<decimal>(); //equal to (decimal)123
2.2、枚举类型
Mapster 会自动把枚举映射到数字类型,同样也支持 字符串到枚举 和 枚举到字符串的映射。
.NET 默认实现 枚举/字符串 转换非常慢,Mapster 比 .NET 的默认实现快两倍。
在 Mapster 中,字符串转枚举,如果字符串为空或空字符串,那么枚举将初始化为第一个枚举值。
在Mapster中,也支持标记的枚举。
var e = "Read, Write, Delete".Adapt<FileShare>();
//FileShare.Read | FileShare.Write | FileShare.Delete
对于不同类型的枚举,Mapster 默认将值映射为枚举。调用 EnumMappingStrategy
方法可以指定枚举映射方式,如:
TypeAdapterConfig.GlobalSettings.Default
.EnumMappingStrategy(EnumMappingStrategy.ByName);
2.3、字符串类型
在 Mapster 中,将其它类型映射为字符串时,Mapster 将调用类型的 ToString
方法。
如果将字符串映射为类型时,Mapster 将调用类型的 Parse
方法。
var s = 123.Adapt<string>(); // 等同于: 123.ToString();
var i = "123".Adapt<int>(); // 等同于: int.Parse("123");
2.4、集合
包括列表、数组、集合、包括各种接口的字典之间的映射: IList<T>
, ICollection<T>
, IEnumerable<T>
, ISet<T>
, IDictionary<TKey, TValue>
等等…
var list = db.Pocos.ToList();
var target = list.Adapt<IEnumerable<Dto>>();
2.5、可映射对象
Mapster 可以使用以下规则映射两个不同的对象
- 源类型和目标类型属性名称相同。 例如:
dest.Name = src.Name
- 源类型有
GetXXXX
方法。例如:dest.Name = src.GetName()
- 源类型属性有子属性,可以将子属性的赋值给符合条件的目标类型属性,例如:
dest.ContactName = src.Contact.Name
或dest.Contact_Name = src.Contact.Name
class Staff {
public string Name { get; set; }
public int GetAge() {
return (DateTime.Now - this.BirthDate).TotalDays / 365.25;
}
public Staff Supervisor { get; set; }
...
}
struct StaffDto {
public string Name { get; set; }
public int Age { get; set; }
public string SupervisorName { get; set; }
}
var dto = staff.Adapt<StaffDto>();
//dto.Name = staff.Name, dto.Age = staff.GetAge(), dto.SupervisorName = staff.Supervisor.Name
可映射对象类型包括:
- 类
- 结构体
- 接口
- 实现
IDictionary<string, T>
接口的字典类型 - Record 类型 (类、结构体、接口)
对象转换为字典的例子:
var point = new { X = 2, Y = 3 };
var dict = point.Adapt<Dictionary<string, int>>();
dict["Y"].ShouldBe(3);
Record 类型的例子:
class Person {
public string Name { get; }
public int Age { get; }
public Person(string name, int age) {
this.Name = name;
this.Age = age;
}
}
var src = new { Name = "Mapster", Age = 3 };
var target = src.Adapt<Person>();
自动映射 Record 类型有一些限制:
- Record 类型属性必须没有
set
- 只有一个非空构造函数
- 构造函数中的所有参数名称必须与属性名称相同
如果不符合以上规则,需要增加额外的 MapToConstructor 配置
3、映射配置
使用 TypeAdapterConfig<TSource, TDestination>.NewConfig()
或 TypeAdapterConfig<TSource, TDestination>.ForType()
配置类型映射;
当调用 NewConfig
方法时,将会覆盖已存在的类型映射配置。
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Ignore(dest => dest.Age)
.Map(dest => dest.FullName,
src => string.Format("{0} {1}", src.FirstName, src.LastName));
如若不想覆盖之前已经创建好的映射配置,可以使用 TypeAdapterConfig<TSource, TDestination>.ForType()
;
ForType
方法与 NewConfig
的差别:如果指定类型映射配置不存在,那它将创建一个新的映射,如果指定类型的映射配置已存在,那么它将会扩展已有的映射配置,而不是删除或替换已有的映射配置。
TypeAdapterConfig<TSource, TDestination>
.ForType()
.Ignore(dest => dest.Age)
.Map(dest => dest.FullName,
src => string.Format("{0} {1}", src.FirstName, src.LastName));
3.1、全局设置
使用全局设置将映射策略应用到所有的映射配置。
TypeAdapterConfig.GlobalSettings.Default.PreserveReference(true);
对于特定的类型映射,你可以使用 TypeAdapterConfig<SimplePoco, SimpleDto>.NewConfig()
覆盖全局映射配置。
TypeAdapterConfig<SimplePoco, SimpleDto>.NewConfig().PreserveReference(false);
3.2、基于条件的映射
你可以使用 When
方法,当满足某个条件时,进行一些特定的映射操作。
下面的这个例子,当任何一个映射的 源类型和目标类型 相同时,不映射 Id
属性:
TypeAdapterConfig.GlobalSettings.When((srcType, destType, mapType) => srcType == destType)
.Ignore("Id");
在下面这个例子中,映射配置只对 IQueryable
生效:
TypeAdapterConfig.GlobalSettings.When((srcType, destType, mapType) => mapType == MapType.Projection)
.IgnoreAttribute(typeof(NotMapAttribute));
3.3、未知源类型
在不确定源类型的时候,使用 ForDestinationType
来创建针对于 目标类型 的映射配置。
比如使用 AfterMapping
在映射完成后调用目标类型对象的 Validate
方法:
TypeAdapterConfig.GlobalSettings.ForDestinationType<IValidator>()
.AfterMapping(dest => dest.Validate());
注意!在上面的代码段中指定目标类型为 IValidator
接口,那么将会把映射配置应用到所有实现了 IValidator
的类型。
3.4、泛型类型
如果映射的是泛型类型,可以通过将泛型类型传给 ForType
来创建设置.
TypeAdapterConfig.GlobalSettings.ForType(typeof(GenericPoco<>), typeof(GenericDto<>))
.Map("value", "Value");
4、映射配置的继承
4.1、源类型映射配置子类默认继承
Mapster 默认会把 源类型的 映射配置 应用到 源类型的子类。
如创建了一个 SimplePoco
-> SimpleDto
的映射配置:
TypeAdapterConfig<SimplePoco, SimpleDto>.NewConfig()
.Map(dest => dest.Name, src => src.Name + "_Suffix");
那么继承了 SimplePoco
的 DerivedPoco
也将应用同样的映射配置:
var dest = TypeAdapter.Adapt<DerivedPoco, SimpleDto>(src);
//dest.Name = src.Name + "_Suffix"
如果不希望子类使用父类映射配置,可以设置 AllowImplicitSourceInheritance
为 false
关闭继承:
TypeAdapterConfig.GlobalSettings.AllowImplicitSourceInheritance = false;
4.2、目标类型映射配置默认不继承
Mapster 默认不会把 目标类型的 映射配置 应用到 目标类型的子类。
可以设置 AllowImplicitDestinationInheritance
开启:
TypeAdapterConfig.GlobalSettings.AllowImplicitDestinationInheritance = true;
4.3、手动继承
可以通过 Inherits
方法显示的继承类型映射配置:
TypeAdapterConfig<DerivedPoco, DerivedDto>.NewConfig()
.Inherits<SimplePoco, SimpleDto>();
父类也可继承子类的映射关系
TypeAdapterConfig<Vehicle, VehicleDto>.NewConfig()
.Include<Car, CarDto>();
Vehicle vehicle = new Car { Id = 1, Name = "Car", Make = "Toyota" };
var dto = vehicle.Adapt<Vehicle, VehicleDto>();
dto.ShouldBeOfType<CarDto>();
((CarDto)dto).Make.ShouldBe("Toyota"); //The 'Make' property doesn't exist in Vehicle
5、配置实例
5.1、不同场景独立配置
在 Mapster 中,默认的配置实例为 TypeAdapterConfig.GlobalSettings
,如果需要在不同场景下有不同的映射配置,Mapster 提供了 TypeAdapterConfig
用于实现此需求:
var config = new TypeAdapterConfig();
config.Default.Ignore("Id");
如何给 配置实例 添加l类型映射配置?
直接使用 NewConfig
和 ForType
方法即可:
config.NewConfig<TSource, TDestination>()
.Map(dest => dest.FullName,
src => string.Format("{0} {1}", src.FirstName, src.LastName));
config.ForType<TSource, TDestination>()
.Map(dest => dest.FullName,
src => string.Format("{0} {1}", src.FirstName, src.LastName));
注意! 配置实例 在程序中一定要作为单例存在,否则会影响性能!
通过将 配置实例 作为参数传给 Adapt
方法来应用特定的类型映射配置:
var result = src.Adapt<TDestination>(config);
也可以创建一个指定 TypeAdapterConfig
的 Mapper
,使用 Mapper
来做映射:
var mapper = new Mapper(config);
var result = mapper.Map<TDestination>(src);
5.2、映射配置的复制与扩展
如果想从现有的配置中创建配置实例,可以使用 Clone
方法。
例如 复制全局配置实例 :
var newConfig = TypeAdapterConfig.GlobalSettings.Clone();
var newConfig = oldConfig.Clone();
Fork
方法内部直接调用 Clone
方法,但是使用 Fork
方法的形式与使用 Clone
方法有些许差别。
Fork 可以内部直接进行映射扩展
var forked = mainConfig.Fork(config =>
{
config.ForType<Poco, Dto>()
.Map(dest => dest.code, src => src.Id);
});
var dto = poco.Adapt<Dto>(forked);
以上代码等同于使用 Clone
方法实现的以下代码:
var forked = mainConfig.Clone();
forked.ForType<Poco, Dto>()
.Map(dest => dest.code, src => src.Id);
var dto = poco.Adapt<Dto>(forked);
5.3、配置入口和遗漏补充
映射配置应该只初始化并且只进行一次配置。因此在编写代码的时候不能将映射配置和映射调用放在同一个地方。多次设置同一个配置会抛出异常,如下:
config.ForType<Poco, Dto>().Ignore("Id");
var dto1 = poco1.Adapt<Dto>(config);
config.ForType<Poco, Dto>().Ignore("Id"); //<--- 这里将抛出异常,因为在这之前已经触发过了映射
var dto2 = poco2.Adapt<Dto>(config);
一般会放在程序的入口,如 Main方法 Startup方法。
但是难免在使用时会对映射配置进行补充,这时可通过Fork函数进行补充,不用担心性能问题,只有第一次使用配置实例时会编译,之后的调用将从缓存中获取。
var dto = poco.Adapt<Dto>(
config.Fork(forked => forked.ForType<Poco, Dto>().Ignore("Id"));
6、不同程序集的映射配置
将映射配置分布在许多不同的程序集上是比较常见的。
6.1、Scan
允许扫描程序集中的这些规则会很有帮助,这样你就有一些基本的方法来组织规则,并且不会忘记调用注册码。在某些情况下,甚至可能需要按特定顺序注册程序集,以便某些规则优先于其他规则。装配扫描有助于实现这一点。
程序集扫描很简单,只需在程序集中创建任意数量的实现,然后从类中调用:IRegister
Scan
TypeAdapterConfig
public class MyRegister : IRegister
{
public void Register(TypeAdapterConfig config)
{
config.NewConfig<TSource, TDestination>();
//OR to create or enhance an existing configuration
config.ForType<TSource, TDestination>();
}
}
要在全局级别进行扫描和注册,请执行以下操作:
TypeAdapterConfig.GlobalSettings.Scan(assembly1, assembly2, assemblyN)
对于特定的配置实例:
var config = new TypeAdapterConfig();
config.Scan(assembly1, assembly2, assemblyN);
6.2、Apply
如果使用其他程序集扫描库(如MEF),则可以使用Apply
方法轻松应用注册。
var registers = container.GetExports<IRegister>();
config.Apply(registers);
Apply
方法还允许您有选择地从一个或多个而不是每个组件中进行拾取。
var register = new MockingRegister();
config.Apply(register);
7、映射验证和编译
7.1、验证映射配置是否存在错误
调用 TypeAdapterConfig<Source, Destination>.NewConfg()
的 Compile
方法将验证 特定类型的映射配置是否存在错误;
调用 配置实例 的 Compile
方法以验证 配置实例中的映射配置 是否存在错误;
另外,如果启用了 显式映射 , 它还将包含没有在映射器中注册的类的错误。
// 验证特定配置
var config = TypeAdapterConfig<Source, Destination>.NewConfig();
config.Compile();
// 验证整个配置实例的配置
TypeAdapterConfig<Source, Destination>.NewConfig();
TypeAdapterConfig<Source2, Destination2>.NewConfig();
TypeAdapterConfig.GlobalSettings.Compile();
7.2、编译配置
Mapster 默认将在第一次调用映射时自动编译:
var result = poco.Adapt<Dto>();
你也可以通过调用 配置实例 或 特定映射配置的Compile
方法编译映射:
// 全局配置实例
TypeAdapterConfig.GlobalSettings.Compile();
// 配置实例
var config = new TypeAdapterConfig();
config.Compile();
// 特定配置
var config = TypeAdapterConfig<Source, Destination>.NewConfig();
config.Compile();
推荐在程序添加映射配置完成后调用一次 Compile
方法,可以快速验证 映射配置中是否存在错误,而不是在运行到某一行业务代码时触发错误降低效率。
注意!调用 Compile
方法前应该完成所有的映射配置,调用 Compile
方法之后 配置实例 就不允许添加修改其它映射配置!
8、自定义映射
8.1、自定义属性映射关系
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Map(dest => dest.FullName,
src => string.Format("{0} {1}", src.FirstName, src.LastName));
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Map(dest => dest.Gender, // dest.Gender: Genders.Male 或 Genders.Female 枚举类型
src => src.GenderString); // src.GenderString: "Male" 或 "Female" 字符串类型
8.2、条件映射
可以通过设置 Map
方法的第三个参数,实现在 源对象 满足某些条件下进行映射;
当存在多个条件的情况下,Mapster 会依次往下执行判断条件是否满足,直到满足条件为止;
当找不到满足条件的映射时,将分配空值或默认值:
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Map(dest => dest.FullName, src => "Sig. " + src.FullName, srcCond => srcCond.Country == "Italy")
.Map(dest => dest.FullName, src => "Sr. " + src.FullName, srcCond => srcCond.Country == "Spain")
.Map(dest => dest.FullName, src => "Mr. " + src.FullName);
使用 IgnoreIf
方法,当满足条件时将忽略此成员的映射:
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.IgnoreIf((src, dest) => !string.IsNullOrEmpty(dest.Name), dest => dest.Name);
Mapster 默认映射时会将 源对象的所有成员映射到目标对象,如果不想映射空值,那么可以使用 IgnoreNullValues
方法进行配置:
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.IgnoreNullValues(true);
注意!如果想要在满足条件时跳过映射,应该使用 IgnoreIf
,详情可参阅
8.3、映射私有成员
TypeAdapterConfig<TSource, TDestination>
.NewConfig()
.Map("PrivateDestName", "PrivateSrcName");
8.4、多级映射
TypeAdapterConfig<Poco, Dto>.NewConfig()
.Map(dest => dest.Child.Name, src => src.Name);
映射 Dto 中属性的值到 Poco
public class Poco
{
public string Name { get; set; }
public string Extra { get; set; }
}
public class Dto
{
public string Name { get; set; }
public SubDto SubDto { get; set; }
}
public class SubDto
{
public string Extra { get; set; }
}
如果想将 Dto
中的所有属性和 Dto.SubDto
中的所有属性映射到 Poco
,那么可以通过配置 dto.SubDto
映射到 Poco
来实现:
TypeAdapterConfig<Dto, Poco>.NewConfig()
.Map(poco => poco, dto => dto.SubDto);
8.5、空值
如果 src.Child
为 null
,那么映射到 dest.Name
的配置不会抛出 NullPointerException
,而是映射空值:
TypeAdapterConfig<Poco, Dto>.NewConfig()
.Map(dest => dest.Name, src => src.Child.Name);
8.6、多个源映射到同一个类型
如果想将 Dto1
与Dto2
两个类型映射到 Poco
类型,那么可以通过将 Dto1
Dto2
包装成一个 tuple,然后将 tuple.Item1
和 tuple.Item2
映射到 Poco
来实现:
TypeAdapterConfig<(Dto1, Dto2), Poco>.NewConfig()
.Map(dest => dest, src => src.Item1)
.Map(dest => dest, src => src.Item2);
8.7、特性
AdaptIgnore 特性
当一个属性有 [AdaptIgnore]
标记时,这个属性将不会被映射:
public class Product {
public string Id { get; set; }
public string Name { get; set; }
[AdaptIgnore]
public decimal Price { get; set; }
}
当一个成员有 [AdaptIgnore]
标记时,不管是 源到目标 还是 目标到源 的映射都将会忽略这个成员,可以使用 MemberSide
指定单方的忽略。
例如,只有 Product
当作 源映射时,Price
字段才会被忽略:
public class Product {
public string Id { get; set; }
public string Name { get; set; }
[AdaptIgnore(MemberSide.Source)]
public decimal Price { get; set; }
}
AdaptMember 特性标记
使用 [AdaptMember]
特性标记可以实现修改映射的名称。
例如,将 Id
映射为 Code
:
public class Product {
[AdaptMember("Code")]
public string Id { get; set; }
public string Name { get; set; }
}
使用 [AdaptMember]
特性标记可以实现映射非公开成员:
public class Product {
[AdaptMember]
private string HiddenId { get; set; }
public string Name { get; set; }
}