I'm somewhat new to AutoMapper and wanted to map a POCO-ish object to a perhaps more complex DTO, the latter tries to be a representation of a Google Books API's Volume resource:
Book.cs
public class Book
{
public string Isbn10 { get; set; }
public string Isbn13 { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public string Publisher { get; set; }
public DateTime Publication { get; set; }
public int Pages { get; set; }
public string Description { get; set; }
public bool InStock { get; set; }
}
BookDto.cs
public class BookDto
{
public string Kind { get; set; }
public string Id { get; set; }
public VolumeInfo VolumeInfo { get; set; }
}
public class VolumeInfo
{
public string Title { get; set; }
public List<string> Authors { get; set; }
public string Publisher { get; set; }
public string PublishedDate { get; set; }
public string Description { get; set; }
public int PageCount { get; set; }
public List<IndustryIdentifier> IndustryIdentifiers { get; set; }
}
public class IndustryIdentifier
{
public string Type { get; set; }
public string Identifier { get; set; }
}
So according to the documentation we could simply flatten the nested type:
AutoMapperConfigurator.cs
public static class AutoMapperConfigurator
{
public static void Configure()
{
Mapper.CreateMap<Book, BookDto>()
.ForMember(dto => dto.Id, options => options.Ignore())
.ForMember(dto => dto.Kind, options => options.Ignore())
.ForMember(dto => dto.VolumeInfo.Title, options => options.MapFrom(book => book.Title))
.ForMember(dto => dto.VolumeInfo.Authors, options => options.MapFrom(book => book.Author.ToArray()))
.ForMember(dto => dto.VolumeInfo.Publisher, options => options.MapFrom(book => book.Publisher))
.ForMember(dto => dto.VolumeInfo.PublishedDate, options => options.MapFrom(book => book.Publication))
.ForMember(dto => dto.VolumeInfo.Description, options => options.MapFrom(book => book.Description))
.ForMember(dto => dto.VolumeInfo.PageCount, options => options.MapFrom(book => book.Pages))
;
}
}
But unfortunately when running the Mapper.AssertConfigurationIsValid()
Test I'm getting the following error:
System.ArgumentException : Expression 'dto => dto.VolumeInfo.Title' must resolve to top-level member and not any child object's properties. Use a custom resolver on the child type or the AfterMap option instead. Parameter name: lambdaExpression
So now following that advice trying with AfterMap:
public static class AutoMapperConfigurator
{
public static void Configure()
{
Mapper.CreateMap<Book, BookDto>()
.ForMember(dto => dto.Id, options => options.Ignore())
.ForMember(dto => dto.Kind, options => options.Ignore())
.AfterMap((book, bookDto) => bookDto.VolumeInfo = new VolumeInfo
{
Title = book.Title,
Authors = new List<string>(){ book.Author },
Publisher = book.Publisher,
PublishedDate = book.Publication.ToShortDateString(),
Description = book.Description,
PageCount = book.Pages
});
}
}
When running the test again I'm now getting this message:
Unmapped members were found. Review the types and members below. Add a custom mapping expression, ignore, add a custom resolver, or modify the source/destination type Book -> BookDto (Destination member list) Dotnet.Samples.AutoMapper.Book -> Dotnet.Samples.AutoMapper.BookDto (Destination member list) VolumeInfo
Should I create additional mappings between the nested classes? Any guidance will be sincerely appreciated, thanks much in advance.
I've done something similar before using .ForMember with an internal mapping for the VolumnInfo mapping:
public static class AutoMapperConfigurator
{
public static void Configure()
{
Mapper.CreateMap<Book, VolumeInfo>()
.ForMember(dto => dto.Authors, options => options.MapFrom(book => book.Author.Split(',')))
.ForMember(dto => dto.PublishedDate, options => options.MapFrom(book => book.Publication))
.ForMember(dto => dto.PageCount, options => options.MapFrom(book => book.Pages))
.ForMember(dto => dto.IndustryIdentifiers, options => options.Ignore());
Mapper.CreateMap<Book, BookDto>()
.ForMember(dto => dto.Id, options => options.Ignore())
.ForMember(dto => dto.Kind, options => options.Ignore())
.ForMember(dto => dto.VolumeInfo, options => options.MapFrom(book => Mapper.Map<Book, VolumeInfo>(book)));
}
}
Here are a couple of unit tests that verify the functionality:
[TestFixture]
public class MappingTests
{
[Test]
public void AutoMapper_Configuration_IsValid()
{
AutoMapperConfigurator.Configure();
Mapper.AssertConfigurationIsValid();
}
[Test]
public void AutoMapper_MapsAsExpected()
{
AutoMapperConfigurator.Configure();
Mapper.AssertConfigurationIsValid();
var book = new Book
{
Author = "Castle,Rocks",
Description = "Awesome TV",
InStock = true,
Isbn10 = "0123456789",
Isbn13 = "0123456789012",
Pages = 321321,
Publication = new DateTime(2012, 11, 01),
Publisher = "Unknown",
Title = "Why I Rock"
};
var dto = Mapper.Map<Book, BookDto>(book);
Assert.That(dto.Id, Is.Null);
Assert.That(dto.Kind, Is.Null);
Assert.That(dto.VolumeInfo, Is.Not.Null);
Assert.That(dto.VolumeInfo.Authors, Is.Not.Null);
Assert.That(dto.VolumeInfo.Authors.Count, Is.EqualTo(2));
Assert.That(dto.VolumeInfo.Authors[0], Is.EqualTo("Castle"));
Assert.That(dto.VolumeInfo.Authors[1], Is.EqualTo("Rocks"));
Assert.That(dto.VolumeInfo.Description, Is.EqualTo("Awesome TV"));
Assert.That(dto.VolumeInfo.IndustryIdentifiers, Is.Null);
Assert.That(dto.VolumeInfo.PageCount, Is.EqualTo(321321));
Assert.That(dto.VolumeInfo.PublishedDate, Is.EqualTo(new DateTime(2012, 11, 01).ToString()));
Assert.That(dto.VolumeInfo.Publisher, Is.EqualTo("Unknown"));
Assert.That(dto.VolumeInfo.Title, Is.EqualTo("Why I Rock"));
}
}