Question

I need to map two ways between a flat ViewModel and a deep structured Domain model. This will be a common scenario in our solution.

My models are:

public class Client 
{
   ...
   public NotificationSettings NotificationSettings { get; set; }
   public ContactDetails ContactDetails { get; set; }
   ...
}

public class NotificationSettings
{
    ...
    public bool ReceiveActivityEmails { get; set; }
    public bool ReceiveActivitySms { get; set; }
    ...
}

public class ContactDetails
{
    ...
    public string Email { get; set }
    public string MobileNumber { get; set; }
    ...
}

public class ClientNotificationOptionsViewModel
{
    public string Email { get; set }
    public string MobileNumber { get; set; }
    public bool ReceiveActivityEmails { get; set; }
    public bool ReceiveActivitySms { get; set; }
}

Mapping code:

Mapper.CreateMap<Client, ClientNotificationOptionsViewModel>()
    .ForMember(x => x.ReceiveActivityEmails, opt => opt.MapFrom(x => x.NotificationSettings.ReceiveActivityEmails))
    .ForMember(x => x.ReceiveActivitySms, opt => opt.MapFrom(x => x.NotificationSettings.ReceiveActivitySms))
    .ForMember(x => x.Email, opt => opt.MapFrom(x => x.ContactDetails.Email))
    .ForMember(x => x.MobileNumber, opt => opt.MapFrom(x => x.ContactDetails.MobileNumber));

// Have to use AfterMap because ForMember(x => x.NotificationSettings.ReceiveActivityEmail) generates "expression must resolve to top-level member" error
Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
    .IgnoreUnmapped()
    .AfterMap((from, to) =>
    {
        to.NotificationSettings.ReceiveActivityEmail = from.ReceiveActivityEmail;
        to.NotificationSettings.ReceiveActivitySms = from.ReceiveActivitySms;
        to.ContactDetails.Email = from.Email;
        to.ContactDetails.MobileNumber = from.MobileNumber;
    });


...

// Hack as ForAllMembers() returns void instead of fluent API syntax
public static IMappingExpression<TSource, TDest> IgnoreUnmapped<TSource, TDest>(this IMappingExpression<TSource, TDest> expression)
{
    expression.ForAllMembers(opt => opt.Ignore());
    return expression;
}

I dislike it because:

1) It is cumbersome

2) The second mapping pretty much dismantles Automapper's functionality and implements the work manually - the only advantage of it is consistency of referencing Automapper throughout the code

Can anyone suggest:

a) A better way to use Automapper for deep properties?

b) A better way to perform two-way mapping like this?

c) Advice on whether I should bother using Automapper in this scenario? Is there a compelling reason not to revert to the simpler approach of coding it up manually? eg.:

void MapManually(Client client, ClientNotificationOptionsViewModel viewModel)
{
    viewModel.Email = client.ContactDetails.Email;
    // etc
}  

void MapManually(ClientNotificationOptionsViewModel viewModel, Client client)
{
    client.ContactDetails.Email = viewModel.Email;
    // etc
}

-Brendan

P.S. restructuring domain models is not the solution.

P.P.S It would be possible to clean up the above code through extension methods & some funky reflection to set deep properties... but I'd rather use automapper features if possible.

Was it helpful?

Solution 2

In the end I found AutoMapper was unsuited to my scenario.

Instead I built a custom utility to provide bidirectional mapping & deep property mapping allowing configuration as follows. Given the scope of our project I believe this is justified.

BiMapper.CreateProfile<Client, ClientNotificationsViewModel>()
    .Map(x => x.NotificationSettings.ReceiveActivityEmail, x => x.ReceiveActivityEmail)
    .Map(x => x.NotificationSettings.ReceiveActivitySms, x => x.ReceiveActivitySms)
    .Map(x => x.ContactDetails.Email, x => x.Email)
    .Map(x => x.ContactDetails.MobileNumber, x => x.MobileNumber);

BiMapper.PerformMap(client, viewModel);
BiMapper.PerformMap(viewModel, client);

Apologies I cannot share the implementation as it's commercial work. However I hope it helps others to know that it isn't impossible to build your own, and can offer advantages over AutoMapper or doing it manually.

OTHER TIPS

This can be done also in this way:

Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
    .ForMember(x => x.NotificationSettings, opt => opt.MapFrom(x => new NotificationSettings() { ReceiveActivityEmails = x.ReceiveActivityEmails, ReceiveActivitySms = x.ReceiveActivitySms}))
    .ForMember(x => x.ContactDetails, opt => opt.MapFrom(x => new ContactDetails() { Email = x.Email, MobileNumber = x.MobileNumber }));

But is not much different than your solution.

Also, you can do it by creating a map from your model to your inner classes:

Mapper.CreateMap<ClientNotificationOptionsViewModel, ContactDetails>();

Mapper.CreateMap<ClientNotificationOptionsViewModel, NotificationSettings>();

Mapper.CreateMap<ClientNotificationOptionsViewModel, Client>()
    .ForMember(x => x.NotificationSettings, opt => opt.MapFrom(x => x))
    .ForMember(x => x.ContactDetails, opt => opt.MapFrom(x => x));

You don't need to specify ForMember in the new mappings because the properties has the same name in both classes.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top