Reputation: 5366
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
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
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
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.
Upvotes: 7