Mapster 构造函数映射指南:用 ConstructUsing 与 MapToConstructor 掌控目标对象创建
2026/9/18 22:37:38 网站建设 项目流程

Mapster 构造函数映射指南:用 ConstructUsing 与 MapToConstructor 掌控目标对象创建

【免费下载链接】MapsterA fast, fun and stimulating object to object Mapper项目地址: https://gitcode.com/GitHub_Trending/ma/Mapster

Mapster 默认通过无参构造函数创建目标对象,但在实际项目中,DTO 常常只暴露带参构造函数、依赖工厂方法或需要对象初始化器来预置默认值。本文基于 Mapster 的ConstructUsingMapToConstructor两大配置项,完整讲解如何接管目标对象的创建过程、如何按构造函数参数自动映射、以及如何显式指定特定构造函数,并给出仓库源码级的实现佐证,帮助你写出可复制、可运行的映射配置。

目录

  • 一、默认创建方式的局限
  • 二、用 ConstructUsing 自定义目标对象创建
    • 无参构造函数场景
    • 非默认构造函数场景
    • 对象初始化器场景
    • 带 destination 参数的重载
  • 三、用 MapToConstructor 映射到构造函数
    • 全局默认开启
    • 类型对级别开启
    • 自定义构造函数参数映射(Pascal case)
    • 多构造函数时如何自动选择
    • 显式指定 ConstructorInfo
  • 四、源码级原理剖析
    • 构造工厂在 Settings 中的存储
    • 构造函数选择算法
    • 无默认构造函数时的错误提示
  • 五、典型实战场景
    • 映射到接口类型
    • 搭配 AfterMapping 使用 destination 重载
  • 六、相关文档与测试

一、默认创建方式的局限

Mapster 的核心Adapt操作在创建目标对象时,默认期望目标类型具备无参构造函数(empty constructor)。在 BaseAdapter.cs 中,当目标类型没有默认构造函数时,会抛出如下异常:

No default constructor for type '{arg.DestinationType.Name}', please use 'ConstructUsing' or 'MapWith'

这说明 Mapster 本身已经内置了两种解决方案:ConstructUsing(自定义创建逻辑)或MapWith(整体替换映射逻辑)。本文聚焦前者,并延伸到相关的MapToConstructor配置。

此外,默认情况下 Mapster只会映射到字段和属性(fields and properties),构造函数参数并不在默认映射范围内。要让映射结果直接通过构造函数注入,需要使用MapToConstructor显式开启。

二、用 ConstructUsing 自定义目标对象创建

ConstructUsing允许你提供一个函数调用来创建目标对象,而不是使用默认的(无参)对象创建方式。该函数返回一个目标类型的实例,你可以调用自己的构造函数、工厂方法,或任何能产出期望类型对象的代码。

无参构造函数场景

当目标对象创建不需要源对象参与时,使用无参重载:

TypeAdapterConfig<TSource, TDestination>.NewConfig() .ConstructUsing(() => new TDestination());

对应源码位于 TypeAdapterSetter.cs:

public TypeAdapterSetter<TDestination> ConstructUsing(Expression<Func<TDestination>> constructUsing) { this.CheckCompiled(); Settings.ConstructUsingFactory = arg => constructUsing; return this; }

可以看到ConstructUsing接受的是表达式树Expression<Func<...>>)而非普通委托,这意味着它会被内联进 Mapster 编译生成的映射表达式中,而不是作为外部委托调用,从而不影响映射性能。

非默认构造函数场景

最典型的用法是调用目标类型的非默认构造函数,从源对象取参:

//Example using a non-default constructor TypeAdapterConfig<TSource, TDestination>.NewConfig() .ConstructUsing(src => new TDestination(src.Id, src.Name));

源码中对应的是带源对象参数的重载,位于 TypeAdapterSetter.cs:

public TypeAdapterSetter<TSource, TDestination> ConstructUsing(Expression<Func<TSource, TDestination>> constructUsing) { Settings.ConstructUsingFactory = arg => constructUsing; return this; }

仓库测试 WhenUsingNonDefaultConstructor.cs 验证了这一行为:即使目标类型同时具备无参构造函数,配置了ConstructUsing后,映射仍会走自定义构造函数,且Unmapped属性被赋值为传入的固定值"unmapped",同时IdName等可映射成员依旧正常映射。

对象初始化器场景

你同样可以使用对象初始化器(object initializer),预置某些目标对象自己的成员值:

//Example using an object initializer TypeAdapterConfig<TSource, TDestination>.NewConfig() .ConstructUsing(src => new TDestination{Unmapped = "unmapped"});

这种写法在 WhenUsingNonDefaultConstructor.cs 的Dest_Calls_Calls_Factory_Method_With_ConstructUsing测试中也有对应验证。

需要注意:ConstructUsing的表达式体在投影(Projection)场景下只支持NewMemberInit两种节点类型。在 ClassAdapter.cs 中,如果投影模式下传入的是其他表达式(如方法调用、复杂逻辑),会抛出InvalidOperationException

ConstructUsing for projection is support only New and MemberInit expression.

带 destination 参数的重载

当目标对象已存在(例如映射到已有目标对象)时,ConstructUsing还有一个带destination参数的重载,可以读取目标对象的当前状态参与构造:

//Example using an overload with `destination` parameter TypeAdapterConfig<TSource, TDestination>.NewConfig() .ConstructUsing((src, destination) => new TDestination(src.Id, destination?.Name ?? src.Name));

对应源码位于 TypeAdapterSetter.cs,该重载只在UseDestinationValue或目标值存在时才有意义。仓库测试 WhenPerformingAfterMapping.cs 也展示了该重载与 AfterMapping 组合使用的场景。

三、用 MapToConstructor 映射到构造函数

默认情况下 Mapster 只映射字段和属性。通过MapToConstructor可以将映射目标改为构造函数参数,让不可变 DTO(只有只读属性、没有 setter)也能被完整填充。

全局默认开启

在全局设置中开启,所有类型对默认映射到构造函数:

//global level TypeAdapterConfig.GlobalSettings.Default.MapToConstructor(true);

类型对级别开启

也可以只针对某个类型对开启:

//type pair TypeAdapterConfig<Poco, Dto>.NewConfig().MapToConstructor(true);

MapToConstructor(bool)的源码实现位于 TypeAdapterSetter.cs,开启时在 Settings 中写入"*"标记,关闭时写入null

public static TSetter MapToConstructor<TSetter>(this TSetter setter, bool value) where TSetter : TypeAdapterSetter { setter.CheckCompiled(); setter.Settings.MapToConstructor = value ? "*" : null; return setter; }

CheckCompiled()意味着一旦该配置对应的映射已经编译完成,就不能再修改,否则会抛异常——这也是 Mapster 所有设置项的统一行为。

自定义构造函数参数映射(Pascal case)

当构造函数参数名与源成员名不一致时,需要自定义映射,此时必须使用Pascal case(帕斯卡命名,即首字母大写):

class Poco { public string Id { get; set; } ... } class Dto { public Dto(string code, ...) { ... } }
TypeAdapterConfig<Poco, Dto>.NewConfig() .MapToConstructor(true) .Map('Code', 'Id'); //use Pascal case

Map('Code', 'Id')的含义是:目标构造函数的Code参数(Pascal case 形式)从源对象的Id属性取值。构造函数参数在内部被规范化为 Pascal case 表示,因此自定义映射必须遵守这一约定。

多构造函数时如何自动选择

如果目标类有 2 个或更多构造函数,Mapster 会自动选择能够满足映射的、参数数量最多的那个构造函数:

class Poco { public int Foo { get; set; } public int Bar { get; set; } } class Dto { public Dto(int foo) { ... } public Dto(int foo, int bar) { ...} //<-- Mapster will use this constructor public Dto(int foo, int bar, int baz) { ... } }

在这个例子中,Poco只有FooBar两个可映射成员,因此Dto(int foo, int bar)是"能完全映射的参数最多的构造函数",会被选中;Dto(int foo, int bar, int baz)虽然参数更多,但因为Poco没有Baz成员,无法满足映射。

显式指定 ConstructorInfo

除了自动选择,你还可以通过反射拿到ConstructorInfo并显式传入:

var ctor = typeof(Dto).GetConstructor(new[] { typeof(int), typeof(int) }); TypeAdapterConfig<Poco, Dto>.NewConfig() .MapToConstructor(ctor);

对应源码位于 TypeAdapterSetter.cs,该方法包含两个重要的运行时校验:

  • 构造函数声明类型必须能赋值给TDestination,否则抛出ArgumentException("Constructor cannot be assigned to type TDestination")
  • 构造函数的声明类型不能是抽象类,否则抛出ArgumentException("Constructor of abstract type cannot be created")

仓库测试 WhenMappingToConstructor.cs 验证了显式指定构造函数的效果:Dto定义了三个构造函数,显式指定Dto(string id)后,映射结果中Id被正确赋值,而Name为 null、Age为 0,其余可写属性Prop仍通过普通属性映射填充。

四、源码级原理剖析

构造工厂在 Settings 中的存储

无论是ConstructUsing的哪个重载,最终都统一写入Settings.ConstructUsingFactory,其类型为Func<CompileArgument, LambdaExpression>,定义在 TypeAdapterSettings.cs。编译时,CompileArgument.cs 通过GetConstructUsing()惰性求值该工厂:

internal LambdaExpression? GetConstructUsing() { if (_fetchConstructUsing) return _constructUsing; _constructUsing = Settings.ConstructUsingFactory?.Invoke(this); _fetchConstructUsing = true; return _constructUsing; }

_fetchConstructUsing标志保证了同一个编译参数实例中工厂只被调用一次,避免重复求值影响性能。

构造函数选择算法

MapToConstructor(true)与自动选择构造函数的完整逻辑在 ClassAdapter.cs 的CreateInstantiationExpression中:

  1. 若目标类型有默认构造函数、或已配置ConstructUsing,且未配置MapToConstructor,则走基类的默认实例化路径;
  2. 否则,从Settings.MapToConstructor中读取ConstructorInfo
  3. 若没有显式指定,则通过destType.GetConstructors()获取所有构造函数,按参数数量降序排序OrderByDescending(it => it.GetParameters().Length)),逐个尝试CreateClassConverter生成映射;
  4. 返回第一个能成功完成参数映射的构造函数(FirstOrDefault(it => it != null));
  5. 如果全部尝试失败,则回退到第一个构造函数的非严格模式(GetConstructorModel(constructors[0], false));
  6. 若仍无法生成,则回退到默认的实例化表达式。

值得注意的是,代码中destType还处理了目标为接口的情况:通过DynamicTypeGenerator.GetTypeForInterface为接口动态生成实现类型,再在其上寻找构造函数(见 ClassAdapter.cs)。

无默认构造函数时的错误提示

在 BaseAdapter.cs 中,当最终无法创建目标实例且目标类型没有默认构造函数时,会抛出带有明确指引的异常信息,提示开发者改用ConstructUsingMapWith。这条错误信息实际上就是"何时应该使用本文配置项"的最直接判断标准。

五、典型实战场景

映射到接口类型

当目标是接口时,ConstructUsing可以配合动态生成的接口实现类型使用。仓库测试 WhenUsingNonDefaultConstructor.cs 展示了将SimplePoco映射到ISimpleDtoWithDefaultConstructor接口时,通过ConstructUsing提供具体实现类实例的做法:

TypeAdapterConfig<SimplePoco, ISimpleDtoWithDefaultConstructor>.NewConfig() .IgnoreNullValues(true) .ConstructUsing(src => new SimpleDtoWithDefaultConstructor { Unmapped = "unmapped" }) .Compile(); var dto = TypeAdapter.Adapt<ISimpleDtoWithDefaultConstructor>(simplePoco);

搭配 AfterMapping 使用 destination 重载

destination参数的ConstructUsing重载适合在目标对象已存在的合并场景中使用,它允许构造逻辑参考目标对象的当前值(如destination?.Name ?? src.Name这样的回退策略)。这种"先构造、再 AfterMapping 收尾"的组合在 WhenPerformingAfterMapping.cs 中均有测试覆盖。

六、相关文档与测试

  • 原文档:Constructor mapping
  • 设置项总览:Settings 目录,相关主题还包括 Setting values、Constructor 相关
  • 核心实现:
    • TypeAdapterSetter.cs:ConstructUsingMapToConstructor各重载定义
    • ClassAdapter.cs:构造函数自动选择算法
    • BaseAdapter.cs:无默认构造函数的异常提示
    • TypeAdapterSettings.cs:ConstructUsingFactoryMapToConstructor设置存储
  • 测试用例:
    • WhenMappingToConstructor.cs:自动选择与显式ConstructorInfo两种模式
    • WhenUsingNonDefaultConstructor.cs:非默认构造函数、工厂方法、接口目标三种场景
    • WhenPerformingAfterMapping.cs:ConstructUsingAfterMapping组合

核心要点回顾ConstructUsing用于完全接管目标对象的创建(自定义构造函数、工厂方法或初始化器),MapToConstructor用于让映射结果直接注入构造函数参数;当存在多个构造函数时 Mapster 自动选择"参数最多且能完全映射"的那个,也可以显式传入ConstructorInfo精确控制;自定义构造参数映射必须使用 Pascal case。两者都能与全局Default设置、投影(Projection)、接口目标等场景无缝配合。

【免费下载链接】MapsterA fast, fun and stimulating object to object Mapper项目地址: https://gitcode.com/GitHub_Trending/ma/Mapster

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询