AutoMapper, how to keep references between mapped

2019-06-22 07:50发布

问题:

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.

回答1:

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


回答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");
        }
    }
}