You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mapster中MapWith/ConstructUsing的递归映射问题及解决方案咨询

Mapster映射无公开默认构造函数实体的递归与配置问题

1. 相关代码

Author实体代码

public class Author : BaseEntity
{
    public virtual string Name { get; private set; }
    public virtual Address PhysicalAddress { get; private set; }

    [JsonIgnore]
    [IgnoreDataMember]
    [ForeignKey("Recipe")]
    public virtual Guid RecipeId { get; private set; }
    public virtual Recipe Recipe { get; private set; }


    public static Author Create(AuthorForCreationDto authorForCreationDto)
    {
        new AuthorForCreationDtoValidator().ValidateAndThrow(authorForCreationDto);
        var config = new TypeAdapterConfig();
        config.Apply(new AuthorProfile());
        var mapper = new MapsterMapper.Mapper(config);
        var newAuthor = mapper.Map<Author>(authorForCreationDto);
        
        newAuthor.QueueDomainEvent(new AuthorCreated(){ Author = newAuthor });
        
        return newAuthor;
    }

    public void Update(AuthorForUpdateDto authorForUpdateDto)
    {
        new AuthorForUpdateDtoValidator().ValidateAndThrow(authorForUpdateDto);
        var config = new TypeAdapterConfig();
        config.Apply(new AuthorProfile());
        var mapper = new MapsterMapper.Mapper(config);
        mapper.Map(authorForUpdateDto, this);
        
        QueueDomainEvent(new AuthorUpdated(){ Id = Id });
    }
    
    protected Author() { } // For EF + Mocking
}

映射配置代码

public class AuthorProfile : IRegister
{
    public void Register(TypeAdapterConfig config)
    {
        config.NewConfig<string, PostalCode>()
            .MapWith(src => new PostalCode(src));
        config.NewConfig<PostalCode, string>()
            .MapWith(src => src.Value);
        config.NewConfig<Address, AddressDto>()
            .TwoWays();
        config.NewConfig<AddressForCreationDto, Address>()
            .MapWith(src => new Address(src.Line1, src.Line2, src.City, src.State, src.PostalCode, src.Country))
            .TwoWays();
        config.NewConfig<AddressForUpdateDto, Address>()
            .MapWith(src => new Address(src.Line1, src.Line2, src.City, src.State, src.PostalCode, src.Country))
            .TwoWays();
        
        config.NewConfig<Author, AuthorDto>()
            .TwoWays();
        config.NewConfig<AuthorForCreationDto, Author>()
            .TwoWays();
        config.NewConfig<Author, AuthorForUpdateDto>()
            .TwoWays();
    }
}

2. 遇到的问题

  • 直接映射报错:因Author无公开默认构造函数,Mapster无法实例化对象,错误信息:

----> System.InvalidOperationException : No default constructor for type 'Author', please use 'ConstructUsing' or 'MapWith'

  • ConstructUsing配置后递归调用:尝试用ConstructUsing(x => Author.Create(x))配置映射时,出现无限递归。原因是Author.Create方法内部又调用了mapper.Map<Author>(dto),而映射配置又指向Author.Create,形成循环调用。

  • Update方法映射无合适方案:Update方法依赖AuthorForUpdateDto,无法复用创建逻辑,且当前映射配置无法直接更新已有Author实例。

3. 可行解决方案

方案一:剥离映射逻辑到应用层,依赖注入Mapper

核心是把实体中的映射逻辑移到应用服务层,避免在实体方法内重复创建Mapper实例,同时解决循环调用问题。

  1. 重构应用服务层:
public class AuthorService
{
    private readonly IMapper _mapper;

    public AuthorService(IMapper mapper)
    {
        _mapper = mapper;
    }

    public Author CreateAuthor(AuthorForCreationDto dto)
    {
        new AuthorForCreationDtoValidator().ValidateAndThrow(dto);
        var author = _mapper.Map<Author>(dto);
        author.QueueDomainEvent(new AuthorCreated(){ Author = author });
        return author;
    }

    public void UpdateAuthor(Author author, AuthorForUpdateDto dto)
    {
        new AuthorForUpdateDtoValidator().ValidateAndThrow(dto);
        _mapper.Map(dto, author);
        author.QueueDomainEvent(new AuthorUpdated(){ Id = author.Id });
    }
}
  1. 修改映射配置:
    允许Mapster访问非公开构造函数,并手动映射属性:
// 全局配置(启动时添加)
TypeAdapterConfig.GlobalSettings.Default
    .AllowImplicitConstructorAccess(true);

// AuthorProfile中的配置
config.NewConfig<AuthorForCreationDto, Author>()
    .Map(dest => dest.Name, src => src.Name)
    .Map(dest => dest.PhysicalAddress, src => src.PhysicalAddress)
    .TwoWays();

config.NewConfig<AuthorForUpdateDto, Author>()
    .Map(dest => dest.Name, src => src.Name)
    .Map(dest => dest.PhysicalAddress, src => src.PhysicalAddress);

这种方式符合领域驱动设计原则,实体只负责领域逻辑,映射和验证逻辑交给应用层处理。

方案二:实体内部添加初始化方法,映射时手动创建实例

如果要保留实体的静态Create方法,可在实体中添加内部初始化方法,映射时直接调用保护构造函数并初始化属性:

  1. 修改Author实体:
public class Author : BaseEntity
{
    // ...原有属性

    protected Author() { }

    // 内部初始化方法
    internal void Initialize(string name, Address physicalAddress)
    {
        Name = name;
        PhysicalAddress = physicalAddress;
    }

    // ...原有方法
}
  1. 修改映射配置:
config.NewConfig<AuthorForCreationDto, Author>()
    .MapWith(src => 
    {
        var address = new Address(src.PhysicalAddress.Line1, src.PhysicalAddress.Line2, src.PhysicalAddress.City, src.PhysicalAddress.State, src.PhysicalAddress.PostalCode, src.PhysicalAddress.Country);
        var author = new Author();
        author.Initialize(src.Name, address);
        return author;
    })
    .TwoWays();

config.NewConfig<AuthorForUpdateDto, Author>()
    .Map(dest => dest.Name, src => src.Name)
    .Map(dest => dest.PhysicalAddress, src => new Address(src.PhysicalAddress.Line1, src.PhysicalAddress.Line2, src.PhysicalAddress.City, src.PhysicalAddress.State, src.PhysicalAddress.PostalCode, src.PhysicalAddress.Country));

方案三:公开默认构造函数但限制使用

若允许妥协,可将默认构造函数改为公开,同时用Obsolete标记禁止直接调用:

public class Author : BaseEntity
{
    // ...原有属性

    [Obsolete("必须使用Create方法创建实例", true)]
    public Author() { } // 公开但标记过时,编译时会报错

    // ...原有方法
}

此时Mapster可直接实例化Author,同时通过过时标记防止开发者直接调用默认构造函数。但这种方式存在一定设计妥协,需谨慎使用。

总结

优先推荐方案一,既解决了循环调用和映射配置问题,又符合领域驱动设计的职责分离原则,避免实体承担映射等非领域逻辑。

内容的提问来源于stack exchange,提问作者Paul DeVito

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 07:15:38