Productive Rage

Dan's techie ramblings

Teaching AutoMapper about "verbose constructors"

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 curiousity 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

The Plan

  1. Take two Types - srcType and destType - and consider every constructor in destType..
  2. For each argument in the constructor, try to find a property in srcType that can be used as the value for that argument
    • That property must meet name-matching criteria
    • Its value must be mappable to the constructor argument's type
  3. If multiple destType constructors can be callled using srcType's data, the most appropriate one must be selected
  4. The ConstructorInfo reference and a list of "Property Getters" are handed off to a "Type Converter" class that now has all of the necessary information to create a new instance of destType given a srcType reference
    • A "Property Getter" is an object that can retrieve the value of a specified PropertyInfo from a srcType instance and cast that value to a particular type (ie. the type that the destType constructor argument requires)
  5. This "Type Converter" will expose a Convert method that accepts a srcType reference and returns a new destType instance - we can pass this as a Func<srcType, destType> to an AutoMapper ConstructUsing method call and we're all done!

The Players

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.

The Plan - Re-written

  1. Pass srcType and destType to an ITypeConverterByConstructorFactory and call Get, this will consider each of destType's constructors and determine which can have its arguments specified with data from srcType's properties by..
  2. Calling Get on an IPropertyGetterFactory for each argument name and type, passing the srcType
    • IPropertyGetterFactory will use an INameMatcher to compare property names to the specified argument name
    • IPropertyGetterFactory will use its own judgement to determine whether the property mapping is valid (eg. the AutoMapperEnabledPropertyGetterFactory will allow properties whose type can be converted as required by AutoMapper)
  3. The ITypeConverterByConstructorFactory may now have multiple ITypeConverterByConstructor instances (each will represent a ConstructorInfo and contain IPropertyGetters to retrieve data from the srcType to satisfy all of the constructor's arguments), it will use an ITypeConverterPrioritiser to pick the best one
  4. This ITypeConverterByConstructor<srcType, destType> has a method Convert which returns a new destType instance given a srcType reference - success! This allows us to hook up AutoMapper with CreateMap and ConstructUsing.

The implementation

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

And now?

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!

Example

// 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

Comments

Visual Studio Express - The Cheats!

Does it make me cheap that I use Visual Studio Express at home instead of paying hundreds of pounds for some full version? I hope not! :) I mean, isn't that why Microsoft released them, so that us hobby coders ("work" code by day, "hobby" code by evening!) can enjoy their tools??

There have been a couple of restrictions which I'd taken for granted as part of the "Express" package that had been bugging me.. until recently.

The first is very straight-forward: Visual Studio 2010's menu options weren't consistent with 2008 and me hitting [ALT]-[B]-[U] wasn't building my project, I had to go [ALT]-[D]-[B], via the "Debug" menu. Ok, it's still F6 to go direct.. But that's only because Express users would apparently prefer "Basic Settings" - this is an easy one; go through Tools / Settings / Expert Settings and now everything feels much more familiar. Hurrah!

Visual Studio 2008 - if I hit F6 to "build" the solution, it builds to the Release folder, using Release settings. If I hit F5 to "Start debugging", it builds the project (before executing it) to the Debug folder, using Debug settings. So maybe that's just how Express work..

Except it's not! Click Tools / Customize, choose the Commands tab and drag the "Solution Configuration" command from the Build menu onto one of the toolbars in Visual Studio. Now you can choose either Debug or Build before you build the solution (or any other configurations you've created in Configuration Manager).

Having said that, there are still a few things that are lacking that I bump into from time to time; there's no Threads window which you can use when pausing execution, there's no Quick Watch option which can be annoying when you're poking at deep-hierarchy objects in the debugger and - weirdly - the "Open Folder in Windows Explorer" is absent from the context menu in the Solution Explorer. But all in all I can't help but be impressed by Express products - and even more so with these tweaks applied!

Posted at 20:26

Comments