AutoMapper bi-directional mapping between first item in collection and single property - automapper

I'm using AutoMapper 6. Consider the following classes:
public class Source
{
public FlattenableClass Flattenable { get; set; }
public List<EmailAddress> EmailAddresses { get; set; }
}
public class Destination
{
public string FlattenableProp1 { get; set; }
public string FlattenableProp2 { get; set; }
public MappedEmailAddress EmailAddress1 { get; set; }
}
where FlattenableClass has properties named Prop1 and Prop2. As you can see, the source has a collection of EmailAddress but the destination only needs the first one because although our database allows a collection of email addresses, the application is going to support one. I believe I can arrange this mapping from Source to Destination like so:
CreateMap<Source, Destination>()
.ForMember(d => d.EmailAddress1, opt => opt.ResolveUsing(s => s.EmailAddresses?.FirstOrDefault()));
but, unsurprisingly, if I then call ReverseMap() on that, it doesn't know how to map EmailAddress1 back into the EmailAddresses collection. Is there any way to get it to map EmailAddress1 back to an EmailAddress and add it to the EmailAddresses collection?

I haven't found any support in AutoMapper but I found a work around. I just created a target property in the source to do the work for me:
public class Destination
{
public string FlattenableProp1 { get; set; }
public string FlattenableProp2 { get; set; }
public MappedEmailAddress EmailAddress1 { get; set; }
public List<MappedEmailAddress> EmailAddresses
{
get
{
List<MappedEmailAddress> emails = new List<MappedEmailAddress>();
if (EmailAddress1 != null) emails.Add(EmailAddress1);
return emails;
}
set
{
if (value == null || value.Count == 0)
EmailAddress1 = new MappedEmailAddress();
else
EmailAddress1 = value[0];
}
}
}
This lets AutoMapper map Source.EmailAddresses to Destination.EmailAddresses and then Destination.EmailAddresses does the work of mapping to EmailAddress1. It's not ideal in that I had to expose collection of email addresses property which I don't want to ever be used by anyone but auto-mapper, but it gets it done.

Related

EF Core Collections using Automapper.Collection.EntityFrameworkCore

Given I have 2 classes, Foo and Bar:
public class Foo
{
private readonly List<Bar> _bars = new List<Bar>();
public int Id { get; private set; }
public string Name { get; private set; }
public IEnumerable<Bar> Bars => _bars;
public void AddBar(Bar bar)
{
_bars.Add(bar);
}
public static Foo Create(string name)
{
return new Foo { Name = name };
}
private Foo() { }
}
public class Bar
{
public int Id { get; private set; }
public string Description { get; private set; }
public static Bar Create(string description)
{
return new Bar { Description = description };
}
}
With 2 corresponding DTOs,
public class BarDto
{
public int Id { get; set; }
public string Description { get; set; }
}
public class FooDto
{
public int Id { get; set; }
public string Name { get; set; }
public List<BarDto> Bars { get; set; }
public FooDto()
{
Bars = new List<BarDto>();
}
}
And an AutoMapper/AutoMapper.Collection.EntityFrameworkCore setup of
var config = new MapperConfiguration(cfg =>
{
cfg.AddCollectionMappers();
cfg.UseEntityFrameworkCoreModel<DemoContext>();
cfg.CreateMap<BarDto, Bar>().EqualityComparison((src, dest) => src.Id == dest.Id);
cfg.CreateMap<FooDto, Foo>().ForMember(dest => dest.Bars, opt =>
{
opt.MapFrom(s => s.Bars);
opt.UseDestinationValue();
}).EqualityComparison((src, dest) => src.Id == dest.Id);
});
I have a use case whereby the incoming FooDto may contain inserted, appended, updated and deleted items in the Bars collection which I am attempting to handle by:
Looking up the existing entity from the database
Mapping changes from the DTO to the entity
Saving the changes to the database
However the following code produces an InvalidOperationException exception stating that "The instance of entity type 'Bar' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached"
var fooToUpdate = db.Foos.Include(_ => _.Bars).FirstOrDefault(_ => _.Id == fooDto.Id);
mapper.Map(fooDto, fooToUpdate);
db.SaveChanges();
My understanding was that becuase I am setting EqualityComparison for the BarDto -> Bar mapping it should update the tracked entity and the save operation should succeed becuase it was referencing the same object?
I am not sure if I'm going about this the wrong way or simply missing somthing in the configuration.
Update
It seems the problem I am facing may be related to this issue on github.

Pass different AutoMapper context per nested mapping

I know we can set the Context items when we call Map(), and it will be available to every map operation. Is there a way to change those context items during mapping?
Suppose I have these source types:
public class OuterSource {
public string TimeZone { get; set; }
public string Name { get; set; }
public InnerSource[] InnerArray { get; set; }
}
public class InnerSource {
public DateTime Created { get; set; }
public string Message { get; set; }
}
and these destination types:
public class OuterDest {
public string Name { get; set; }
public InnerDest[] InnerArray { get; set; }
}
public class InnerDest {
public DateTime Created { get; set; }
public string Message { get; set; }
}
The only difference is that InnerSource.Created is in UTC and I want to map it to the local time zone. However the time zone is in OuterSource, not InnerSource.
Normally, I would set up my mappers like so:
CreateMap<OuterSource, OuterDest>();
CreateMap<InnerSource, InnerDest>();
But that wouldn't work because when it comes to mapping InnerSource to InnerDest it does not have access to OuterSource.TimeZone.
So I'm currently forced to set my mapping like so:
CreateMap<OuterSource, OuterDest>()
.ForMember(dest => dest.InnerArray, opt => opt.ResolveUsing(
//loop through source.InnerArray and do the datetime
//conversion manually
));
I consider that a code smell. What I would love to do is to pass the timezone to the nested mapping somehow. I would appreciate any pointers towards that direction.

Automapper, mapping single destination property as a concatenation of multiple source property

I have a situation where I need to map a single property as a combination of multiple source properties based on some conditions.
Destination :
public class Email
{
public Email() {
EmailRecipient = new List<EmailRecipient>();
}
public string Subject{get; set;}
public string Body {get; set;}
public virtual ICollection<EmailRecipient> EmailRecipient { get; set; }
}
public class EmailRecipient
{
public int EmaiId { get; set; }
public string RecipientEmailAddress { get; set; }
public int RecipientEmailTypeId { get; set; }
public virtual Email Email { get; set; }
}
Source:
public class EmailViewModel
{
public List<EmailRecipientViewModel> To { get; set; }
public List<EmailRecipientViewModel> Cc { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
public class EmailRecipientViewModel
{
public string RecipientEmailAddress { get; set; }
}
I want Mapper.Map<EmailViewModel,Email>()
Here I would like to map my Email.EmailRecipient as a combination of EmailViewModel.To and EmailViewModel.Cc.
However the condition is, Email.EmailRecipient.RecipientEmailTypeId will be 1 for To and 2 for Cc
Hope my question is clear.
One possible way to achieve this is to create a map that uses a specific method for this conversion. The map creation would be:
Mapper.CreateMap<EmailViewModel, Email>()
.ForMember(e => e.EmailRecipient, opt => opt.MapFrom(v => JoinRecipients(v)));
Where the JoinRecipients method would perform the conversion itself. A simple implementation could be something like:
private ICollection<EmailRecipient> JoinRecipients(EmailViewModel viewModel) {
List<EmailRecipient> result = new List<EmailRecipient>();
foreach (var toRecipient in viewModel.To) {
result.Add(new EmailRecipient {
RecipientEmailTypeId = 1,
RecipientEmailAddress = toRecipient.RecipientEmailAddress
});
}
foreach (var ccRecipient in viewModel.Cc) {
result.Add(new EmailRecipient {
RecipientEmailTypeId = 2,
RecipientEmailAddress = ccRecipient.RecipientEmailAddress
});
}
return result;
}
I'm a huge opponent of converters, mostly because for other people in your project, things will just happen 'like magic' after the mapping call.
An easier way of handling this would be to implement the property as a method that converts other properties on the viewmodel to the required formatting. Example:
public class EmailViewModel
{
public ICollection<EmailRecipient> EmailRecipient {
get {
return To.Union(Cc);
}
}
public List<EmailRecipientViewModel> To { get; set; }
public List<EmailRecipientViewModel> Cc { get; set; }
public string Subject { get; set; }
public string Body { get; set; }
}
Now automapper automatically maps from EmailRecipient property to EmailRecipient property, and if someone is trying to figure out how it happens, they just need to look on the viewmodel.
Editing this some years later: Just as a warning, doing things this way means that every time you call EmailRecipient, you incur the o(n) task of unioning the To and Cc fields. This is fine if you're only dealing with one email, but if you're reusing the viewmodel and someone sticks it in a loop with say, every other email in the system, it might be a huge performance issue. In that case I'd go with the accepted answer so that you dodge this potential performance pitfall.

Automapper, Mapping one object member type to multiple concrete type

I have this Party class which contains an object data type coming from a service. It can contain two different member types for the Item property.
public class Party
{
public string DMVID {get; set;}
public object Item { get; set; }
}
and this DTO
public class PartyDTO
{
public string DMVID {get; set;}
public BusinessDTO BusinessItem { get; set; }
public IndividualDTO IndividualItem { get; set; }
}
How can I map the output of the Item to BusinessItem or IndividualItem.
I know this one would not work. Mapper.CreateMap<Party, PartyDTO>();
I don't know if conditional mapping can solve this or a resolver like this one.
Hey maybe this will help you out! I tested it, but i am using AutoMapper just for two days!
Allright here are your noted classes!!!
public class Party
{
public string DMVID { get; set; }
public object Item { get; set; }
}
public class PartyDTO
{
public string DMVID { get; set; }
public BuisnessDTO BusinessItem { get; set; }
public IndividualDTO IndividualItem { get; set; }
}
public class BuisnessDTO
{
public int Number
{
get;
set;
}
}
public class IndividualDTO
{
public string Message
{
get;
set;
}
}
and here your is the MapperConfiguration for this current scenario!
// Edit There was no need here for some conditions
AutoMapper.Mapper.CreateMap<Party, PartyDTO>()
.ForMember(dto => dto.BusinessItem, map =>
map.MapFrom(party => party.Item as BuisnessDTO);
)
.ForMember(dto => dto.IndividualItem, map =>
map.MapFrom(party => party.Item as IndividualDTO);
);
// And this is another way to achive the mapping in this scenario
AutoMapper.Mapper.CreateMap<PartyDTO, Party>()
.ForMember(party => party.Item, map => map.MapFrom( dto => (dto.BusinessItem != null) ? (dto.BusinessItem as object) : (dto.IndividualItem as object)));
And i created this sample for it!
Party firstParty = new Party()
{
DMVID = "something",
Item = new BuisnessDTO()
{
Number = 1
}
};
Party secondParty = new Party()
{
DMVID = "something",
Item = new IndividualDTO()
{
Message = "message"
}
};
PartyDTO dtoWithBuisness = AutoMapper.Mapper.Map<PartyDTO>(firstParty);
PartyDTO dtoWithIndividual = AutoMapper.Mapper.Map < PartyDTO>(secondParty);
Party afterParty = AutoMapper.Mapper.Map<Party>(dtoWithBuisness);
afterParty = AutoMapper.Mapper.Map < Party>(dtoWithIndividual);
Of course there are other possibility, but I think thats exactly what you wanted.

Automapper newbie question regarding list property

As a new fan of AutoMapper, how would I use it to do the following:
Given the following classes, I want to create FlattenedGroup from Group where the list of item string maps to the title property of Item.
public class Group
{
public string Category { get; set; }
public IEnumerable<Item> Items { get; set; }
}
public class Item
{
public int ID { get; set; }
public string Title { get; set; }
}
public class FlattenedGroup
{
public string Category { get; set; }
public IEnumerable<string> Items { get; set; }
}
Thanks
Joseph
The other thing you can do is create a converter from Item -> string:
Mapper.CreateMap<Item, string>().ConvertUsing(item => item.Title);
Now you don't need to do anything special in your Group -> FlattenedGroup map:
Mapper.CreateMap<Group, FlattenedGroup>();
That's all you'd need there.
Give this a try, you can probably use Linq and a lambda expression to map the list of strings in FlattenedGroup with the titles in Group.
Mapper.CreateMap<Group, FlattenedGroup>()
.ForMember(f => f.Category, opt => opt.MapFrom(g => g.Category))
.ForMember(f => f.Items, opt => opt.MapFrom(g => g.Items.Select(d => d.Title).ToList()));
Make sure you add System.Linq to your using statements

Resources