Super-simple Object Mapper
If you need a full-featured object mapper with minimal set up, I recommend you take a look at AutoMapper on Codeplex. If you need a quick-and-dirty solution, maybe the following code could help you out.
First off, why am I not using AutoMapper? At my current client, there are strict rules as to the use of open source software. There's a process in place for requesting the use of a particular open source tool, but with the red-tape of the approval process (reviews, signatures, justification, etc…), it could literally take 3 – 6 months. It's just not worth it for what I need right now. So I rolled my own.
This mapper is super-simple, not very smart and may have a bug or two in it, but it works for what I need it to do and reduces a lot of hand coding. USE AT YOUR OWN RISK!
It uses two simple rules to map data between two objects:
- If a property name and type on the source match the name and type of a destination property, the value is copied.
- If the user has defined a custom mapping action, use that to copy data (but rule #1 is always executed first).
Let's dig into the details.
First, I set up a generic class that takes in a couple of types – my source and destination types. I added a clause on the destination type that it must be 'new-able' so that I could provide a utility function that would create a destination object, map it's values from a source and return it to you.
public class Mapper<TSource, TDest> where TDest : new()
{
}
Copying Properties
When copying data from a source object to a destination object, we get all public instance properties of the destination and see if they have a matching (same name and same type) property on the source. If so, we set the value on our destination object:
protected virtual void CopyMatchingProperties(TSource source, TDest dest)
{
foreach(var destProp in typeof(TDest).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanWrite))
{
var sourceProp =
typeof (TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.Name == destProp.Name && p.PropertyType == destProp.PropertyType).
FirstOrDefault();
if( sourceProp != null)
{
destProp.SetValue(dest, sourceProp.GetValue(source, null), null);
}
}
}
Custom Transformations
I want the ability to define my own transformation for special cases. This is simply a list of Action<TSource, TDest> delegates:
protected readonly IList<Action<TSource, TDest>> mappings = new List<Action<TSource, TDest>>();
public virtual void AddMapping(Action<TSource, TDest> mapping)
{
mappings.Add(mapping);
}
Again, simple yet functional.
Perform Mappings
The last thing we need is a couple of methods to execute the actual mapping:
public virtual TDest MapObject(TSource source, TDest dest)
{
CopyMatchingProperties(source, dest);
foreach(var action in mappings)
{
action(source, dest);
}
return dest;
}
public virtual TDest CreateMappedObject(TSource source)
{
TDest dest = new TDest();
return MapObject(source, dest);
}
You'll see that "CreatedMappedObject" was the reason we needed to have the new-able clause on the TDest generic parameter.
Usage
Now let's put this into action! Given a simple domain object and view model:
public class DomainObject
{
public string Name { get; set; }
public DateTime DOB { get; set; }
public int Age { get; set; }
public string Address { get; set; }
}
public class ViewModel
{
public string Name { get; set; }
public int Age { get; set; }
}
As you can see, our view model only needs the Name and Age. Our mapping code looks like this:
var mapper = new Mapper<DomainObject, ViewModel>();
var viewModel = mapper.CreateMappedObject(domainObject);
if the view model is created somewhere else and pre-populated with other data, we would use the MapObject method instead of creating a new instance of ViewModel:
var mapper = new Mapper<DomainObject, ViewModel>();
var viewModel = InitializeViewModel();
viewModel = mapper.MapObject(domainObject, viewModel);
Now let's assume we want to add the user's birth year to the view:
public class ViewModel
{
public string Name { get; set; }
public int Age { get; set; }
public int BirthYear { get; set; }
}
Yes, we could pass along the entire date of birth, but this way the view model is getting only what it needs and doesn't need to do any additional processing to get the year:
var mapper = new Mapper<DomainObject, ViewModel>();
mapper.AddMapping((source,dest) => dest.BirthYear = source.DOB.Year);
var viewModel = mapper.CreateMappedObject(domainObject);
To encourage re-use and centralize the setup of any custom transformations, I create a subclass of my Mapper class:
public class DomainModelToViewModelMapper : Mapper<DomainObject, ViewModel>
{
public DomainModelToViewModelMapper()
{
this.AddMapping((s, d) => d.BirthYear = s.DOB.Year);
}
}