Geeks With Blogs

News This is the *old* blog. The new one is at blog.sixeyed.com
Elton Stoneman
This is the *old* blog. The new one is at blog.sixeyed.com

[Source: http://geekswithblogs.net/EltonStoneman]

This is the first of a series of posts covering my generic  anything-to-object mapping library on github: Sixeyed.Mapping.

1.       Mapping and Auto-Mapping Objects

2.       Mapping and Auto-Mapping Objects from IDataReader

3.       Mapping and Auto-Mapping Objects from XML

4.       Mapping and Auto-Mapping Objects from CSV

5.       Comparing Sixeyed.Mapping to AutoMapper

Enterprise projects typically have entities of the same kind defined multiple times to encapsulate different representations. A User domain entity may be projected into a UserModel, containing a flattened subset of the User properties for display:

image

With layers for domain entities, data contracts, service contract requests and responses, and presentation models you may have five definitions of a related entity, all of which are under your control, and all of which will (hopefully) have consistent naming conventions. Code to manually map between entity representations looks unnecessary as the source and target are so similar:

User user = GetFullUser();

UserModel model = newUserModel

    {

        DateOfBirth = user.DateOfBirth,

        FirstName = user.FirstName,

        Id = user.Id,

        LastName = user.LastName

//Intentionally leave out AddressLine1 for now

    };

This is time-consuming, error-prone, and can add a huge maintenance overhead when properties are added or removed. Neater to use a generic auto-map, which matches properties between target and source entities, and populates target objects:

User user = GetFullUser();

var model = AutoMap<User, UserModel>.CreateTarget(user);

Sixeyed.Mapping on github provides functionality for auto-mapping, and for creating static maps. (For an alternative, Jimmy Bogard’sAutoMapper on CodePlex, is well established but it has a different approach. I wanted a consistent interface for auto maps and manual maps, the ability to map from different sources, and a smaller performance hit - see Comparing Sixeyed.Mapping to AutoMapper).

Auto-Mapping

Auto-mapping is done at runtime, so when the entity definitions change there are no upstream code changes. AutoMap uses reflection, but the performance hit is relatively small and the map can be cached if it’s going to be used repeatedly. The example above is the simplest, but for cases which aren’t covered by discoverable mappings, you can specify individual property mapping actions:

var map = new AutoMap<User, UserModel>()

                .Specify((s, t) => t.AddressLine1 = s.Address.Line1) //flatten

                .Specify((s, t) => t.FirstName = s.FirstName.ToUpper()) //convert

                .Specify((s, t) => t.Address = AutoMap<Address, PartialAddress>.CreateTarget(s.Address)); //nested map

var model = map.Create(user);

Any properties not explicitly specified are auto-mapped. Mapping degrades gracefully, so any properties which can’t be mapped (either because the names can’t be matched, or the source cannot be read from, or the target cannot be written to) are not populated. (Optionally you can force exceptions to be thrown on mismatches).

AutoMap uses a naming strategy to match properties. By default this uses a simple matching algorithm, ignoring case and stripping non-alphanumeric characters. You can override the default to use exact name matching, aggressive name matching (which acts like the simple match but additionally strips vowels and double-letters), or to supply your own strategy (implementing IMatchingStrategy):

var map = new AutoMap<User, UserModel>(); //matches "IsValid" and "IS_VALID"

var exactMap = newAutoMap<User, UserModel>()

.Matching<ExactNameMatchingStrategy>(); //matches "IsValid" and "IsValid"

var aggressiveMap = newAutoMap<User, UserModel>()

.Matching<AggressiveNameMatchingStrategy>()//matches "IsValid" and "ISVLD"

var customMap = newAutoMap<User, UserModel>()

                        .Matching<LegacyNameMatchingStrategy>(); //custom, matches "IsValid" and "bit_ISVALID"

Internally, AutoMap uses the naming strategy to generate a list of IPropertyMapping objects which represent maps between source and target properties. By default the list is only cached for the lifetime of the map, so the performance cost of reflecting over the types is incurred every time an AutoMap is instantiated and used. The justification for this is that the mapping cache will grow unknowably large, so a simple dictionary cache could end up with a large memory footprint. Equally the performance hit is small, and .NET uses internal caching for reflected types, so in subsequent generations of the same type of map the performance hit will be smaller.

AutoMap does provide a caching strategy if you do want the mappings cached. You can either use the internal cache (which is a simple dictionary and will never be flushed), the standard .NET runtime cache, or provide a wrapper over your own caching layer with an ICachingStrategy implementation:

var map = newAutoMap<User, UserModel>(); //mappings not cached

var dictionaryMap = newAutoMap<User, UserModel>()

                             .Cache<DictionaryCachingStrategy>(); //mappings cached in dictionary

var cachedMap = newAutoMap<User, UserModel>()

.Cache<MemoryCacheCachingStrategy>(); //mappings cached in .NET runtime cache

 

Static Mapping

For complex maps, or for scenarios where you don’t want the reflection performance hit at all, you can define a static map. The interface is the same as AutoMap, except by default all properties have to be specified – there is no auto-mapping of unspecified targets, so additionally the naming and caching strategies are ignored.

Static object maps are derived from ClassMap, with the specifications made in the constructor (FluentNHibernate-style):

    public classUserToUserModelMap : ClassMap<User, UserModel>

    {

public UserToUserModelMap()

        {

            Specify(s => s.Id, t => t.Id);

            Specify(s => s.FirstName, t => t.FirstName);

            Specify(s => s.LastName, t => t.LastName);

            Specify((s, t) => t.AddressLine1 = s.Address.Line1);

            Specify((s, t) => t.Address.PostCode = s.Address.PostCode.Code);

        }

    }

There are various Specify overloads, so you can specify mappings in an action, or specify source and target with funcs as you prefer. Execute the map in the same way by calling Create or Populate to map from the source instance to a target:

User user = GetFullUser();

var map = new UserToUserModelMap();

UserModel model = map.Create(user);

You can mix-and-match static and auto-mapping by setting AutoMapUnspecifiedTargets, meaning that the auto-map will be used for any target properties which have not been explicitly specified:

    public class UserToUserModelMap : ClassMap<User, UserModel>

    {

        public UserToUserModelMap()

        {

            AutoMapUnspecifiedTargets = true;

            Specify((s, t) => t.AddressLine1 = s.Address.Line1);

            Specify((s, t) => t.Address.PostCode = s.Address.PostCode.Code);

        }

    }

This also allows your static map to leverage the naming and caching strategies of AutoMap.

Nested Maps

AutoMap doesn’t traverse object graphs, it will only populate properties in the first-level object (except where you have specified a mapping for a child object). To populate full graphs you can use nested auto-maps or static maps, with one of the Specify overloads to supply a conversion which invokes the map on the target property:

    Specify((s, t) => t.User = new FullUserToPartialUserMap().Create(s.User));

    //or:

    Specify(s => s.User, t => t.User, c =>new FullUserToPartialUserMap().Create(c));

    //or:

    Specify((s, t) => t.User = AutoMap<User, UserModel>.CreateTarget(s.User));

Performance

As always, the generic solution has a performance implication, although the mapping has had a couple of rounds of optimisation done to minimise the overhead. The highest-value AutoMap solution, which removes as much code and maintenance overhead as possible, has the highest impact. Populating 250,000 objects, the static AutoMap<>.CreateTarget() method takes 13 seconds, compared to 5 seconds for manually populating the targets.  Caching the map reduces the time to 8 seconds, and generating the map once and reusing it reduces it again to 7 seconds. Using a static map takes 6 seconds:

image

In a more representative sample, mapping a single object, the disparity is not so pronounced. Manual and AutoMap versions take approximately the same time; in different test runs, one will be quicker than the other. The static map is consistently faster than manually populating the target object (what? Yes. Possibly due to the hard-core reflection optimisation technique from Jon Skeet):

image

Up to 1,000 objects, the performance hit in using the AutoMap is negligible:

image

Above 1,000 objects the cost is more pronounced:

image

Note that the effort in mapping is computational, not memory-bound, so in a higher-spec system the differences will be smaller.

In a production system, adding 0.0x seconds to a service call involving a database lookup or a service call is likely to be acceptable, especially if the map is used for a single object, or the map can be reused – in which case the overhead will be 0.00x seconds. Likewise if you’re populating a single model for a view, it’s likely to be justifiable for the reduction in the solution’s technical debt.

In different scenarios, the computation of the AutoMap may be an unacceptable performance hit, in which case a static map at least isolates the mapping logic and provides some of the benefits, at a lower performance cost.

Posted on Tuesday, January 3, 2012 11:22 PM Fluent , github | Back to top


Comments on this post: Mapping and Auto-Mapping Objects

# re: Mapping and Auto-Mapping Objects
Requesting Gravatar...
This looks excellent! I've always never been thrilled with the usage of AutoMapper and this addresses many of the concerns I've had with AutoMapper. Previously I tried to leave AutoMapper for ValuInjector but found it too inconsistent.

I have some suggestions on what could really raise the bar even further. I'll open issues on github for them.
Left by Chris Marisic on Jan 04, 2012 2:55 PM

# re: Mapping and Auto-Mapping Objects
Requesting Gravatar...
Thanks Chris, glad you found it useful. One of your issues in github is already in the codebase, and the others I have left open to be fixed in the next commit.
Left by Elton on Jan 04, 2012 9:26 PM

# re: Mapping and Auto-Mapping Objects
Requesting Gravatar...
Thanks for the great info keep them coming
Left by Driving instructor Bristol on Apr 02, 2012 2:49 PM

# re: Mapping and Auto-Mapping Objects
Requesting Gravatar...
Tuve un problema con el mapeo y Mapping.So Auto muchas gracias por informar sobre el mismo.
Left by eliminar la celulitis on Jun 29, 2012 8:02 AM

Your comment:
 (will show your gravatar)


Copyright © Elton Stoneman | Powered by: GeeksWithBlogs.net