AutoMapper versus Extension Methods versus Implicit Casts
Download Project
Imagine the database code is legacy, can’t be changed, and the Database Person object from the database namespace looks like this:
using System; using System.ComponentModel.DataAnnotations; namespace WcfToEntityAutomapperExample.DAL.Model { /// <summary> /// Example of an Person object where properties are not defined in a way you want /// to expose via WCF. /// </summary> class PersonRow { public int Id { get; set; } [Required] [StringLength(50)] public string FName { get; set; } [Required] [StringLength(50)] public string MName { get; set; } [Required] [StringLength(50)] public string LName { get; set; } [Required] public DateTime BD { get; set; } public DateTime? DD { get; set; } } }
You can’t use this in your WCF service, and not just beccause PersonRow, FName and LName just look tacky, though that is reason alone. No, the real problem is ambiguity and confusion. MName isn’t exactly clear. Is it Maiden name, Mother’s last name, or middle name? And what is BD and DD? Such confusion and ambiguity isn’t acceptable in an exposed API.
So you create a Person DataContract to expose with WCF that looks like this:
using System; using System.Runtime.Serialization; namespace WcfToEntityAutomapperExample.Services.Model { [DataContract] public class Person { [DataMember] public string FirstName { get; set; } [DataMember] public string MiddleName { get; set; } [DataMember] public string LastName { get; set; } [DataMember] public DateTime DateOfBirth { get; set; } [DataMember] public DateTime? DateOfDeath { get; set; } } }
Wow. That will look much better in your exposed WCF API.
So now there is a problem. We need to convert Person to PersonRow.
Solving with extension methods
It would be easy to write extension methods to do this:
- Add an extension method
using WcfToEntityAutomapperExample.DAL.Model; using WcfToEntityAutomapperExample.Services.Model; namespace WcfToEntityAutomapperExample.Extensions { static class PersonExtensions { public static PersonRow ToPersonRow(this Person person) { return new PersonRow { FName = person.FirstName, MName = person.MiddleName, LName = person.LastName, BD = person.DateOfBirth, DD = person.DateOfDeath }; } } }
- Add a reverse mapping extension method.
OK. Now we have this extension method and we can use it anywhere we want. However, we forgot. We need to do this in reverse too. We need an extension method for PersonRow to Person. So add this method to PersonExtensions class.
using WcfToEntityAutomapperExample.DAL.Model; using WcfToEntityAutomapperExample.Services.Model; namespace WcfToEntityAutomapperExample.Extensions { static class PersonExtensions { public static PersonRow ToPersonRow(this Person person) { return new PersonRow { FName = person.FirstName, MName = person.MiddleName, LName = person.LastName, BD = person.DateOfBirth, DD = person.DateOfDeath }; } public static Person ToPerson(this PersonRow personRow) { return new Person { FirstName = personRow.FName, MiddleName = personRow.MName, LastName = personRow.LName, DateOfBirth = personRow.BD, DateOfDeath = personRow.DD }; } } }
- Use the extension method in the web service call.
using System.Collections.Generic; using System.Linq; using WcfToEntityAutomapperExample.DAL; using WcfToEntityAutomapperExample.DAL.Model; using WcfToEntityAutomapperExample.Extensions; using WcfToEntityAutomapperExample.Services.Interfaces; using WcfToEntityAutomapperExample.Services.Model; namespace WcfToEntityAutomapperExample.Services { public class Service1 : IService1 { public void AddPersonExtensionMethod(Person person) { using (var dbContext = new PersonDbContext()) { dbContext.People.Add(person.ToPersonRow()); dbContext.SaveChanges(); } } public List<Person> FindExtensionMethod(string lastName) { using (var dbContext = new PersonDbContext()) { var foundPeopleFromDb = dbContext.People.Where(p => p.LName == lastName).ToList(); return foundPeopleFromDb.Select(p => p.ToPerson()).ToList(); } } } }
Now, you have 40 other objects to do this too.
Hint: Take a moment to compare this to the AutoMapper method below and ask yourself which is better.
Extension Method Conclusion
Simple. Easy to use. Easy to read. Makes sense. The extension method name is an important part of this clarity. I used ToPerson and ToPersonRow. But it would also work with AsPerson and AsPersonRow.
Anybody can read this code and understand it.
If another field is added it is easy to add to the extension method on a single place so code isn’t strewn about.
Using AutoMapper
Why is AutoMapper better than the above extension method? Let’s do the same thing with AutoMapper. You be the judge of whether it is a better solution.
Well, so far, I can’t find any benefit from AutoMapper.
Here is what I need to do:
- Add AutoMapper library from NuGet. That adds a dll and another dependency to maintain.
- Create a static class to configure AutoMapper mappings: AutoMapperConfig.cs.
- Add mappings both ways: From Person to PersonRow and from PersonRow to Person.
using AutoMapper; using WcfToEntityAutomapperExample.DAL.Model; using WcfToEntityAutomapperExample.Services.Model; namespace WcfToEntityAutomapperExample.Map { public static class AutoMapperConfig { internal static void RegisterMappings() { Mapper.CreateMap<Person, PersonRow>() .ForMember(dest => dest.FName, opt => opt.MapFrom(src => src.FirstName)) .ForMember(dest => dest.MName, opt => opt.MapFrom(src => src.MiddleName)) .ForMember(dest => dest.LName, opt => opt.MapFrom(src => src.LastName)) .ForMember(dest => dest.BD, opt => opt.MapFrom(src => src.DateOfBirth)) .ForMember(dest => dest.DD, opt => opt.MapFrom(src => src.DateOfDeath)).ReverseMap(); } } }
Now, you have 40 other objects to do this too.
Hint: Take a moment to compare this to the extension method above and ask yourself which is better. - Find a global location to call AutoMapperConfig.cs: Global.asax/Global.asax.cs.
Note: If you don’t have a Global.asax/Global.asax.cs, then you need to add this.using System; using System.Web; using WcfToEntityAutomapperExample.Map; namespace WcfToEntityAutomapperExample { public class Global : HttpApplication { protected void Application_Start(object sender, EventArgs e) { AutoMapperConfig.RegisterMappings(); } protected void Session_Start(object sender, EventArgs e) { } protected void Application_BeginRequest(object sender, EventArgs e) { } protected void Application_AuthenticateRequest(object sender, EventArgs e) { } protected void Application_Error(object sender, EventArgs e) { } protected void Session_End(object sender, EventArgs e) { } protected void Application_End(object sender, EventArgs e) { } } }
- Call it in your service.
using System.Collections.Generic; using System.Linq; using AutoMapper; using WcfToEntityAutomapperExample.DAL; using WcfToEntityAutomapperExample.DAL.Model; using WcfToEntityAutomapperExample.Extensions; using WcfToEntityAutomapperExample.Services.Interfaces; using WcfToEntityAutomapperExample.Services.Model; namespace WcfToEntityAutomapperExample.Services { public class Service1 : IService1 { public void AddPersonAutoMapper(Person person) { using (var dbContext = new PersonDbContext()) { dbContext.People.Add(Mapper.Map<PersonRow>(person)); dbContext.SaveChanges(); } } public List<Person> FindAutoMapper(string lastName) { using (var dbContext = new PersonDbContext()) { var foundPeopleFromDb = dbContext.People.Where(p => p.LName == lastName).ToList(); return foundPeopleFromDb.Select(Mapper.Map<Person>).ToList(); } } } }
AutoMapper conclusion
Something just isn’t right here. You have to call more complex code to get an object converted. The ReverseBack() method saves us from having to create the reverse copy manually. Still, there are two methods and three lambda’s per property to copy. Hardly saving code or making life easier.
The configuration code looks way more complex than the extension method code. I had a bug in my mapper config, and I couldn’t see it because the code is so busy.
The AutoMapper code also isn’t intuitive. The methods aren’t obvious and it is not clear what it is doing without reading the documentation. Mapper.Map
Note: AutoMapper supposedly adds a feature that allows for a copy if the properties are the same with just one line of code: Mapper.CreateMap
I can see how if you had a lot of objects with identical properties that AutoMapper would be tempting. Still, the naming and lack of readability gets to me. Mapping.Map
If a field is added and named the same, nothing has to be done and AutoMapper works. However, if the fields are named differently, then you still have to add it to the Mapper config.
Implicit Casts
You could do this with Implicit casts.
- Add an implicit cast tot he object under your control, Person.
using System; using System.Runtime.Serialization; using WcfToEntityAutomapperExample.DAL.Model; namespace WcfToEntityAutomapperExample.Services.Model { [DataContract] public class Person { [DataMember] public string FirstName { get; set; } [DataMember] public string MiddleName { get; set; } [DataMember] public string LastName { get; set; } [DataMember] public DateTime DateOfBirth { get; set; } [DataMember] public DateTime? DateOfDeath { get; set; } // User-defined conversion from Digit to double public static implicit operator Person(PersonRow personRow) { return new Person { FirstName = personRow.FName, MiddleName = personRow.MName, LastName = personRow.LName, DateOfBirth = personRow.BD, DateOfDeath = personRow.DD }; } // User-defined conversion from double to Digit public static implicit operator PersonRow(Person person) { return new PersonRow { FName = person.FirstName, MName = person.MiddleName, LName = person.LastName, BD = person.DateOfBirth, DD = person.DateOfDeath }; } } }
The implicit cast is not included in the client code so it is fine to add to the Person DataContract.
- Use it in your services.
using System.Collections.Generic; using System.Linq; using AutoMapper; using WcfToEntityAutomapperExample.DAL; using WcfToEntityAutomapperExample.DAL.Model; using WcfToEntityAutomapperExample.Extensions; using WcfToEntityAutomapperExample.Services.Interfaces; using WcfToEntityAutomapperExample.Services.Model; namespace WcfToEntityAutomapperExample.Services { public class Service1 : IService1 { public void AddPersonImplicitCast(Person person) { using (var dbContext = new PersonDbContext()) { dbContext.People.Add(person); dbContext.SaveChanges(); } } public List<Person> FindImplicitCast(string lastName) { using (var dbContext = new PersonDbContext()) { var foundPeopleFromDb = dbContext.People.Where(p => p.LName == lastName).ToList(); return foundPeopleFromDb.Select(p => (Person)p).ToList(); } } } }
Implicit Cast Conclusion
Implicit Cast was pretty simple. I didn’t need any other classes. However, it muddied up a DataContract model class.
It is not obvious why you can add a Person where a PersonRow is needed, but it makes sense.
If I add a property or field, I’d have to add it to the cast.
My Winner
To me it is the extension method, with implicit casts a close second. I just like the simplicity of the code. A first year developer can understand and use it. I also like that it doesn’t muddy up the Model object iself like implicit operators do. Nor does it require me to create a mapping config, and initialize the mapping config.
A read a unit testing argument that unit tests won’t fail when a field is added. I had to disagree. I can put refection code in my unity test fail a test if a property is not copied. Now the reflection code is in a test project not in the production project.
My Loser
AutoMapper. It just doesn’t add the simplicity that it claims to. It by far the most complex in this scenario. Complexity != better. The gains of auto mapping Properties and Fields with the same name doesn’t outweigh the losses in readability.
Also, extension methods are far faster than AutoMapper. I didn’t do benchmarks but 7 times is what other have found. I have some data sets that take a couple of seconds to return. Times a couple of seconds by 7 and you will quickly see that such performance matters. The cost to use reflection when looping through can’t be good for you.
Also, I don’t buy into the argument that performance doesn’t matter. Performance issues pile up over time. I agree that you should not write unreadable code to optimize before you know know that readable code performs poorly. However, if two pieces of code are clear and readable and one is more performant, use the more performant. You shouldn’t make your code more complex and harder to understand to get unnecessary optimization. But with AutoMapper, you are making your code more complex and harder to understand to get less performance? How does that make sense?
Script the creation of the extension methods for objects with members named the same. You’ll be better off for it. You could even add the script to a pre-build command so the extension method is updated pre-build whenever a property is added.
Implicit cast seams to be interesting, but you need to reference your entity model in your datacontract project and it can be an issue if you share your datacontract dll with other parties.
I don't know this method thanks for your share.
I use extension method to convert my object, I agree with your point of view on AutoMapper, it's a great solution when most of the properties has same name in other case it's a waste of time.