Mapster 高性能对象映射框架
Mapster 简介
Mapster 是一个使用简单,功能强大,性能极佳的对象映射框架
为什么选择 Mapster ?
性能 & 内存占用
与 AutoMapper 相比,Mapster 在速度和内存占用方面表现更加优秀,可以在只使用1/3内存的情况下获得4倍的性能提升。
并且通过使用以下组件可以获得更高的性能:
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 |
Mapster 创建 目标对象 并将符合规则的成员映射到目标对象中:
var destObject = sourceObject.Adapt<Destination>();
创建一个对象,Mapster将把 源对象 映射到这个对象:
sourceObject.Adapt(destObject);
Mapster 还提供了对 IQueryable 的映射扩展:
using (MyDbContext context = new MyDbContext())
{
// 使用 ProjectToType 映射到目标类型
var destinations = context.Sources.ProjectToType<Destination>().ToList();
// 手动编写映射
var destinations = context.Sources.Select(c => new Destination {
Id = p.Id,
Name = p.Name,
Surname = p.Surname,
....
})
.ToList();
可以从任何地方调用 Adapt
方法。
var dest = src.Adapt<TSource, TDestination>();
或者直接
var dest = src.Adapt<TDestination>();
这两个扩展方法做的都是同样的事情。src.Adapt<TDestination>
将把 src
转换为 object 类型。因此,如果要转换的是值类型,那么请使用 src.Adapt<TSource, TDestination>
以避免不必要的装箱/拆箱。
在一些情况下,需要将 映射器 或 工厂函数 传递到依赖注入容器中。Mapster 提供了 IMapper
和 Mapper
来满足这个需求:
IMapper mapper = new Mapper();
并且使用 Map
函数来执行映射:
var result = mapper.Map<TDestination>(source);
在大多数情况下,Adapt
方法就足够了,但有时需要使用构建器来支持一些特殊的场景。
一个基本的例子 —— 传递运行时的值:
var dto = poco.BuildAdapter()
.AddParameters("user", this.User.Identity.Name)
.AdaptToType<SimpleDto>();
如果使用 IMapper
实例,你可以通过 From
创建构建器。
var dto = mapper.From(poco)
.AddParameters("user", this.User.Identity.Name)
.AdaptToType<SimpleDto>();
基本类型的转换 ,例如: int/bool/dobule/decimal
,包括可空的基本类型。
只要C#支持类型转换的类型,那么在 Mapster 中也同样支持转换。
decimal i = 123.Adapt<decimal>(); //equal to (decimal)123;
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);
在 Mapster 中,将其它类型映射为字符串时,Mapster 将调用类型的 ToString
方法。
如果将字符串映射为类型时,Mapster 将调用类型的 Parse
方法。
var s = 123.Adapt<string>(); // 等同于: 123.ToString();
var i = "123".Adapt<int>(); // 等同于: int.Parse("123");
包括列表、数组、集合、包括各种接口的字典之间的映射: IList<T>
, ICollection<T>
, IEnumerable<T>
, ISet<T>
, IDictionary<TKey, TValue>
等等…
var list = db.Pocos.ToList();
var target = list.Adapt<IEnumerable<Dto>>();
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
配置
使用 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));
使用全局设置将映射策略应用到所有的映射配置。
TypeAdapterConfig.GlobalSettings.Default.PreserveReference(true);
对于特定的类型映射,你可以使用 TypeAdapterConfig<SimplePoco, SimpleDto>.NewConfig()
覆盖全局映射配置。
TypeAdapterConfig<SimplePoco, SimpleDto>.NewConfig().PreserveReference(false);
你可以使用 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));
在不确定源类型的时候,使用 ForDestinationType
来创建针对于 目标类型 的映射配置。
比如使用 AfterMapping
在映射完成后调用目标类型对象的 Validate
方法:
TypeAdapterConfig.GlobalSettings.ForDestinationType<IValidator>()
.AfterMapping(dest => dest.Validate());
注意!在上面的代码段中指定目标类型为 IValidator
接口,那么将会把映射配置应用到所有实现了 IValidator
的类型。
如果映射的是泛型类型,可以通过将泛型类型传给 ForType
来创建设置.
TypeAdapterConfig.GlobalSettings.ForType(typeof(GenericPoco<>), typeof(GenericDto<>))
.Map("value", "Value");
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;
Mapster 默认不会把 目标类型的 映射配置 应用到 目标类型的子类。
可以设置 AllowImplicitDestinationInheritance
开启:
TypeAdapterConfig.GlobalSettings.AllowImplicitDestinationInheritance = true;
可以通过 Inherits
方法显示的继承类型映射配置:
TypeAdapterConfig<DerivedPoco, DerivedDto>.NewConfig()
.Inherits<SimplePoco, SimpleDto>();
可以通过 Include
方法显示的让子类继承父类的映射配置:
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"); // "Make" 属性在 Vehicle 中不存在
在 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);
如果想从现有的配置中创建配置实例,可以使用 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);
映射配置应该只初始化并且只进行一次配置。因此在编写代码的时候不能将映射配置和映射调用放在同一个地方。
例如下面的例子,运行将会抛出一个异常:
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
方法Global.asax.cs
Startup.cs
// Application_Start in Global.asax.cs
config.ForType<Poco, Dto>().Ignore("Id");
// in Controller class
var dto1 = poco1.Adapt<Dto>(config);
var dto2 = poco2.Adapt<Dto>(config);
将配置实例和映射配置分开,可能会导致代码处于不同的位置,在代码编写过程中可能会出现遗漏之类的问题。
使用 Fork
函数可以使配置实例与映射配置在同一位置:
var dto = poco.Adapt<Dto>(
config.Fork(forked => forked.ForType<Poco, Dto>().Ignore("Id"));
不用担心性能问题,只有第一次使用配置实例时会编译,之后的调用将从缓存中获取。
Fork
方法默认使用文件名、行号作为键值。但是如果在在泛型类或泛型方法中调用Fork
方法,必须指定键和类型全名称,防止 Fork
从不同的类型参数返回无效的配置。
IQueryable<TDto> GetItems<TPoco, TDto>()
{
var forked = config.Fork(
f => f.ForType<TPoco, TDto>().Ignore("Id"),
$"MyKey|{typeof(TPoco).FullName}|{typeof(TDto).FullName}");
return db.Set<TPoco>().ProjectToType<TDto>(forked);
}
映射配置处于多个不同的程序集是比较常见的情况。
也许你的域程序集有一些映射到域对象的规则,而你的 web api 有一些特定的规则来映射到您的 api 约定。
Mapster 支持扫描程序集注册映射配置,可以帮助你快速注册映射配置并减小忘记编码注册配置的机率。
在某些特定的情况下可以顺序注册程序集,以达到重写映射配置的目的。
扫描程序集注册映射配置非常简单,只需要在程序集中创建一个或多个 IRegister
的实现,然后调用 TypeAdapterConfig
实例的 Scan
方法即可:
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);
如果你使用的是其它 程序集扫描库(如 MEF ), 那么你可以调用配置实例的 Apply
方法将映射配置注册器添加到配置实例中:
var registers = container.GetExports<IRegister>();
config.Apply(registers);
Apply
方法允许你选择某些 映射配置注册器 添加到 配置实例 中,而不是像 Scan
方法把程序集里的所有 映射配置注册器 添加到 配置实例 中:
var register = new MockingRegister();
config.Apply(register);
Mapster 支持通过为类型增加 特性标签 的方式 添加类型映射配置:
[AdaptTo(typeof(StudentDto), PreserveReference = true)]
public class Student {
...
}
为了在单元测试中验证映射,并帮助处理“Fail Fast”情况,添加了以下严格映射模式。
通过修改 配置实例 RequireExplicitMapping
值决定是否开启强制显示映射。
当 RequireExplicitMapping
值为 true
时,所有类型映射必须显式配置,即使非常简单:
// 默认值为: "false"
TypeAdapterConfig.GlobalSettings.RequireExplicitMapping = true;
// 当你必须为每个类有一个显式的配置,即使它只是:
TypeAdapterConfig<Source, Destination>.NewConfig();
通过修改 配置实例 RequireDestinationMemberSource
值决定是否开启强制目标类型成员检查。
当 RequireDestinationMemberSource
值为 true
时,所有 目标类型的字段 必须与 源类型字段 保持一致或显示的指定映射或忽略成员:
// 默认值为: "false"
TypeAdapterConfig.GlobalSettings.RequireDestinationMemberSource = true;
调用 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();
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
方法之后 配置实例 就不允许添加修改其它映射配置!
例如有以下 父类、子类:
public class ParentPoco
{
public string Id { get; set; }
public List<ChildPoco> Children { get; set; }
public string Name { get; set; }
}
public class ChildPoco
{
public string Id { get; set; }
public List<GrandChildPoco> GrandChildren { get; set; }
}
public class GrandChildPoco
{
public string Id { get; set; }
}
如果你配置了父类型:
TypeAdapterConfig<ParentPoco, ParentDto>.NewConfig()
.PreserveReference(true);
默认情况下,子类型不会从 PreserveReference
中得到效果。
因此必须在 ParentPoco
中指定所有类型映射:
TypeAdapterConfig<ParentPoco, ParentDto>.NewConfig()
.PreserveReference(true);
TypeAdapterConfig<ChildPoco, ChildDto>.NewConfig()
.PreserveReference(true);
TypeAdapterConfig<GrandChildPoco, GrandChildDto>.NewConfig()
.PreserveReference(true);
或者可以调用 全局配置实例 的 PreserveReference
方法:
TypeAdapterConfig.GlobalSettings.Default.PreserveReference(true);
你可以使用 Fork
方法来定义仅将指定的映射应用于嵌套映射而不污染全局设置的配置:
TypeAdapterConfig<ParentPoco, ParentDto>.NewConfig()
.Fork(config => config.Default.PreserveReference(true));
忽略为null或为空的字符串
再比如,Mapster 只能忽略 null 值 (IgnoreNullValues),但是你可以使用 Fork
来忽略 null 或空值。
TypeAdapterConfig<ParentPoco, ParentDto>.NewConfig()
.Fork(config => config.ForType<string, string>()
.MapToTargetWith((src, dest) => string.IsNullOrEmpty(src) ? dest : src)
);
# 如果已经拥有dotnet-tools.json,则跳过此步骤
dotnet new tool-manifest
dotnet tool install Mapster.Tool
简单映射的情况下, 只需要安装 Mapster.Core
:
PM> Install-Package Mapster.Core
如果需要 TypeAdapterConfig
进行复杂映射配置,需要安装 Mapster
:
PM> Install-Package Mapster
Mapster.Tool 提供了3个命令
- model: 从实体生成模型
- extension: 从实体生成扩展方法
- mapper: 从接口生成映射器
并且 Mapster.Tool 提供了以下选项
- -a: 定义输入程序集
- -b: 指定用于生成动态输出和名称空间的基本名称空间
- -n: 定义生成类的命名空间
- -o: 定义输出目录
- -p: 打印完整的类型名称(如果Poco/Dto有相同的名称)
- -r: 生成record 类型而不是POCO类型
将以下代码添加到 csproj
文件中:
<Target Name="Mapster">
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet build" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster model -a "$(TargetDir)$(ProjectName).dll"" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a "$(TargetDir)$(ProjectName).dll"" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a "$(TargetDir)$(ProjectName).dll"" />
</Target>
在 csproj
文件目录下生成如下命令:
dotnet msbuild -t:Mapster
将以下代码添加到 csproj
文件中:
<Target Name="Mapster" AfterTargets="AfterBuild">
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster model -a "$(TargetDir)$(ProjectName).dll"" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a "$(TargetDir)$(ProjectName).dll"" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a "$(TargetDir)$(ProjectName).dll"" />
</Target>
将以下代码添加到 csproj
文件中:
<ItemGroup>
<Generated Include="**\*.g.cs" />
</ItemGroup>
<Target Name="CleanGenerated">
<Delete Files="@(Generated)" />
</Target>
清理命令如下:
dotnet msbuild -t:CleanGenerated
如果POCOs和dto有相同的名称,您可能需要使用完整的类型名称来生成,通过 -p
选项:
<Target Name="Mapster">
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet build" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster model -a "$(TargetDir)$(ProjectName).dll" -p" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -a "$(TargetDir)$(ProjectName).dll" -p" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -a "$(TargetDir)$(ProjectName).dll" -p" />
</Target>
例如,存在以下结构:
Sample.CodeGen
- Domains
- Sub1
- Domain1
- Sub2
- Domain2
如果将基本名称空间指定为 Sample.CodeGen.Domains
:
<Exec WorkingDirectory="$(ProjectDir)"
Command="dotnet mapster model -a "$(TargetDir)$(ProjectName).dll" -n Sample.CodeGen.Generated -b Sample.CodeGen.Domains" />
代码将生成到:
Sample.CodeGen
- Generated
- Sub1
- Dto1
- Sub2
- Dto2
有3种方式来生成dto和映射代码
- Fluent API: 如果不想编辑实体类定义,或者从不同程序集中的实体类型生成dto。
- Attributes: 如果想保持映射配置到实体类。
- Interfaces: 如果已经有dto,并且想通过接口定义映射。