One of the projects I’m working on (at the time of writing this post) is a C# client which uses Java web services. When creating the proxies we end up with DTO objects which have a few issues, some are aesthetic others functional.
On the aesthetic from they do not adhere to the Pascal case naming convention and use arrays instead of C# Lists, on the functional side they ofcourse are lightweight DTO objects so contain no business logic or the other additional functionality which we’d want in our Domain objects.
So we have ended up with a mass of mapping factories and functionality that converts a DTO object to a Domain object and in some cases the reverse also (not all Domain’s need to be converted back to DTO’s). There’s absolutely nothing wrong with these factories and mapping mechanisms whatsoever, but I came across AutoMapper recently (although it looks like its been around for a long while – so I’m somewhat late to the party) and thought I’d try to use it on a set of similar scenarios.
I’ll start with a couple of simple DTO classes, PersonView and CountryView.
public class CountryView
{
public string name { get; set; }
}
public class PersonView
{
public string name { get; set; }
public int age { get; set; }
public CountryView country { get; set; }
public PersonView[] children { get; set; }
}
to start with the domain objects are going to be just as simple as the DTO’s but conforming to our preference of Pascal case property names and IList’s instead of arrays. So the domains look like
public class Country
{
public string Name { get; set; }
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public Country Country { get; set; }
public IList<Person> Children { get; set; }
}
Okay nothing very complicated or particularly useful in the real world, if we were trying to model a family tree, but it’s good enough for us to start playing with.
So the next question is obviously how we tell AutoMapper to associate the mapping of one type to the other. This is handled via the Mapper.CreateMap method. We need to tell the mapper how to map every object type that makes up the object tree.
Mapper.CreateMap<PersonView, Person>();
Mapper.CreateMap<CountryView, Country>();
Note: If we wanted to convert the Domain objects back to DTO we’d need Mapper.CreatMap entries with the generic parameters switched also.
Finally, we want to actually convert one type of data (in this instance the DTO) to another type of data (in this case the Domain object). To do this we simply use
PersonView dto = GetPerson();
Person domain = Mapper.Map<PersonView, Person>(dto);
I’ll leave the reader to create the GetPerson method and supply some test data. But upon successful completion of the Mapper.Map call the domain object should now have all the data copied from the DTO plus a List instead of an array.
So within AutoMapper it’s obviously matched the properties by a case insensitive comparison of the property names but what, I hear you ask, if the property names on PersonView did not match those in the domain object.
To solve this we simply add some information to the mapping declarations along the following lines (assuming the DTO object name property is now n, age is a etc.)
Mapper.CreateMap<PersonView, Person>().
ForMember(d => d.Age, o => o.MapFrom(p => p.a)).
ForMember(d => d.Name, o => o.MapFrom(p => p.n)).
ForMember(d => d.Name, o => o.MapFrom(p => p.co)).
ForMember(d => d.Name, o => o.MapFrom(p => p.ch));
Finally for this post (as it’s mean’t to be the first impressions of AutoMapper not a comprehensive user guide :)) is what if the DTO property names all end in a standard postfix, for example nameField, ageField etc. Maybe the naming convention from the guys developing the web service uses the xxxField format and we’d prefer to not use the same. We wouldn’t really want to have to create the ForMember mappings for every field if we could help it. Instead we could use
Mapper.Initialize(c => c.RecognizePostfixes("Field"));
Note: the solution above is global, so would affect all mappings, but essentially now can handle exact case insensitive matches as well as insensitive matches with the postfix “Field”.