Wednesday, January 20, 2010

Diving a Little Deeper into AutoMapper Part 1

Disclaimer When reading this post, please keep in mind that there are much better ways to handle what you're about to read and the sole purpose of this post is to give you examples of how to map to different types of objects with AutoMapper and it is not a best practice or implementation post





Everytime I use AutoMapper, I think this is such an awesome tool! I think it’s hilarious to go back and read my first post in April where it states, “Now automapper is one thing I'm not sure I'll benefit from yet.” and I go on to say, “I'm pretty confident that I will eventually slap myself in the face for posting this just because I'm sure it is an awesome tool that I will one day see as VERY cool, but right now...I don't”. Well, I’m slapping myself :)

So let’s dive into some awesomeness that is AutoMapper. If you haven’t read the AutoMapper – ASP.NET MVC post or downloaded Jimmy’s sample code or have any understanding of AutoMapper, you might want to read one of those items first because they will help you understand how to setup AutoMapper.

Okay, so I have this basic Domain Model:

image

I want to map it to this View Model:

image

Notice the view model is flattened out completely and ignores a few properties. Alright, let’s get to the code!

Here is my ViewToDomainProfile:

    public class ViewToDomainProfile : Profile
{
public const string NameOfProfile = "ViewToDomainProfile";
protected override string ProfileName
{
get { return NameOfProfile; }
}

protected override void Configure()
{
CreateMaps();
}

private void CreateMaps()
{
CreateMap<
string, Gender>().ConvertUsing<GenderConverter>();

CreateMap<
PersonView, Demographics>()
.ForMember(dest => dest.NationalId, opts => opts.Ignore());

CreateMap<
PersonView, ContactInfo>()
.ForMember(dest => dest.MailingAddress, opts => opts.ResolveUsing<
AddressResolver>())
.ForMember(dest => dest.EmailAddress, opts =>
opts.MapFrom(src => src.ContactInfoEmailAddress));

CreateMap<
PersonView, Person>()
.ForMember(dest => dest.ContactInfo, opts =>
opts.MapFrom(
Mapper.Map<PersonView, ContactInfo>))
.ForMember(dest=>dest.Demographics, opts=>opts.MapFrom(
Mapper.Map<PersonView, Demographics>))
.ForMember(dest => dest.PersonName, opts => opts.ResolveUsing<
PersonNameResolver>());
}
}

There are a few new techniques in here that I’ve never gone over.


First this:

CreateMap<string, Gender>().ConvertUsing<GenderConverter>();

What this says is that anytime you try to go from a string to a Gender Enum, use this class to convert it. The GenderConverter class looks like this:

    internal class GenderConverter : ITypeConverter<string, Gender>
{
public Gender Convert(string source)
{
var ignorecase = true;
return (Gender)Enum.Parse(typeof(Gender), source, ignorecase);
}
}

I tend to only make things public when it becomes necessary, which is why this one is internal. In most the samples I’ve found, the converter classes are public. I put the ignorecase variable above so you’d know what the last bool meant in the Enum.Parse. So this is pretty cool and pretty powerful. Anytime you try to map string and Gender, this is called. You’ll also notice that I don’t have Gender mapped anywhere in any of the CreateMap<> definitions.


Okay, let’s keep going.


Second this:

            CreateMap<PersonView, Demographics>()
.ForMember(dest => dest.NationalId, opts => opts.Ignore());

This code is simply ignoring my NationalId property of Demographics. If I didn’t ignore the property, the mapping would still occur, but when you’re setting up your unit test, the Mapper.AssertConfigurationIsValid(ViewToDomainProfile.NameOfProfile); would fail. So AutoMapper likes you to tell it everything so it knows if you meant to ignore something or not. Everything else I didn’t have to map because the names match. Thank you reflection & AutoMapper!


Third this:

 CreateMap<PersonView, ContactInfo>()
.ForMember(dest => dest.MailingAddress, opts => opts.ResolveUsing<
AddressResolver>())
.ForMember(dest => dest.EmailAddress, opts => opts.MapFrom(src => src.ContactInfoEmailAddress));

So this one does something funky in that it has an AddressResolver, which means basically that MailingAddress has properties and AutoMapper can only map the top-level properties. So Jimmy gives us some cool ways to handle these things. Here’s what the AddressResolver looks like:

    internal class AddressResolver:ValueResolver<PersonView, Address>
{
protected override Address ResolveCore(PersonView source)
{
return new Address
{
City = source.MailingAddressCity,
PostalCode = source.MailingAddressPostalCode,
State = source.MailingAddressState,
StreetLine = source.MailingAddressStreetLine
};
}
}

Basically it instantiates a new Address class and sets the properties. Done and Next!


Fourth this:

 CreateMap<PersonView, Person>()
.ForMember(dest => dest.ContactInfo, opts => opts.MapFrom(
Mapper.Map<PersonView, ContactInfo>))
.ForMember(dest=>dest.Demographics, opts=>opts.MapFrom(
Mapper.Map<PersonView, Demographics>))
.ForMember(dest => dest.PersonName, opts => opts.ResolveUsing<
PersonNameResolver>());

Alrighty…so this one does a few things..two of the same things and then one thing the same as “third this”. So I’m actually waiting on a response from the AutoMapper-users group on whether or not this is a good practice. It passes all the tests and I think it looks clean and made sense to me. So, hopefully I’ll get their approval. I know Jimmy responds often, so hopefully I’ll hear somthing tomorrow. I’ll update the post if it’s not a good thing :)


So the above code simply calls the other two CreateMap definitions for ContactInfo & Demographics. It also resolves the PersonName like we did with Address above. This post is already pretty long, so I won’t put that code in here…mainly cause I want to post my unit tests.


Here are the tests:


First call the AutoMapperConfigurator in the setup like this:

  [TestFixture]
public class ViewToDomainProfileTests
{
[
TestFixtureSetUp]
public void SetupDemoTests()
{
AutoMapperConfigurator.Configure();
}

The first unit test I do is the AsserConfigurationIsValid that Jimmy gives us. It looks like this:

        [Test]
public void automapper_should_be_configured_correctly()
{
Mapper.AssertConfigurationIsValid(ViewToDomainProfile.NameOfProfile);
}

Okay, then I tested the demographics area on my person & personview. Please ignore my test names…they’re not very good.

        [Test]
public void demographics_should_equal_personview_demographics()
{
var personview = new PersonView();
personview.Gender =
"Male";
personview.BirthDate =
new DateTime(1990, 1, 1);
personview.Ethnicity =
"Klingon";
var person = Mapper.Map<PersonView, Person>(personview);

Assert.AreEqual(person.Demographics.Gender, Gender.Male);
Assert.AreEqual(person.Demographics.BirthDate, personview.BirthDate);
Assert.AreEqual(person.Demographics.Ethnicity, personview.Ethnicity);
}

Then I wanted to make sure my PersonNameResolver was working, so I did this:

        [Test]
public void person_name_should_equal_personview_name()
{
var personview = new PersonView();
personview.PersonName =
"John Test Smith";

var person = Mapper.Map<PersonView, Person>(personview);

Assert.AreEqual(person.PersonName.FirstName, "John");
Assert.AreEqual(person.PersonName.MiddleName, "Test");
Assert.AreEqual(person.PersonName.LastName, "Smith");
}

Finally I wanted to make sure my ContactInfo mapping was working, so I wrote this test:

 [Test]
public void person_address_should_equal_personview_address()
{
var personview = new PersonView();
personview.MailingAddressStreetLine =
"1234 Test St.";
personview.MailingAddressCity =
"Testville";
personview.MailingAddressState =
"TX";
personview.MailingAddressPostalCode =
"12345-1234";
personview.ContactInfoEmailAddress =
"test@test.com";
personview.HomePhoneNumber =
"123-456-7890";

var person = Mapper.Map<PersonView, Person>(personview);

Assert.AreEqual(person.ContactInfo.MailingAddress.StreetLine, personview.MailingAddressStreetLine);
Assert.AreEqual(person.ContactInfo.MailingAddress.City, personview.MailingAddressCity);
Assert.AreEqual(person.ContactInfo.MailingAddress.State, personview.MailingAddressState);
Assert.AreEqual(person.ContactInfo.MailingAddress.PostalCode, personview.MailingAddressPostalCode);
Assert.AreEqual(person.ContactInfo.EmailAddress, personview.ContactInfoEmailAddress);
Assert.AreEqual(person.ContactInfo.HomePhoneNumber, personview.HomePhoneNumber);
}

All done! Stay tuned for part 2 where I basically reverse this and go DomainToView and I talk about why some of the names in my ViewModel are weird…like ContactInfoEmailAddress.



Thanks for reading!

UPDATED: 1/20/2009 7:20PM
Please note that going from domain model to view model and back to domain model is NOT a recommended practice. I did hear back from the AutoMapper user group and they wanted to make sure to point that out. Also, all the AutoMapper code is valid, so I will leave it all as is for reference of how to map certain items. I think it’s a good example of mapping random things, which was the original purpose. I do not have any of this code in a production environment, it is simply a demo on how to use AutoMapper to map your objects.

Shout it

kick it on DotNetKicks.com

blog comments powered by Disqus
Related Posts Plugin for WordPress, Blogger...