25 April 2011
As I alluded to in an earlier post (The joys of AutoMapper), I've been wanting to look into a way to get AutoMapper to work with these once-instantiated / always-valid / verbose-constructor classes I'm such a fan of. As I'd hoped, it's actually not that big of a deal and I've put together a demo project:
https://github.com/ProductiveRage/AutoMapper-By-Constructor-1
There's an example in that download (and at the bottom of this post) if curiosity gets the better of you but I'm going to step through an outline of the solution here.
Before we get going, it's worth noting that I'm hoping to expand on this solution and improve it in a number of areas - to make life easier if you're starting with this post, I've tagged the repository as "FirstImplementation" in its current state, so for the solution in its current form (as I'm about to describe), it may be best to download it from here:
https://github.com/ProductiveRage/AutoMapper-By-Constructor-1/tree/FirstImplementation
There's a class that tries to locate a property on srcType which can be used as a particular constructor argument:
public interface IPropertyGetterFactory
{
IPropertyGetter Get(Type srcType, string propertyName, Type destPropertyType);
}
The IPropertyGetterFactory implementation will apply the name-matching criteria - it will compare "propertyName" to the actual names of properties on srcType - so it will have access to:
public interface INameMatcher
{
bool IsMatch(string from, string to);
}
If the IPropertyGetterFactory manages to find a property name / type match it return an IPropertyGetter:
public interface IPropertyGetter
{
Type SrcType { get; }
PropertyInfo Property { get; }
Type TargetType { get; }
object GetValue(object src);
}
We have a class which considers all of the constructors of destType and tries to match up their argument names to srcType properties using an IPropertyGetterFactory:
public interface ITypeConverterByConstructorFactory
{
ITypeConverterByConstructor<TSource, TDest> Get<TSource, TDest>();
}
If ITypeConverterByConstructorFactory is able to find destType constructors whose arguments can be fully populated by srcType data, it returns:
public interface ITypeConverterByConstructor<TSource, TDest>
{
TDest Convert(TSource src);
ConstructorInfo Constructor { get; }
IEnumerable<PropertyInfo> SrcProperties { get; }
}
The ITypeConverterByConstructor may make use of an IConstructorInvoker implementation which handles the passing of the arguments to the constructor to create the new destType instance.
public interface IConstructorInvokerFactory
{
IConstructorInvoker<T> Get<T>(ConstructorInfo constructor);
}
public interface IConstructorInvoker<TDest>
{
TDest Invoke(object[] args);
}
For the cases where multiple destType constructors where available, a way to decide which is best is required (in most cases, we'll probably be interested in the constructor which has the most arguments, but there might be special cases):
public interface ITypeConverterPrioritiserFactory
{
ITypeConverterPrioritiser<TSource, TDest> Get<TSource, TDest>();
}
public interface ITypeConverterPrioritiser<TSource, TDest>
{
ITypeConverterByConstructor<TSource, TDest> Get(IEnumerable<ITypeConverterByConstructor<TSource, TDest>> options);
}
Some of the key elements - ITypeConverterByConstructor, IConstructorInvoker, ITypeConverterPrioritiser - have generic typeparams specified but the ITypeConverterByConstructorFactory that prepares the ITypeConverterByConstructor does not; I wanted to be able to use one ITypeConverterByConstructorFactory instance to prepare converters for various combinations of srcType, destType. This is why these key elements have factory interfaces to instantiate them - the factory class will have no typeparam specification but will create "worker" classes that do. IPropertyGetter is an exception to this pattern as I was expecting to have to have to maintain a list of them in each ITypeConverterByConstructor and so they would have to at least share a interface without typeparams.
These interfaces and corresponding classes can all be found in the GitHub repository and hopefully it will make a reasonable amount of sense now that everything's been outlined here. With a basic knowledge of reflection and AutoMapper hopefully the code won't be too difficult to read through and there are examples both in the solution itself and in the Readme.
Again, there is a repository branch that only covers what's discussed here and not all the following work I'm planning for it:
https://github.com/ProductiveRage/AutoMapper-By-Constructor-1/tree/FirstImplementation
I'm happy I've solved the initial case I set out to, but it seems now like AutoMapper needn't be as key as I was first envisaging! For cases where the types don't all match up into nice assignable-to conversions, AutoMapper definitely comes in handy - but one class of cases I'd like to use this for would be converting from (asmx) webservice interface objects (where all properties have loose getters and setters) to a validated-by-constructor class. Most of the time the property types would match and wouldn't need AutoMapper. And then maybe the conversion could be compiled using IL generation or Linq Expressions so that it would be as fast as hand-written code, just without the opportunity for typos.. Intriguing!
// Get a no-frills, run-of-the-mill AutoMapper Configuration reference..
var mapperConfig = new Configuration(
new TypeMapFactory(),
AutoMapper.Mappers.MapperRegistry.AllMappers()
);
mapperConfig.SourceMemberNamingConvention = new LowerUnderscoreNamingConvention();
// .. teach it the SourceType.Sub1 to DestType.Sub1 mapping (unfortunately AutoMapper can't
// magically handle nested types)
mapperConfig.CreateMap<SourceType.Sub1, ConstructorDestType.Sub1>();
// If the translatorFactory is unable to find any constructors it can use for the conversion,
// the translatorFactory.Get method will return null
var translatorFactory = new SimpleTypeConverterByConstructorFactory(
new ArgsLengthTypeConverterPrioritiserFactory(),
new SimpleConstructorInvokerFactory(),
new AutoMapperEnabledPropertyGetterFactory(
new CaseInsensitiveSkipUnderscoreNameMatcher(),
mapperConfig
)
);
var translator = translatorFactory.Get<SourceType, ConstructorDestType>();
if (translator == null)
throw new Exception("Unable to obtain a mapping");
// Make our translation available to the AutoMapper configuration
mapperConfig.CreateMap<SourceType, ConstructorDestType>().ConstructUsing(translator.Convert);
// Let AutoMapper do its thing!
var dest = (new MappingEngine(mapperConfig)).Map<SourceType, ConstructorDestType>(
new SourceType()
{
Value = new SourceType.Sub1() { Name = "Test1" },
ValueList = new[]
{
new SourceType.Sub1() { Name = "Test2" },
new SourceType.Sub1() { Name = "Test3" }
},
ValueEnum = SourceType.Sub2.EnumValue2
}
);
public class SourceType
{
public Sub1 Value { get; set; }
public IEnumerable<Sub1> ValueList { get; set; }
public Sub2 ValueEnum { get; set; }
public class Sub1
{
public string Name { get; set; }
}
public enum Sub2
{
EnumValue1,
EnumValue2,
EnumValue3
}
}
public class ConstructorDestType
{
private Sub1 _value;
private IEnumerable<Sub1> _valueList;
private Sub2 _valueEnum;
public ConstructorDestType(Sub1 value, IEnumerable<Sub1> valueList, Sub2 valueEnum)
{
if (value == null)
throw new ArgumentNullException("value");
if (valueList == null)
throw new ArgumentNullException("valueList");
if (!Enum.IsDefined(typeof(Sub2), valueEnum))
throw new ArgumentOutOfRangeException("valueEnum");
_value = value;
_valueList = valueList;
_valueEnum = valueEnum;
}
public Sub1 Value { get { return _value; } }
public IEnumerable<Sub1> ValueList { get { return _valueList; } }
public Sub2 ValueEnum { get { return _valueEnum; } }
public class Sub1
{
public string Name { get; set; }
}
public enum Sub2
{
EnumValue1,
EnumValue_2,
EnumValue3
}
}
Posted at 17:58
Dan is a big geek who likes making stuff with computers! He can be quite outspoken so clearly needs a blog :)
In the last few minutes he seems to have taken to referring to himself in the third person. He's quite enjoying it.