Mapster中MapWith/ConstructUsing的递归映射问题及解决方案咨询
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实例,同时解决循环调用问题。
- 重构应用服务层:
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 }); } }
- 修改映射配置:
允许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方法,可在实体中添加内部初始化方法,映射时直接调用保护构造函数并初始化属性:
- 修改Author实体:
public class Author : BaseEntity { // ...原有属性 protected Author() { } // 内部初始化方法 internal void Initialize(string name, Address physicalAddress) { Name = name; PhysicalAddress = physicalAddress; } // ...原有方法 }
- 修改映射配置:
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

