I have a flattened DTO which I need to map to a Parent with Children relationship. I'd like to do this via AutoMapper as I'm using it in other places and it works great. I've seen examples of mapping a Parent and Child but not when the Child is a collection and the source is a flattened DTO. I've created some classes that I can use for getting the configuration correct. Below are my sample classes:
public class Parent
{
public int ParentId { get; set; }
public string ParentName { get; set; }
public List<Child> Children { get; set; }
}
public class Child
{
public int ChildId { get; set; }
public string ChildName { get; set; }
}
public class ParentChildDTO
{
public int ParentId { get; set; }
public string ParentName { get; set; }
public int ChildId { get; set; }
public string ChildName { get; set; }
}
I'm performing the mapper initialization on startup. I'm not getting any errors until I try to perform the mapping. Below is my mapper initialization code. I've kept in the commented out line to show the other way that I've tried to accomplish this:
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.CreateMap<ParentChildDTO, Child>();
cfg.CreateMap<ParentChildDTO, Parent>()
.ForMember(dest => dest.Children, opt => opt.MapFrom(src => src));
//.ForMember(dest => dest.Children, opt => opt.MapFrom(src => new Child { ChildId = src.ChildId, ChildName = src.ChildName }));
});
Below is my code that I'm using for trying to perform the mapping configuration:
ParentChildDTO parentChildDTO = new ParentChildDTO { ParentId = 1, ParentName = "Parent Name", ChildId = 2, ChildName = "Child Name" };
Parent parent = AutoMapper.Mapper.Map<ParentChildDTO, Parent>(parentChildDTO);
List<LienActivity> mapTest = AutoMapper.Mapper.Map<List<BaseActivityUploadDTO>, List<LienActivity>>(request.Activities);
I've considered using a Custom Value Resolver, but was hoping to avoid the complexity and extra code if what I'm doing is possible with the correct configuration.
Here's the error that I get with the above code:
Error mapping types.
Mapping types: ParentChildDTO -> Parent
Type Map configuration: ParentChildDTO -> Parent
Property: Children
Here is another option where you define custom mapping just for the Children property.
Mapper.Initialize(cfg =>
{
cfg.CreateMap<ParentChildDTO, Parent>()
.ForMember(d => d.Children,
opt => opt.MapFrom(src => new List<Child>() { new Child() { ChildId = src.ChildId, ChildName = src.ChildName } }));
});
Given what you are going to use this for based on your comments - the below configuration should do the job (parent properties are resolved by default AutoMapper conventions so no need to explicitly map):
AutoMapper.Mapper.Initialize(cfg =>
{
cfg.CreateMap<ParentChildDTO, Parent>()
.ConstructUsing(item => new Parent
{
Children = new List<Child>
{
new Child
{
ChildId = item.ChildId,
ChildName = item.ChildName
}
}
});
});
Related
I want to like merge those Source objects into a List<Destination>. Notice that SourceParent and Destination Id property MUST be the same.
var parent = new SourceParent
{
Id = 1,
Childs = new List<SourceChild>
{
new SourceChild { ChildId = 12, OtherProperty = "prop1" },
new SourceChild { ChildId = 13, OtherProperty = "prop2" },
new SourceChild { ChildId = 14, OtherProperty = "prop3" },
}
};
Mapper.Initialize(cfb =>
{
cfb.CreateMap<SourceParent, List<Destination>>()
.ForMember(dest => dest, opt => opt.MapFrom(src => src.Childs));
cfb.ValidateInlineMaps = false;
});
List<Destination> destination = Mapper.Map<SourceParent, List<Destination>>(parent);
Classes:
public class SourceParent
{
public int Id { get; set; }
public List<SourceChild> Childs { get; set; }
}
public class SourceChild
{
public string OtherProperty { get; set; }
public int ChildId { get; set; }
}
public class Destination
{
public int SourceParentId { get; set; }
public string OtherProperty { get; set; }
public int ChildId { get; set; }
}
Is there a way to create a mapping rule for this case? Is it even possible?
I think your best option here is to define a TypeConverter.
You can do TypeConverters inline like I've done below or you can define a class that implements the ITypeConverter<TIn, TOut> interface.
cfb.CreateMap<SourceParent, List<Destination>>().ConvertUsing((src, dest, context) =>
{
return src.Childs.Select(x =>
{
var destination = context.mapper.Map<Destination>(x);
destination.SourceParentId = src.Id;
return destination;
}
});
If you wanted to (I usually stay away from this because it can get unruly fast) you could define another custom mapping using a tuple or a wrapper class like this.
cfb.CreateMap<SourceParent, List<Destination>>().ConvertUsing((src, dest, context) =>
{
return src.Childs.Select(x => context.mapper.Map<Destination>((src.Id, x)))
});
cfb.CreateMap<(int partentId, SourceChild child), Destination>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.parentId))
.ForMember(dest => dest.ChildId, opt => opt.MapFrom(src => src.child.Id))
.ForMember(dest => dest.OtherProperty , opt => opt.MapFrom(src => src.child.OtherProperty ));
This can be nice for small examples but if you are doing it often it can lead to really cluttered mapper configurations (in my opinion), but it does simplify your type converter.
I have a Tag class and a corresponding TagDto class which I want to map from Tag to TagDto.
Due to my usage of EF Core I have a Collection to a class called MoneyItemTag which represents a many-to-many relationship.
Here are my classes:
public abstract class MoneyItemBase
{
public int Id { get; set; }
public string ItemText { get; set; }
public decimal Amount { get; set; }
public MoneyItemType MoneyItemType { get; protected set; }
public ICollection<MoneyItemTag> MoneyItemTags { get; set; }
}
public class MoneyItemTag
{
public int MoneyItemId { get; set; }
public MoneyItemBase MoneyItem { get; set; }
public int TagId { get; set; }
public Tag Tag { get; set; }
}
public class Tag
{
public int TagId { get; set; }
public string TagName { get; set; }
public ICollection<MoneyItemTag> MoneyItemTags { get; set; }
}
[Fact]
public void Tag_TagDto_Mapping()
{
Mapper.Initialize(cfg =>
{
//cfg.AddProfile<MappingProfile>();
cfg.CreateMap<Tag, TagDto>()
.ForMember(x => x.MoneyItems, opts => opts.MapFrom(src => src.MoneyItemTags.Select(x => x.MoneyItem)));
cfg.CreateMap<MoneyItemBase, MoneyItemBaseDto>()
.ForMember(x => x.Tags, opts => opts.MapFrom(src => src.MoneyItemTags.Select(y => y.Tag.TagName).ToList()));
});
MoneyItemTag mo = new MoneyItemTag();
mo.MoneyItem = new SingleIncome() { Id = 2, ItemText = "test", Active = false, DueDate = DateTime.Now, Amount = 33 };
mo.MoneyItemId = 2;
var mit = new List<MoneyItemTag>() { mo };
Tag tag = new Tag() { TagId = 2, TagName = "test", MoneyItemTags = mit };
mo.TagId = 2;
mo.Tag = tag;
var dto = Mapper.Map<TagDto>(tag);
Assert.NotNull(dto);
And this relationship is causing some trouble. When I run the following test or when I want make the mapping in my Asp.Net core application, I get the following exception:
AutoMapper.AutoMapperMappingException : Error mapping types.
Mapping types:
Tag -> TagDto
HouseholdBook.Data.Model.Tag -> HouseholdBook.Dto.TagDto
Type Map configuration:
Tag -> TagDto
HouseholdBook.Data.Model.Tag -> HouseholdBook.Dto.TagDto
Property:
MoneyItems
---- AutoMapper.AutoMapperMappingException : Error mapping types.
Mapping types:
MoneyItemBase -> MoneyItemBaseDto
HouseholdBook.Data.Model.MoneyItemBase -> HouseholdBook.Dto.MoneyItemBaseDto
Type Map configuration:
MoneyItemBase -> MoneyItemBaseDto
HouseholdBook.Data.Model.MoneyItemBase -> HouseholdBook.Dto.MoneyItemBaseDto
Property:
Id
-------- System.NullReferenceException : Object reference not set to an
instance of an object.
What is wrong here? I cannot see from the exception message what I am missing.
You need to create explicit mappings for all the nested classes:
cfg.CreateMap<HouseholdBook.Data.Model.Tag, HouseholdBook.Dto.TagDto>();
opts => opts.MapFrom() is used when members names in source and target doesn't match, it doesn't register map between these types.
So finally I figured out what was wrong.
MoneyItemBase is abstract and abstract classes cannot be instantiated directly.
I ended up with mapping my class hierarchy using the Include method of CreateMap.
Below is a snippet from my example.
CreateMap<MoneyItemBase, MoneyItemBaseDto>()
.Include<SingleIncome, SingleMoneyItemDto>()
.ForMember(x => x.Tags, opts => opts.MapFrom(src => src.MoneyItemTags.Select(y => y.Tag.TagName).ToList()));
CreateMap<SingleIncome, SingleMoneyItemDto>();
All. For example we have such simple classes
public class SimpleModel
{
public int PropertyId { get; set; }
public ICollection<SimpleModelCollectionItem> SimpleCollection { get; set; }
}
public class SimpleModelCollectionItem
{
public int PropertyId { get; set; }
}
public class SimpleEntity
{
public int Id { get; set; }
public int PropertyId { get; set; }
public virtual ICollection<SimpleEntityCollectionItem> SimpleCollection { get; set; }
}
public class SimpleEntityCollectionItem
{
public int Id { get; set; }
public int PropertyId { get; set; }
}
and we have some configuration code
AutoMapper.Mapper.CreateMap<SimpleModel, SimpleEntity>()
.ForMember(dest => dest.Id, src => src.Ignore())
.ForMember(dest => dest.SimpleCollection, src => src.UseDestinationValue());
AutoMapper.Mapper.CreateMap<SimpleModelCollectionItem, SimpleEntityCollectionItem>()
.ForMember(dest => dest.Id, src => src.Ignore());
AutoMapper.Mapper.AssertConfigurationIsValid();
and test data initialization
var model = new SimpleModel
{
PropertyId = 2,
SimpleCollection =
new List<SimpleModelCollectionItem>
{
new SimpleModelCollectionItem
{
PropertyId = 7
}
}
};
var entity = new SimpleEntity
{
Id = 1,
PropertyId = 3,
SimpleCollection =
new List<SimpleEntityCollectionItem>
{
new SimpleEntityCollectionItem
{
Id = 5,
PropertyId = 4
}
}
};
AutoMapper.Mapper.Map(model, entity);
and I expect to see
Console.WriteLine(entity.Id); // 1
Console.WriteLine(entity.PropertyId); // 2
Console.WriteLine(entity.SimpleCollection.First().Id); // 5 but was 0
Console.WriteLine(entity.SimpleCollection.First().PropertyId); // 7
Is it possible to set Id for inner collection equals to 5 as it was initially?
So, when AutoMapper was mapping your collection, it actually removed the old item from the destination collection and then added a new item.
You can verify this by using this code:
var item_before = entity.SimpleCollection.First();
AutoMapper.Mapper.Map(model, entity);
var item_after = entity.SimpleCollection.First();
bool item_same_references = object.ReferenceEquals(item_before, item_after);
The value of item_same_references will be false.
This happens even if you are using src => src.UseDestinationValue(). Using this only means that the collection object it self should be reused, but not the items in that collection.
I am not sure if there is a way to tell AutoMapper to also use the same collection items.
Also, thinking about it, what happens if the destination collection contains more of fewer items than the source collection?
So, the zero you are getting is because when AutoMapper creates the new item to add to the collection, the default value of the Id property is default(int) which is zero.
I suggest the following:
I am assuming that the number of items in the source and destination collections are equal.
First, modify your mapping configuration to instruct AutoMapper to ignore the collection like this:
AutoMapper.Mapper.CreateMap<SimpleModel, SimpleEntity>()
.ForMember(dest => dest.Id, opt => opt.Ignore())
.ForMember(dest => dest.SimpleCollection, opt => opt.Ignore());
Then, create a method like this that maps collection items in place (without removing and then adding new items) like this:
public void MapCollectionsInPlace<TSource, TDestination>(IEnumerable<TSource> source_collection,
IEnumerable<TDestination> destination_collection)
{
var source_enumerator = source_collection.GetEnumerator();
var destination_enumerator = destination_collection.GetEnumerator();
while (source_enumerator.MoveNext())
{
if(!destination_enumerator.MoveNext())
throw new Exception("Source collection has more items than destination collection");
AutoMapper.Mapper.Map(source_enumerator.Current, destination_enumerator.Current);
}
}
Now, you can use the following code:
AutoMapper.Mapper.Map(model, entity);
MapCollectionsInPlace(model.SimpleCollection, entity.SimpleCollection);
I would use AutoMapper to create an expression to use with LINQ (e.g. LINQ To SQL):
void Main()
{
Mapper.CreateMap<Person, PersonDto>();
Mapper.Engine.CreateMapExpression<Person, PersonDto>().ToString().Dump();
}
public class Person
{
public string Name { get; set; }
public IEnumerable<Person> Children { get; set; }
}
public class PersonDto
{
public string Name { get; set; }
public IEnumerable<PersonDto> Children { get; set; }
}
But a StackOverflowException occurs mapping the "Children" property (I had already written to Jimmy in the past).
The author told that the problem is that the root class has a self reference, but it's not supported by many LINQ providers.
If the root class has not self-references the problem is another with AutoMapper 3,
because the produced LINQ expression is:
x => new PersonDto() { Name = x.Name, Children = x.Children.Select(y => new AddressDto { Name = y.Name })}
Instead of:
x => new PersonDto() { Name = x.Name, Children = IIF((x.Children == null), null, x.Children.Select(y => new AddressDto { Name = y.Name }))}
So if the child property of the root class is null, the projection will fail.
At the moment is not possible to use AutoMapper for this purpose.
So i have this;
public class Parent
{
public string SomeProperty { get; set; }
public Child ChildProperty { get; set; }
}
public class Child
{
public string ChildProperty { get; set; }
public string OtherChildProperty { get; set; }
}
public class Flat
{
public string SomeProperty { get; set; }
public string ChildProperty { get; set; }
}
And then I do this;
Flat source = new Flat { SomeProperty = "test", ChildProperty = "test" };
Parent dest = GetParentFromDataContext();
Mapper.Map<Flat,Parent>(source,dest);
Then my expectation is that dest.ChildProperty.OtherChildProperty is still set to whatever it was when it was pulled from the datacontext. However I'm struggling to do this.
If I CreateMap as so, then I get a "must resolve to top-level member" exception;
Mapper.CreateMap<Flat,Parent>()
.ForMember(dest => dest.Parent.ChildProperty.ChildProperty,
options => options.MapFrom(source => source.ChildProperty))
.ForMember(dest => dest.Parent.ChildProperty.OtherChildProperty,
options => options.Ignore());
However if I do the following, then the new Child {} replaces the Child pulled from the datacontext essentially clearing OtherChildProperty;
Mapper.CreateMap<Flat,Parent>()
.ForMember(dest => dest.Child
options => options.MapFrom(source => new Child { ChildProperty = source.ChildProperty } ));
How can i map this and preserve the child properties I wish to ignore?
You are trying to reverse flattening process with automapper and that is just wrong tool for the job. See this SO question.
Inelegant, but this works;
Mapper.CreateMap<Flat,Parent>()
.ForMember(dest => dest.ChildProperty, options => options.Ignore());
Mapper.CreateMap<Flat,Child>()
.ForMember(dest => dest.OtherChildProperty, options => options.Ignore());
Mapper.Map<Flat,Parent>(source,dest);
Mapper.Map<Flat,Child>(source,dest.Child);