문제

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.

도움이 되었습니까?

해결책 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.

다른 팁

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.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top