Почему Automapper сопоставляет конкретную коллекцию с новым экземпляром при каждой ссылке на исходную коллекцию?
У меня есть исходный объект, который содержит 2 ссылки на одну и ту же коллекцию. Если я сопоставлю исходный тип со структурно-эквивалентным целевым типом, AutoMapper создаст два экземпляра коллекции в целевом экземпляре.
class SourceThing
{
public string Name { get; set; }
public List<int> Numbers { get; set; }
public List<int> MoreNumbers { get; set; }
}
class TargetThing
{
public string Name { get; set; }
public List<int> Numbers { get; set; }
public List<int> MoreNumbers { get; set; }
}
Если я создам SourceThing
который имеет две ссылки на один и тот же список, сопоставьте его с TargetThing
, результат TargetThing
с двумя отдельными экземплярами коллекции.
public void MapObjectWithTwoReferencesToSameList()
{
Mapper.CreateMap<SourceThing, TargetThing>();
//Mapper.CreateMap<List<int>, List<int>>(); // passes when mapping here
var source = new SourceThing() { Name = "source" };
source.Numbers = new List<int>() { 1, 2, 3 };
source.MoreNumbers = source.Numbers;
Assert.AreSame(source.Numbers, source.MoreNumbers);
var target = Mapper.Map<TargetThing>(source);
Assert.IsNotNull(target.Numbers);
Assert.AreSame(target.Numbers, target.MoreNumbers); // fails
}
Должно ли это быть поведением отображения по умолчанию для конкретных коллекций в AutoMapper? Через тестирование я понял, что если я сопоставил List<int>
в List<int>
Я добиваюсь того поведения, которое хочу, но не понимаю почему. Если AutoMapper отслеживает ссылки и не отображает сопоставленный объект, он не увидит, что source.MoreNumbers
указывает на тот же список, что и source.Numbers
и установить цель соответственно?
2 ответа
Я сделал еще несколько исследований и мастерить. Внутренне, поскольку механизм отображения проходит по графу объектов, он выбирает лучший модуль отображения для каждого типа источника / типа назначения. Если нет нестандартного сопоставления (упрощенно), движок затем будет искать зарегистрированного сопоставителя для типа источника и назначения. Если он находит его, он создает целевой объект, а затем обходит и сопоставляет все свойства. Он также помещает этот целевой объект в ResolutionContext.InstanceCache
, который является Dictionary<ResolutionContext, object>
, Если тот же исходный объект встречается снова в том же вызове корневого отображения, он извлечет объект из кэша, вместо того, чтобы тратить время на повторное сопоставление.
Однако, если нет зарегистрированного преобразователя, двигатель выбирает следующий применимый преобразователь, который в этом случае является AutoMapper.Mappers.CollectionMapper
, Отображатель коллекции создает целевую коллекцию, перечисляет исходную коллекцию и сопоставляет каждый элемент. Он не добавляет целевой объект в кеш. Это явно дизайн.
Контекст разрешения
Что мне действительно интересно, так это то, как объекты кэшируются в InstanceCache. Ключом является текущий ResolutionContext, который содержит тип источника и назначения, а также значение источника. ResolutionContext переопределяет GetHashCode() и Equals(), которые используют те же методы базового значения источника. Я могу определить равенство в пользовательском классе так, чтобы исходная коллекция с несколькими равными, но различными экземплярами этого класса отображалась в коллекцию с несколькими ссылками на один и тот же экземпляр.
Этот класс:
class EquatableThing
{
public string Name { get; set; }
public override bool Equals(object other)
{
if (ReferenceEquals(this, other)) return true;
if (ReferenceEquals(null, other)) return false;
return this.Name == ((EquatableThing)other).Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
Сопоставьте коллекцию с 2 одинаковыми (но отдельными) вещами, и в результате вы получите коллекцию с 2 указателями на одну и ту же вещь!
public void MapCollectionWithTwoEqualItems()
{
Mapper.CreateMap<EquatableThing, EquatableThing>();
var thing1 = new EquatableThing() { Name = "foo"};
var thing2 = new EquatableThing() { Name = "foo"};
Assert.AreEqual(thing1, thing2);
Assert.AreEqual(thing1.GetHashCode(), thing2.GetHashCode());
Assert.AreNotSame(thing1, thing2);
// create list and map this thing across
var list = new List<EquatableThing>() { thing1, thing2};
var result = Mapper.Map<List<EquatableThing>, List<EquatableThing>>(list);
Assert.AreSame(result[0], result[1]);
}
Сохранить ссылки
Я, например, задаюсь вопросом, почему поведение AutoMapper по умолчанию не будет отображать граф объекта как можно ближе к структуре назначения. N исходных объектов приводит к N целевым объектам. Но так как это не так, я хотел бы видеть опцию в методе Map для Preserve References, как это сделал бы сериализатор. Если выбрана эта опция, то каждая отображаемая ссылка помещается в Словарь с использованием средства сравнения равенств ссылок и исходного объекта для ключа и пункта назначения в качестве значения. По сути, если что-то уже сопоставлено, используется объект результата этой карты.
Нет ничего плохого в поведении, это просто, как карты отображаются автоматически.
В верхнем разделе вы создаете список чисел, а затем применяете его ко второму списку чисел. Затем вы можете сравнить, и они одинаковы, потому что объект имеет 2 указателя на один и тот же список. Он не копировал цифры, он просто сделал новую ссылку, как вы и просили.
Теперь перейдите к автомату. Он проходит и отображает от одного объекта до эквивалентного объекта. Он отображает каждое из свойств отдельно, копируя информацию. Таким образом, даже несмотря на то, что в источнике есть указатели на номера в виде указателя на один и тот же список, Autopper отображает каждое из них отдельно. Зачем? Это отображение свойств, а не изучение указателей свойств. И вы бы не хотели, чтобы это делалось в большинстве случаев.
Имеет ли это смысл?
Если конечная цель состоит в том, чтобы пройти тестирование, вопрос заключается не в том, "делать числа и больше чисел, указывающих на один и тот же объект", а в том, чтобы "делать числа и больше чисел, содержащих точно такой же список". В первом случае ответом на оба вопроса является "да", поскольку существует один объект (список) для чисел и большего числа чисел. Во втором ответ - ложь, затем истина, поскольку список эквивалентен, но он не указывает на тот же самый точный объект.
Если вы действительно хотите, чтобы это был один и тот же объект, вам придется играть в игру немного по-другому. Если вы просто хотите узнать, содержит ли список те же элементы, измените утверждение.