Lambda Expression and Object Mappers

Here’s a simple implementation of an object mapper using lambda expressions that I’ve shown yesterday at the Tulsa School of Dev. Say we need to copy some data from one object to another. For this example my objects are a Book and an AlternateBook. The class are shown below:

public class Book
{
    public Book()
    {
    }

    public Book(string title, int yearPublished)
    {
        Title = title;
        YearPublished = yearPublished;
    }

    public string Title { get; set; }
    public int YearPublished { get; set; }
    public string AuthorFirstName { get; set; }
    public string AuthorLastName { get; set; }
}

public class AlternateBook
{
    public string AuthorFullName { get; set; }
    public string Title { get; set; }
}

I could have a mapper that worked like this:

Mapper mapper = new Mapper(book, alternateBook);
mapper.AddMap("Title", "Title");
mapper.AddMap("AuthorFirstName", "AuthorFullName");

That approach would have some problems, though:

  • The properties are mapped using string literals, which means we don’t benefit from strong-typing or IntelliSense (bugs could easily arise because of typos or properties that don’t exist on the objects);
  • The properties have a 1:1 mapping, which in the sample above wouldn’t really work, since I had to map "AuthorFullName" directly to "AuthorFirstName", as oppose AuthorFirstName and AutorLastName.
  • The mapper would have to use Reflection to look up the properties on the given objects and then assign their values (not the most optimized way of doing it…).

The approach using Lambda expression would look like this:

var mapper = new Mapper<Book, AlternateBook>(); mapper.AddMapping((b, ab) => ab.Title = b.Title); mapper.AddMapping((b, ab) => ab.AuthorFullName = b.AuthorFirstName + " " +
b.AuthorLastName);

Points to note:

  • As I instantiate the Mapper I provide the type of objects as Generic parameters;
  • The AddMapping method takes in lambda expressions that receive two parameters (b and ab, for book and alternate book, respectively), which I then use to set the mapping. That gives me both strong-typing and IntelliSense support.
  • The lambda expression also opens up control over how things are pushed into the target object. In the sample below, I can format how the alternate book’s AuthorFullName property gets populated;
  • Since the lambda expression is compiled down to a delegate, there’s no Reflection involved, so the code should run faster when executed.

Now, in case you’re not familiar with lambdas yet: the expression does NOT get executed when we are passing it as a parameter to the AddMapping method. At that point we are only registering the m apping; the actual execution occurs whenever we want it to. I would probably, say, at the application start, register all mappings I’m likely to use. Eventually, when I need to actually process the mapping (copying some data from one object to another), I can just call the ApplyMapping, passing in the objects involved with the mapping. For example:

Book book = new Book { AuthorFirstName = "Claudio", AuthorLastName = "Lassala", Title = "Some book" }; AlternateBook alternateBook = new AlternateBook();
mapper.ApplyMappingsTo(book, alternateBook); Console.WriteLine( "Book:"+ "\r\n Title: "+ book.Title + "\r\n AuthorFirstName: " + book.AuthorFirstName + "\r\n AuthorLastName: " + book.AuthorLastName); Console.WriteLine( "\r\nAlternateBook:" + "\r\n Title: " + alternateBook.Title + "\r\n AuthorFullName: " + alternateBook.AuthorFullName);

The code above should produce the following results:

lambda

So, how’s the Mapper class implemented? Here we go:

public class Mapper<TSource, TTarget>
{
    private Collection<Action<TSource, TTarget>> m_Mappings = 
                    new Collection<Action<TSource, TTarget>>();
    public Collection<Action<TSource, TTarget>> Mappings
    {
        get { return m_Mappings; }
    }

    public void AddMapping(Action<TSource, TTarget> action)
    {
        Mappings.Add(action);
    }

    public void ApplyMappingsTo(TSource source, TTarget target)
    {
        foreach (var mapping in Mappings)
        {
            mapping(source, target);
        }
    }
}

The class keeps a list of all the mappings within its Mappings collection, which is a collection of Action<TSource, TTarget> delegates. Both TSource and TTarget are Generic types provided to the class. The AddMaping method takes in the delegate (which we pass in as a lambda) and adds it to the Mappings collection. The ApplyMappingsTo method  takes in instances for the source and target object, iterates through the Mappings collection, and invokes each mapping (don’t forget it, the mapping is a delegate). 

That’s it. Again, this is just a simple implementation. I’m pretty sure there’s a lot of improvements to be done on top of this, and there’s definitely something some people wrote out there around the same lines; I’ll be looking for that myself in order to see how I can improve this.

For instance, one thing that pops up is: what if the developer messed up and specified the mapping going from the target to the source instead? Like so:

mapper.AddMapping((b, ab) => b.Title = ab.Title);

We certainly don’t want to copy the title from the Alternate Book to the Book object. I don’t know of way yet to have the compiler catch that problem (any ideas?). During runtime, though, I know I could have the AddMapping method analyze the Expression Tree for the lambda it was given, and make sure that the target appears at the left side of the = sign, and the target appears at the right side.

Another improvement that could be made is some optimization. For instance, itake the following code snippet:

mapper.AddMapping((b, ab) => ab.AuthorFullName = b.AuthorFirstName + " " +
b.AuthorLastName);

There, the developer has used string concatenation to build up the value to be assigned to the property. The AddMapping method could have some logic that analyzes the expression tree, and if it finds something like string concatenation, it could replace that with using a StringBuilder. Of course, such optimization should only be done if we can clearly see that as being something that we really needed to squeeze better performance out of.

Also, it’d be convenient to have the mapper just figure out what properties to copy from the source to the target by looking for properties that map 1:1 (that is, property name and type exist on both objects), and then let the developer only supply special mappings for properties that require transformation.

I’ll be doing some more research on better ways to do this kind of stuff.

Advertisements
  1. #1 by William on May 14, 2008 - 9:54 am

    Claudio,
     
    I have read a few of your blogs and I have seen the Collection<T> statement. Is Collection a new keyword that I am not familiar with that is interchangeable with List<T>? I saw it in another post and I thought it was a mistype and now I see it again and now believe it can\’t possibly be a misstype.
     
    Also on your last wishlist item:
     
    "Also, it\’d be convenient to have the mapper just figure out what properties to copy from the source to the target by looking for properties that map 1:1 (that is, property name and type exist on both objects), and then let the developer only supply special mappings for properties that require transformation. "
     
    Wouldn\’t that bring you back to using reflection?
     
    Anyway, I keep learning good quality stuff every time I come here. Keep up the great content!

  2. #2 by Claudio on May 15, 2008 - 8:10 am

    Hi Bill,
     
    I\’m glad my blog is useful for you.
     
    Collection<T> is defined under System.Collections.ObjectModel. The recommendation is that List<T> should be used only internally, or if performance is the most important thing. Collection<T>, on the other hand, should be used whenever the list is exposed in the object\’s API because it provides some extension points that List<T> doesn\’t have. If you expose members as List<T> outside of a class, you get code analysis warnings about it. You can get more information here:
     
    http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=440624&SiteID=1
     
    http://blogs.msdn.com/fxcop/archive/2006/04/27/585476.aspx
     
    That said, in this post\’s example, I should probably *not* have exposed my Mappings property. I mean, I don\’t see any reason why it should be exposed outside the class (I\’ve created the AddMapping method in order to control what gets put in that collection).
     
    Regarding the last item on my wish list, you\’re right, we\’d go back to using reflection, which may or may not pose such a performance hit. Things could possibly be cached anyways, so that the performance hit would only happen at the first hit. Offering that alternative would probably make the class easy for developers to use, since they wouldn\’t have to know much about lambda (which is something that\’ll take a while for people to get used to; many developers don\’t quite get delegates yet…). Sometimes we should favor easy of use over performance, and in this case it\’d be easy enough to either allow the developer to just say "copy from this object to the other" (reflection), or "this is how you should copy from this to that" (lambda).

  3. #3 by Poul K. Sørensen (@pksorensen) on August 7, 2013 - 2:31 pm

    I came across this post searching for something else, but you can use reflection to do the property mapping for names that match with same types. But use it to build the lambda expressions the first time and then cache the lamba. Then you get the same performance as you wrote the lambda your self for all mappings after the first.

    • #4 by claudiolassala on August 7, 2013 - 2:37 pm

      Hi Paul. Yes, thanks for the tip. Wow, it’s been 5 years since I wrote that post. Had even forgotten about it. 🙂

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: