Using AutoMapper: Mapping Instances

Posts in this Series

Mapping to a New Instance

As you've seen in the previous posts in this series, once your mappings are defined, it's a simple matter to actually map from one object to another:

Given:

AutoMapper.Mapper.CreateMap<SourceClass, DestinationClass>();

Then:

var destinationObject = AutoMapper.Mapper.Map<DestinatationClass>(sourceObject);

Mapping to an Existing Instance

There's a few special case scenarios to consider, though. First, the call above creates a brand new instance of DestinationClass, but often, especially in when doing something like an update of an existing object, you don't want a new instance. Instead, you want to map the data onto an existing instance. Map takes a second parameter just for this purpose:

AutoMapper.Mapper.Map(sourceObject, destinationObject);

There's two things to note here. First, we don't have to specify the type to map to for the generic Map call. This is because, now, we're passing the destination object instance, so the destination type can be determined by the type of that object. Second, we're not storing the result of this call in a variable. This is because the destination object is mapped to in place and we're not creating any new instances.

Mapping Collections

One very nice aspect of AutoMapper is that it has native support of working with collections of objects. You don't need to create a custom mapping for something like a List<DestinationClass>: as long as you've defined a mapping for DestinationClass, AutoMapper will be able to automatically map a collection of that type. The only thing you need to do is specify that appropriate collection type for the Map call:

var destinationList = AutoMapper.Mapper.Map<List<DestinationClass>>(sourceList);

This works for all collection types and interfaces: List<T>, ICollection<T>, IEnumerable<T>, etc. There's one very important gotcha here, though: if you try to employ the existing instance mapping:

AutoMapper.Mapper.Map(sourceList, destinationList);

You might be surprised to find that you've actually clobbered your destinationList. This is because AutoMapper is actually mapping the collection, not the objects in that collection, individually. This gets particularly nasty when you consider object hierarchies. Take for example:

public class Pet
{
    public string Name { get; set; }
    public string Breed { get; set; }
}

public class Person
{
    public List<Pet> Pets { get; set; }
}

public class PetDTO
{
    public string Name { get; set; }
    public string Breed { get; set; }
}

public class PersonDTO
{
    public List<PetDTO> Pets { get; set; }
}

So, here we have a Person class with a collection of Pets, and a DTO for each type. If we then created a form to update just the Name property for each pet, we'd end up with an object graph like:

{
    Pets: [
        { Name : "Sparky", Breed : null },
        { Name : "Felix", Breed : null },
        { Name : "Cujo", Breed : null }
    ]
}

Since Breed was not posted it's value is null for each PetDTO instance. Now, if we try to update Person with PersonDTO:

AutoMapper.Mapper.Map(person, personDTO);

We've actually replaced person.Pets with an entirely new collection of Pets, where each Breed property is now null. It's easy to see how you could clobber your data in this way. The solution is unfortunately a little arduous. First, we need to use AutoMapper's Ignore() method on our Pets collection:

AutoMapper.Mapper.CreateMap<PersonDTO, Person>()
    .ForMember(dest => dest.Pets,
               opts => opts.Ignore());

This tells AutoMapper that when it maps from a PersonDTO to a Person it should not map the Pets collection. This means we'll now have to handle this manually.

AutoMapper.Mapper.Map(person, personDTO);
for (int i = 0; i < person.Pets.Count(); i++)
{
    AutoMapper.Mapper.Map(person.Pets[i], personDTO.Pets[i]);
}

What we're doing here is looping through the list of pets on person and mapping each Pet individually. This assumes that your lists are the same on both sides though, i.e. you did not reorder the items and you did not allow the creation or removal of items in your form. To cover variance in ordering, you would want to rely on some sort of identifying property such as:

var pet = person.Pets[i];
var updatedPet = personDTO.Pets.Single(m => m.Id == pet.Id);
AutoMapper.Mapper.Map(pet, updatedPet);

Adds and removals are a bit more complicated:

var updatedPets = new List<Pet>();
foreach (var pet in personDTO.Pets)
{
    var existingPet = person.Pets.SingleOrDefault(m => m.Id == pet.Id);
    // No existing pet with this id, so add a new one
    if (existingPet == null)
    {
        updatedPets.Add(AutoMapper.Mapper.Map<Pet>(pet));
    }
    // Existing pet found, so map to existing instance
    else
    {
        AutoMapper.Mapper.Map(existingPet, pet);
        updatedPets.Add(existingPet);
    }
}
// Set pets to updated list (any removed items drop out naturally)
person.Pets = updatedPets;

This is probably the biggest pain point of working with AutoMapper, but as long as you keep in mind that you need to manually map list items when doing an update, it's manageable.

comments powered by Disqus