AutoMapper:领域模型和视图模型之间的双向深度映射

4

我需要将扁平的 ViewModel 和深层结构的 Domain model 之间进行双向映射。这在我们的解决方案中是一个常见的场景。

我的模型如下:

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

代码映射:

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

我不喜欢它的原因是:
1)它很麻烦。
2)第二种映射方式基本上拆除了Automapper的功能,需要手动实现工作。唯一的优点是在整个代码中引用Automapper时的一致性。
请问是否有更好的方法来使用Automapper进行深度属性映射?
请问是否有更好的方法执行像这样的双向映射?
在这种情况下,我是否应该使用Automapper?是否有令人信服的理由不回到手动编码的简单方法?例如:
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

附言:重构领域模型并不是解决方案。

另外,可以通过扩展方法和一些反射技巧来设置深层属性,从而清理上述代码...但如果可能的话,我宁愿使用automapper功能。


你能分享一下你的ViewModel和Domain类的代码吗?使用AfterMap而不是ForMember有什么原因吗? - thepirat000
这只是示例代码,但我已添加了领域/视图模型类。我使用AfterMap而不是ForMember(),以避免出现“表达式必须解析为顶层成员”的错误。 - Brendan Hill
2
AutoMapper从来没有为这种情况而设计,也可能永远不会。不要用它来做这个。 - Jimmy Bogard
2个回答

4
这也可以用另一种方式实现:
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 }));

但这与您的解决方案并没有太大区别。

此外,您可以通过创建从模型到内部类的映射来完成:

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

在新的映射中,您不需要指定ForMember,因为两个类中的属性名称相同。


感谢您的建议,使用MapFrom(x => x)将有助于减少代码膨胀。现在,AutoMapper代码实际上比手动代码更冗长,我正在寻找一个令人信服的理由,不要简化它并手动执行,或者自己制作一个简单的映射工具-请参见编辑后的评论。 - Brendan Hill
所以你重新发明了轮子 :) - thepirat000

-4
最终我发现AutoMapper不适合我的场景。
相反,我构建了一个自定义工具来提供双向映射和深度属性映射,允许进行以下配置。考虑到我们项目的范围,我认为这是合理的。
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);

很抱歉,我无法分享实现细节,因为这是商业项目。但是,我希望这可以帮助其他人了解,构建自己的实现并不是不可能的,并且可以比AutoMapper或手动实现更具优势。


1
当然不是不可能的 - AutoMapper可以做到。这个答案对未来的读者完全没有用处。 - Katie Kilian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接