简体   繁体   中英

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

I have issues on reverse mapping multiple properties back to complex objects, even with custom value resolvers.

Here are the persistence model:

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; }
}

I have Dapper map data from database and fill in this model.


Here are the domain models I want to map back and forth with the persistence model:

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;
    }
}

I would like to use a value object called Email for both the payer and admin email. You can tell they're just stored flatten in the database/persistence model. And the payer email is required but not the admin email.


I have the mapping configured like following:

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>());
    }
}

After ReverseMap() , I want to grab multiple properties and construct the complex object Email . Hence I define two custom value resolvers for that:

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);  
    }
}

As always, the mapping from the domain model to the Dbo works fine:

在此处输入图像描述

but not the other way, from Dbo to domain model. It's throwing exceptions:

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 demo: https://dotnetfiddle.net/DcTsPG


I wonder if AutoMapper confuses about those two Email objects: payer email and admin email, because they're both are Email type.

In reverse map AutoMapper is failing to create an instance of EmailTask .

Add a parameterless constructor to your EmailTask class -

public EmailTask()
{
    // AutoMapper use only
}

Also, since your value resolvers are creating instance of Email , add a parameterless constructor to your Email class too -

public Email()
{
    // AutoMapper use only
}

Finally, modify the PayerEmail and AdminEmail properties in EmailTask class so they can be set publicly -

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

That should solve your issue.

EDIT:
@David Liang, after reading your comment I'd say, to suit your scenario in light of DDD, you might need to modify your current mapping approach.

The thing is, when you are mapping EmailDbo from EmailTask , the process is easier because EmailDbo is a DTO type class with no parameterized constructor. Therefore, the property mapping only is enough to do the job.

But when you are trying to map EmailTask from EmailDbo , you are trying to instantiate a domain model class which has very strictly defined parameterized constructor that takes complex types as parameters, and is trying to protect how it's properties can and cannot be accessed from outside. Therefore, the .ReverseMap() approach you are using currently will not be very helpful, because the property mapping only will not be enough to provide you all the constructor parameters needed to instantiate the class. There's also AutoMapper's naming convention in the play.

Following is a mapping configuration for EmailTask from EmailDbo , where the reverse mapping is separated out and the value resolvers are refactored out into a helper class. The forward mapping remained unchanged.

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

The Helper class -

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);
    }
}

Here is the complete fiddle - https://dotnetfiddle.net/2MxSdt

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM