AutoMapper, how to keep references between mapped objects?
Asked Answered
S

3

6

I am using AutoMapper to convert a UI model to POCOs that I later serialize to XML using a DataContractSerializer in order to preserve the references between them.

The problem comes that, when mapping, the references between those entities are lost.

The UI classes reference each other, but the mapping process makes new instances for every reference, so the original relations are broken :(

Let me explain:

I have 2 entities of type Person

    Person 
    { 
        List<House> OwnedHouses 
    }

And these 2 objects

John who owns

  • House1

Will who also owns

  • House1

When AutoMapper maps each Person correctly, but when it also maps House1 as two different instances!!

So I have a two copies of House1. John owns his House1 (#1) and Will owns his House1 (#2).

They are not linked anymore.

Is there any way to keep the relations that originally existed?

Thanks.

EDITED: Actually what I have is this:

A Document contains a list of ChildDocuments. Each ChildDocument has a list of Designables (Rectangles, Lines, Ellipses…) and a especial designable called ChildDocumentAdapter that contains itself ANOOTHER ChildDocument. This is the trouble, it can reference another ChildDocument.

The diagram

Strow answered 21/5, 2013 at 9:27 Comment(0)
M
5

If I'm understanding the question, you're performing two separate mapping operations - one for John, another for Will.

@Sunny is right. AutoMapper is not designed to do this. Each call you make to Mapper.Map() is typically independent of any other. By using the same instance of the HouseListConverter, you get the benefit of caching all mapped houses in a dictionary. But you have to either register it globally or pass it as an option to the mapping calls you want grouped together. That's not just extra work, it's hiding a very important implementation detail deep within the converter.

If you map both John and Will in one operation, by putting them into a collection, the output would be what you want without the need for a custom converter or resolver.

It may be an easier alternative for other people with a similar problem.

public void MapListOfPeopleWithSameHouse()
{
    Mapper.CreateMap<Person, PersonDTO>();
    Mapper.CreateMap<House, HouseDTO>();

    var people = new List<Person>();
    var house = new House() { Address = "123 Main" };
    people.Add(new Person() { Name = "John", Houses = new List<House>() { house } });
    people.Add(new Person() { Name = "Will", Houses = new List<House>() { house } });

    var peopleDTO = Mapper.Map<List<PersonDTO>>(people);
    Assert.IsNotNull(peopleDTO[0].Houses);
    Assert.AreSame(peopleDTO[0].Houses[0], peopleDTO[1].Houses[0]);
}
Mathur answered 13/8, 2015 at 18:54 Comment(0)
J
2

While Automapper is not designed with this in mind, it's powerful enough to let you do it, using custom type converters. You need to create your own converter from IList<House> to IList<HouseDto>, and inject it using a factory:

using System;
using System.Collections.Generic;
using AutoMapper;
using NUnit.Framework;
using SharpTestsEx;

namespace StackOverflowExample
{
    public class House
    {
        public string Address { get; set; }
    }

    public class Person
    {
        public IList<House> OwnedHouse { get; set; }
    }

    public class HouseDto
    {
        public string Address { get; set; }
    }

    public class PersonDto
    {
        public IList<HouseDto> OwnedHouse { get; set; }
    }

    [TestFixture]
    public class AutomapperTest
    {
        public interface IHouseListConverter : ITypeConverter<IList<House>, IList<HouseDto>>
        {
        }

        public class HouseListConverter : IHouseListConverter
        {
            private readonly IDictionary<House, HouseDto> existingMappings;

            public HouseListConverter(IDictionary<House, HouseDto> existingMappings)
            {
                this.existingMappings = existingMappings;
            }

            public IList<HouseDto> Convert(ResolutionContext context)
            {
                var houses = context.SourceValue as IList<House>;
                if (houses == null)
                {
                    return null;
                }

                var dtos = new List<HouseDto>();
                foreach (var house in houses)
                {
                    HouseDto mapped = null;
                    if (existingMappings.ContainsKey(house))
                    {
                        mapped = existingMappings[house];
                    }
                    else
                    {
                        mapped = Mapper.Map<HouseDto>(house);
                        existingMappings[house] = mapped;
                    }
                    dtos.Add(mapped);
                }

                return dtos;
            }
        }

        public class ConverterFactory
        {
            private readonly IHouseListConverter resolver;
            public ConverterFactory()
            {
                resolver = new HouseListConverter(new Dictionary<House, HouseDto>());
            }

            public object Resolve(Type t)
            {
                return t == typeof(IHouseListConverter) ? resolver : null;
            }
        }

        [Test]
        public void CustomResolverTest()
        {
            Mapper.CreateMap<House, HouseDto>();
            Mapper.CreateMap<IList<House>, IList<HouseDto>>().ConvertUsing<IHouseListConverter>();
            Mapper.CreateMap<Person, PersonDto>();

            var house = new House {Address = "any"};
            var john = new Person {OwnedHouse = new List<House> {house}};
            var will = new Person { OwnedHouse = new List<House> { house } };

            var converterFactory = new ConverterFactory();
            var johnDto = Mapper.Map<PersonDto>(john, o=>o.ConstructServicesUsing(converterFactory.Resolve));
            var willDto = Mapper.Map<PersonDto>(will, o=>o.ConstructServicesUsing(converterFactory.Resolve));

            johnDto.OwnedHouse[0].Should().Be.SameInstanceAs(willDto.OwnedHouse[0]);
            johnDto.OwnedHouse[0].Address.Should().Be("any");
        }
    }
}  
Jehovist answered 21/5, 2013 at 18:58 Comment(2)
I'll have to check it, but I think you got the point and gave a RIGHT solution. Thanks a lot. Just let me see if I can apply it to my context (it's a bit more complex). Big thanks, dude!Strow
I have just updated the original post to show the real context. A more detailed description is in the thread of the official AutoMapper mailing list here: groups.google.com/forum/?fromgroups#!topic/automapper-users/…Strow
S
1

In my case (.NET 8; AutoMapper 13.0.1) I had to opt in to preserving of references, by adding .PreserveReferences() to the CreateMap call. Example:

var Mapper = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Aircraft, AircraftDTO>().PreserveReferences();
    cfg.CreateMap<AircraftModel, AircraftModelDTO>().PreserveReferences();
    cfg.CreateMap<AircraftType, AircraftTypeDTO>().PreserveReferences();
}).CreateMapper();

In AutoMapper docs they state that you need to opt in:

2.7 Circular references Previously, AutoMapper could handle circular references by keeping track of what was mapped, and on every mapping, check a local hashtable of source/destination objects to see if the item was already mapped. It turns out this tracking is very expensive, and you need to opt-in using PreserveReferences for circular maps to work. Alternatively, you can configure MaxDepth

If I remove "PreserveReferences", then "Assert.ReferenceEquals" returns false for the same objects, but with it, the result is true (noting, that mapping is done in one operation, as mentioned in the accepted answer).

Southwestwardly answered 12/6, 2024 at 9:53 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.