[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:
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:
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):
Up to 1,000 objects, the performance hit in using the AutoMap is negligible:
Above 1,000 objects the cost is more pronounced:
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.