0

I use AutoMapper 10.1.1 to map a DTO to a view model. The view model already exists and needs to be updated with the new DTO, so I use the overload of Map that accepts the source and destination objects. I want AutoMapper to update the destination object, but it should not modify objects referenced by the properties of the destination object. Those objects should be treated by AutoMapper as immutable.

To clarify, several of the properties are simple types (e.g. Guid, int, string, etc.) and several of the properties are complex types (i.e. other DTO classes). Mapping the simple types is, well, simple. But mapping the properties containing complex types can be done in two different ways: AutoMapper could update the sub-properties of the existing object referenced by the main object, or it could map a new object from the source property. It appears UseDestinationValue is intended to control this behavior, but it doesn't seem to do anything:

When UseDestinationValue = true, I expect AutoMapper to do something similar to this, which is exactly what it does:

destinationObject.ComplexProperty.A = sourceObject.ComplexProperty.A;
destinationObject.ComplexProperty.B = sourceObject.ComplexProperty.B;
destinationObject.ComplexProperty.C = sourceObject.ComplexProperty.C;

When UseDestinationValue = false, I expect AutoMapper to do something similar to this:

destinationOjbect.ComplexProperty = new ComplexPropertyType() {
     A = sourceObject.ComplexProperty.A,
     B = sourceObject.ComplexProperty.B,
     C = sourceObject.ComplexProperty.C };

I need the latter behavior.

There is very little documentation on UseDestinationValue and it appears to be widely misunderstood, but note the following sentence from this link:

UseDestinationValue tells AutoMapper not to create a new object for some member, but to use the existing property of the destination object.

If UseDestinationValue() tells it not to create a new object for some member, then by default it should create a new object for each member. This is precisely the behavior I need, but it doesn't seem to work. That same link above states the default was previously UseDestinationValue = true (implying the default is now false). But even if I explicitly specify DoNotUseDestinationValue(), AutoMapper still modifies the existing member of the destination object rather than creating a new object.

Am I missing something or can you confirm this is a bug in AutoMapper?

Here is a minimal repro:

public class Widget
{
    public int Id { get; set; }
}
public class WidgetClone
{
    public int Id { get; set; }
}
public class WidgetHolder
{
    public int Id { get; set; }
    public Widget Widget { get; set; }
}
public class WidgetHolderClone
{
    public int Id { get; set; }
    public WidgetClone Widget { get; set; }
}     
[TestClass]
public class DoNotUseDestinationValueTest
{
    [TestMethod]
    public void MapToSameType()
    {
        var mapper = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<Widget, Widget>();
            cfg.CreateMap<WidgetHolder, WidgetHolder>().ForMember(wh => wh.Widget, opt => opt.DoNotUseDestinationValue());
        }).CreateMapper();
        var widgetHolder1 = new WidgetHolder() { Id = 7, Widget = new Widget() { Id = 77 } };
        var widgetHolder2 = new WidgetHolder() { Widget = new Widget() { Id = 88 } };
        var widget2 = widgetHolder2.Widget;
        mapper.Map(widgetHolder1, widgetHolder2);
        Assert.AreEqual(7, widgetHolder2.Id); // PASS
        Assert.AreEqual(77, widgetHolder2.Widget.Id); // PASS
        Assert.AreEqual(88, widget2.Id); // FAIL!
    }
    [TestMethod]
    public void MapToDifferentType()
    {
        var mapper = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<Widget, WidgetClone>();
            cfg.CreateMap<WidgetHolder, WidgetHolderClone>().ForMember(wh => wh.Widget, opt => opt.DoNotUseDestinationValue());
        }).CreateMapper();
        var widgetHolder = new WidgetHolder() { Id = 7, Widget = new Widget() { Id = 77 } };
        var widgetHolderClone = new WidgetHolderClone() { Widget = new WidgetClone() { Id = 88 } };
        var widgetClone = widgetHolderClone.Widget;
        mapper.Map(widgetHolder, widgetHolderClone);
        Assert.AreEqual(7, widgetHolderClone.Id); // PASS
        Assert.AreEqual(77, widgetHolderClone.Widget.Id); // PASS
        Assert.AreEqual(88, widgetClone.Id); // FAIL!
    }
}

I reviewed all of the following questions and none of them address this issue:

Anatoly
  • 20,799
  • 3
  • 28
  • 42
Jeremiah Mercier
  • 451
  • 5
  • 18
  • @Progman 10.1.1, which is the most recent version as of today. Great clarification though. I updated my question accordingly. – Jeremiah Mercier Dec 14 '20 at 20:59
  • Are you sure you don't have a typo in your 2nd snippet? Don't you mean `A = destinationObject.ComplexProperty.A`? – Xerillio Dec 14 '20 at 21:04
  • @Xerillio, nope. That wouldn't make sense. You want AutoMapper to take the values from your source object and apply them to your destination object. If the properties of your source and destination are immutable-like objects, you need to new up a new object from your source and apply it to your destination as shown in the second snippet. – Jeremiah Mercier Dec 14 '20 at 21:21
  • @JeremiahMercier That looks like a bug. When you check the execution plan via `configuration.BuildExecutionPlan()` it will generate the same execution plan when you use `DoNotUseDestinationValue()` and when you don't use `DoNotUseDestinationValue()` (empty action for `ForMember()` or no `ForMember()` entry at all). Only when you use `UseDestinationValue()` you will get a *different* execution plan, which is weird as it should be the default case anyway. – Progman Dec 15 '20 at 16:40
  • If you pass a destination, it will be used. The sole purpose of `DoNotUseDestinationValue` is to reset `UseDestinationValue` in case it was inherited with `Include`. Other than an anemic domain model, an anti-pattern, mapping from DTOs to models doesn't make much sense. – Lucian Bargaoanu Dec 15 '20 at 19:00
  • @LucianBargaoanu I agree that I should not need DoNotUseDestinationValue because it should be the default. However, your statement "if you pass a destination, it will be used" suggests a misunderstanding: I certainly agree that the destination object passed to the Map function will be used; that's not at issue. The question is whether the properties of that destination object that contain other objects will themselves be modified or replaced when the source is mapped to the destination. Compare the first two snippets in my original question. How do I get the behavior in the second snippet? – Jeremiah Mercier Dec 16 '20 at 03:35
  • @LucianBargaoanu I respect your opinion regarding when AutoMapper is or is not an appropriate solution for mapping. Articles have been written on this subject. However, that is outside the scope of this question. – Jeremiah Mercier Dec 16 '20 at 03:37
  • There is very little documentation on UseDestinationValue, but note this sentence in the [link](https://docs.automapper.org/en/latest/5.0-Upgrade-Guide.html#usedestinationvalue) in my original question, "UseDestinationValue tells AutoMapper not to create a new object for some member, but to use the existing property of the destination object." If UseDestinationValue tells it _not_ to create a new object for some member, then by default it _should_ create a new object for each member. That is precisely the behavior I need, but it doesn't seem to work. – Jeremiah Mercier Dec 16 '20 at 03:53
  • You're just misreading things. Everything works as expected. – Lucian Bargaoanu Dec 16 '20 at 06:03
  • @LucianBargaoanu, it may be working as expected. I can't speak to intent. I think I've shown it's not working as documented. But my issue remains: how do I tell AutoMapper to update a destination object and have it create new objects for each property, i.e. the functionality shown in my second snippet and the tests in the repro? – Jeremiah Mercier Dec 16 '20 at 14:23
  • There is nothing wrong with the docs either. That's what happens by default. The default case is when you don't pass a destination object. I think you should use a mapper that is designed for your use case. AM is not. – Lucian Bargaoanu Dec 16 '20 at 14:44
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/226050/discussion-between-jeremiah-mercier-and-lucian-bargaoanu). – Jeremiah Mercier Dec 16 '20 at 15:09

0 Answers0