ozz
ozz

Reputation: 5366

Map list to existing list in Automapper using a key

Automapper easily handles mapping one list of object types to another list of different objects types, but is it possible to have it map to an existing list using an ID as a key?

Upvotes: 5

Views: 4132

Answers (3)

Ayu Umi
Ayu Umi

Reputation: 1

I couldn't get Michael's example from 2013 to work, so here is an updated version of the code:

    // Converter
    public class AutomapperListConverter<TSource, TDestination> :
    ITypeConverter<List<TSource>, List<TDestination>>
    where TDestination : class
    {
    private readonly Func<TSource, object> _sourcePrimaryKeyExpression;
    private readonly Func<TDestination, object> _destinationPrimaryKeyExpression;
    private readonly IRuntimeMapper _mapper;

    public AutomapperListConverter(
        Expression<Func<TSource, object>> sourcePrimaryKey,
        Expression<Func<TDestination, object>> destinationPrimaryKey,
        IRuntimeMapper mapper
    )
    {
        _sourcePrimaryKeyExpression = sourcePrimaryKey.Compile();
        _destinationPrimaryKeyExpression = destinationPrimaryKey.Compile();
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    public static AutomapperListConverter<TSource, TDestination> Instance(
        Expression<Func<TSource, object>> sourcePrimaryKey,
        Expression<Func<TDestination, object>> destinationPrimaryKey,
        IRuntimeMapper mapper
    )
    {
        return new AutomapperListConverter<TSource, TDestination>(sourcePrimaryKey, destinationPrimaryKey, mapper);
    }

    public List<TDestination> Convert(List<TSource> source, List<TDestination> destination, ResolutionContext context)
    {
     var destinationList = destination != null ? destination.ToList(): new List<TDestination>();
        var sourceList = source.ToList();

        foreach (var sourceItem in sourceList)
        {
            var sourcePrimaryKey = GetPrimaryKey(sourceItem, _sourcePrimaryKeyExpression);
            var matchedDestination = destinationList.FirstOrDefault(dest =>
                string.Equals(
                    GetPrimaryKey(dest, _destinationPrimaryKeyExpression),
                    sourcePrimaryKey,
                    StringComparison.OrdinalIgnoreCase));

            if (matchedDestination != null)
            {
                _mapper.Map(sourceItem, matchedDestination);
            }
            else
            {
                destinationList.Add(_mapper.Map<TDestination>(sourceItem));
            }
        }

        return destinationList;
    }

    private string? GetPrimaryKey<TObject>(TObject entity, Func<TObject, object> expression)
    {
        var tempId = expression.Invoke(entity);
        return System.Convert.ToString(tempId);
    }
}
   


   // Usage
        CreateMap<List<UserDTO>, List<UserVM>>()
        .ConvertUsing((src, dest, context) => {
            var converter = AutomapperListConverter<UserDTO, UserVM>.Instance(
                dto => dto.Id,
                vm => vm.Id,
                context.Mapper);
    
            return converter.Convert(src, dest, context);
        });
    
    CreateMap<List<UserVM>, List<UserDTO>>()
        .ConvertUsing((src, dest, context) => {
            var converter = AutomapperListConverter<UserVM, UserDTO>.Instance(
                vm => vm.Id,
                dto => dto.Id,
                context.Mapper);
    
            return converter.Convert(src, dest, context);
        });

Upvotes: 0

Michael Ciba
Michael Ciba

Reputation: 561

I found this article very useful and as such I thought I would feed back in my generic version of the type converter which you can use to select the property to match on from each object.

Using it all you need to do is:

// Example of usage
Mapper.CreateMap<UserModel, User>();
var converter = CollectionConverterWithIdentityMatching<UserModel, User>.Instance(model => model.Id, user => user.Id);
Mapper.CreateMap<List<UserModel>, List<User>>().ConvertUsing(converter);

//The actual converter
public class CollectionConverterWithIdentityMatching<TSource, TDestination> : 
    ITypeConverter<List<TSource>, List<TDestination>> where TDestination : class
{
    private readonly Func<TSource, object> sourcePrimaryKeyExpression;
    private readonly Func<TDestination, object> destinationPrimaryKeyExpression;

    private CollectionConverterWithIdentityMatching(Expression<Func<TSource, object>> sourcePrimaryKey, Expression<Func<TDestination, object>> destinationPrimaryKey)
    {
        this.sourcePrimaryKeyExpression = sourcePrimaryKey.Compile();
        this.destinationPrimaryKeyExpression = destinationPrimaryKey.Compile();
    }

    public static CollectionConverterWithIdentityMatching<TSource, TDestination> 
        Instance(Expression<Func<TSource, object>> sourcePrimaryKey, Expression<Func<TDestination, object>> destinationPrimaryKey)
    {
        return new CollectionConverterWithIdentityMatching<TSource, TDestination>(
            sourcePrimaryKey, destinationPrimaryKey);
    }

    public List<TDestination> Convert(ResolutionContext context)
    {
        var destinationCollection = (List<TDestination>)context.DestinationValue ?? new List<TDestination>();
        var sourceCollection = (List<TSource>)context.SourceValue;
        foreach (var source in sourceCollection)
        {
            TDestination matchedDestination = default(TDestination);

            foreach (var destination in destinationCollection)
            {
                var sourcePrimaryKey = GetPrimaryKey(source, this.sourcePrimaryKeyExpression);
                var destinationPrimaryKey = GetPrimaryKey(destination, this.destinationPrimaryKeyExpression);

                if (string.Equals(sourcePrimaryKey, destinationPrimaryKey, StringComparison.OrdinalIgnoreCase))
                {
                    Mapper.Map(source, destination);
                    matchedDestination = destination;
                    break;
                }
            }

            if (matchedDestination == null)
            {
                destinationCollection.Add(Mapper.Map<TDestination>(source));
            }
        }

        return destinationCollection;
    }

    private string GetPrimaryKey<TObject>(object entity, Func<TObject, object> expression)
    {
        var tempId = expression.Invoke((TObject)entity);
        var id = System.Convert.ToString(tempId);
        return id;
    }
}

Upvotes: 1

k0stya
k0stya

Reputation: 4315

I have not found better way than the following.

Here are source and destination.

public class Source
{
    public int Id { get; set; } 
    public string Foo { get; set; }
}

public class Destination 
{ 
    public int Id { get; set; }
    public string Foo { get; set; }
}

Define converter (You should change List<> to whatever type you are using).

public class CollectionConverter: ITypeConverter<List<Source>, List<Destination>>
{
    public List<Destination> Convert(ResolutionContext context)
    {
        var destinationCollection = (List<Destination>)context.DestinationValue;
        if(destinationCollection == null)
            destinationCollection = new List<Destination>();
        var sourceCollection = (List<Source>)context.SourceValue;
        foreach(var source in sourceCollection)
        {
            Destination matchedDestination = null;

            foreach(var destination in destinationCollection)
            {
                if(destination.Id == source.Id)
                {
                    Mapper.Map(source, destination);
                    matchedDestination = destination;
                    break;
                }
            }
            if(matchedDestination == null)
                destinationCollection.Add(Mapper.Map<Destination>(source));
        }
        return destinationCollection;
    }
}

And here is actual mapping configuration and example.

Mapper.CreateMap<Source,Destination>();
Mapper.CreateMap<List<Source>,List<Destination>>().ConvertUsing(new CollectionConverter());

var sourceCollection = new List<Source>
{
    new Source{ Id = 1, Foo = "Match"},
    new Source{ Id = 2, Foo = "DoesNotMatchWithDestination"}
};
var destinationCollection = new List<Destination>
{
    new Destination{ Id = 1, Foo = "Match"},
    new Destination{ Id = 3, Foo = "DoeNotMatchWithSource"}
};
var mergedCollection = Mapper.Map(sourceCollection, destinationCollection);

You should get the following result.

Mapping result

Upvotes: 7

Related Questions