从多个属性到复杂对象的 AutoMapper 映射因 ReverseMap 和自定义值解析器而失败

AutoMapper mapping from multiple properties to complex objects fails with ReverseMap and custom value resolver

我在将多个属性反向映射回复杂对象时遇到问题,即使使用自定义值解析器也是如此。

持久性模型如下:

public class EmailDbo
{
    public int EmailId { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime? DateSent { get; set; }
    public string SendTo { get; set; }
    public string Subject { get; set; }
    public string Body { get; set; }
    public bool DownloadAvailable { get; set; }
    public DateTime? AdminDateSent { get; set; }
    public string AdminEmail { get; set; }
    public string AdminSubject { get; set; }
    public string AdminBody { get; set; }
    public int StatusId { get; set; }
}

我有来自数据库的 Dapper 地图数据并填写这个模型。


以下是我想要与持久性模型来回映射的领域模型:

public class Email
{
    public string SendTo { get; private set; }
    public string Subject { get; private set; }
    public string Body { get; private set; }
    public DateTime? DateSent { get; private set; }
    
    public Email(string sendTo, string subject, string body, DateTime? dateSent = null)
    {
        // Validations
        this.SendTo = sendTo;
        this.Subject = subject;
        this.Body = body;
        this.DateSent = dateSent;
    }
}

public enum EmailTaskStatus
{
    Sent = 1,
    Unsent = 2
}

public class EmailTask
{
    public int Id { get; private set; }
    public DateTime DateCreated { get; private set; }
    public Email PayerEmail { get; private set; }
    public Email AdminEmail { get; private set; }
    public bool DownloadAvailableForAdmin { get; private set; }
    public EmailTaskStatus Status { get; private set; }
    
    public EmailTask(int emailTaskId, DateTime dateCreated, Email payerEmail, Email adminEmail,
        bool downloadAvailable, EmailTaskStatus status)
    {
        // Validations
        this.Id = emailTaskId;
        this.DateCreated = dateCreated;
        this.PayerEmail = payerEmail;
        this.AdminEmail = adminEmail;
        this.DownloadAvailableForAdmin = downloadAvailable;
        this.Status = status;
    }
}

我想为付款人和管理员电子邮件使用一个名为 Email 的值对象。您可以看出它们只是平铺存储在 database/persistence 模型中。并且需要付款人电子邮件,但不需要管理员电子邮件。


我的映射配置如下:

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<EmailTask, EmailDbo>()
            .ForMember(dest => dest.EmailId, opts => opts.MapFrom(src => src.Id))
            .ForMember(dest => dest.SendTo, opts => opts.MapFrom(src => src.PayerEmail.SendTo))
            .ForMember(dest => dest.Subject, opts => opts.MapFrom(src => src.PayerEmail.Subject))
            .ForMember(dest => dest.Body, opts => opts.MapFrom(src => src.PayerEmail.Body))
            .ForMember(dest => dest.DateSent, opts => opts.MapFrom(src => src.PayerEmail.DateSent))
            .ForMember(dest => dest.DownloadAvailable, opts => opts.MapFrom(src => src.DownloadAvailableForAdmin))
            .ForMember(dest => dest.AdminEmail, opts => 
            {
                opts.PreCondition(src => (src.AdminEmail != null));
                opts.MapFrom(src => src.AdminEmail.SendTo);
            })
            .ForMember(dest => dest.AdminSubject, opts =>
            {
                opts.PreCondition(src => (src.AdminEmail != null));
                opts.MapFrom(src => src.AdminEmail.Subject);
            })
            .ForMember(dest => dest.AdminBody, opts =>
            {
                opts.PreCondition(src => (src.AdminEmail != null));
                opts.MapFrom(src => src.AdminEmail.Body);   
            })
            .ForMember(dest => dest.AdminDateSent, opts =>
            {
                opts.PreCondition(src => (src.AdminEmail != null)); 
                opts.MapFrom(src => src.AdminEmail.DateSent);   
            })
            .ForMember(dest => dest.StatusId, opts => opts.MapFrom(src => (int)src.Status))
            .ReverseMap()
                .ForCtorParam("status", opts => opts.MapFrom(src => src.StatusId))
                .ForMember(dest => dest.PayerEmail, opts => opts.MapFrom<PayerEmailValueResolver>())
                .ForMember(dest => dest.AdminEmail, opts => opts.MapFrom<AdminEmailValueResolver>());
    }
}

ReverseMap()之后,我想抓取多个属性并构造复杂对象Email。因此,我为此定义了两个 custom value resolvers

public class PayerEmailValueResolver : IValueResolver<EmailDbo, EmailTask, Email>
{
    public Email Resolve(EmailDbo emailDbo, EmailTask emailTask, Email email, ResolutionContext context)
    {
        return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);  
    }
}

public class AdminEmailValueResolver : IValueResolver<EmailDbo, EmailTask, Email>
{
    public Email Resolve(EmailDbo emailDbo, EmailTask emailTask, Email email, ResolutionContext context)
    {
        if (String.IsNullOrWhiteSpace(emailDbo.AdminEmail) &&
            String.IsNullOrWhiteSpace(emailDbo.AdminSubject) &&
            String.IsNullOrWhiteSpace(emailDbo.AdminBody) &&
            !emailDbo.AdminDateSent.HasValue)
        {
            return null;
        }
        
        return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);  
    }
}

一如既往,从域模型到 Dbo 的映射工作正常:

但不是相反,从 Dbo 到域模型。它抛出异常:

Unhandled exception. System.ArgumentException: Program+EmailTask needs to have a constructor with 0 args or only optional args. (Parameter 'type') at lambda_method32(Closure , Object , EmailTask , ResolutionContext ) at AutoMapper.Mapper.MapCore[TSource,TDestination](TSource source, TDestination destination, ResolutionContext context, Type sourceType, Type destinationType, IMemberMap memberMap) at AutoMapper.Mapper.Map[TSource,TDestination](TSource source, TDestination destination) at AutoMapper.Mapper.Map[TDestination](Object source)

.Net Fiddle 演示: https://dotnetfiddle.net/DcTsPG


我想知道 AutoMapper 是否混淆了这两个 Email 对象:付款人电子邮件和管理员电子邮件,因为它们都是 Email 类型。

反向映射 AutoMapper 无法创建 EmailTask 的实例。

为你的 EmailTask class -

添加一个无参数的构造函数
public EmailTask()
{
    // AutoMapper use only
}

此外,由于您的值解析器正在创建 Email 的实例,因此也将无参数构造函数添加到您的 Email class -

public Email()
{
    // AutoMapper use only
}

最后,修改EmailTask中的PayerEmailAdminEmail属性class使其可以公开设置-

public Email PayerEmail { get; set; }
public Email AdminEmail { get; set; }

这应该可以解决您的问题。

编辑:
@David Liang,在阅读您的评论后我会说,为了适应 DDD 的场景,您可能需要修改当前的映射方法。

事实是,当您从 EmailTask 映射 EmailDbo 时,过程更容易,因为 EmailDbo 是一个没有参数化构造函数的 DTO 类型 class。因此,仅 属性 映射就足以完成这项工作。

但是当您尝试从 EmailDbo 映射 EmailTask 时,您正在尝试实例化域模型 class,它具有非常严格定义的参数化构造函数,该构造函数将复杂类型作为参数,并试图保护它的属性如何可以和不能从外部访问。因此,您当前使用的 .ReverseMap() 方法不会很有帮助,因为仅 属性 映射不足以为您提供实例化 class 所需的所有构造函数参数。剧中还有AutoMapper的命名规范

以下是 EmailDboEmailTask 的映射配置,其中反向映射被分离出来,值解析器被重构为助手 class。前向映射保持不变。

CreateMap<EmailDbo, EmailTask>()
    .ConstructUsing((s, d) =>
        new EmailTask(
                s.EmailId,
                s.DateCreated,
                Helper.GetPayerEmail(s),
                Helper.GetAdminEmail(s),
                s.DownloadAvailable,
                (EmailTaskStatus)s.StatusId))
    .IgnoreAllPropertiesWithAnInaccessibleSetter();

Helperclass-

public class Helper
{
    public static Email GetPayerEmail(EmailDbo emailDbo)
    {
        return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
    }

    public static Email GetAdminEmail(EmailDbo emailDbo)
    {
        if (string.IsNullOrWhiteSpace(emailDbo.AdminEmail) && string.IsNullOrWhiteSpace(emailDbo.AdminSubject)
            && string.IsNullOrWhiteSpace(emailDbo.AdminBody) && !emailDbo.AdminDateSent.HasValue)
        {
            return null;
        }
        return new Email(emailDbo.SendTo, emailDbo.Subject, emailDbo.Body, emailDbo.DateSent);
    }
}

这是完整的 fiddle - https://dotnetfiddle.net/2MxSdt