Productive Rage

Dan's techie ramblings

The Less-Effort Extendable LINQ-compilable Mappers

The last post almost finished off something I originally started back last April and enabled the creation of Compilable Type Converters which take properties from a source type and feed them in as constructor arguments on a destination type.

The only issue I had is that the final code to set up conversions was a bit verbose. To create a Converter from SourceEmployee to DestEmployee -

public class SourceEmployee
{
    public string Name { get; set; }
    public SourceRole Role { get; set; }
}

public class SourceRole
{
    public string Description { get; set; }
}

public class DestEmployee
{
    public DestEmployee(string name, DestRole role)
    {
        Name = name;
        Role = role;
    }
    public string Name { get; private set; }
    public DestRole Role { get; private set; }
}

public class DestRole
{
    public DestRole(string description)
    {
        Description = description;
    }
    public string Description { get; private set; }
}

the following code was required:

var nameMatcher = new CaseInsensitiveSkipUnderscoreNameMatcher();

var roleConverterFactory = new CompilableTypeConverterByConstructorFactory(
    new ArgsLengthTypeConverterPrioritiserFactory(),
    new CombinedCompilablePropertyGetterFactory(
        new ICompilablePropertyGetterFactory[]
        {
            new CompilableAssignableTypesPropertyGetterFactory(nameMatcher),
            new CompilableEnumConversionPropertyGetterFactory(nameMatcher)
        }
    )
);

var employeeConverterFactory = new CompilableTypeConverterByConstructorFactory(
    new ArgsLengthTypeConverterPrioritiserFactory(),
    new CombinedCompilablePropertyGetterFactory(
        new ICompilablePropertyGetterFactory[]
        {
            new CompilableAssignableTypesPropertyGetterFactory(nameMatcher),
            new CompilableEnumConversionPropertyGetterFactory(nameMatcher),
            new CompilableTypeConverterPropertyGetterFactory<SourceRole, DestRole>(
                nameMatcher,
                roleConverterFactory.Get<SourceRole, DestRole>()
            )
        }
    )
);

var employeeConverter = employeeConverterFactory.Get<SourceEmployee, DestEmployee>();

For more complicated type graphs this could quickly get tiring! What I really wanted to do was this:

var nameMatcher = new CaseInsensitiveSkipUnderscoreNameMatcher();
var converterFactory = new ExtendableCompilableTypeConverterFactory(
    nameMatcher,
    new ArgsLengthTypeConverterPrioritiserFactory(),
    new ICompilablePropertyGetterFactory[]
    {
        new CompilableAssignableTypesPropertyGetterFactory(nameMatcher),
        new CompilableEnumConversionPropertyGetterFactory(nameMatcher)
    }
);
converterFactory = converterFactory.CreateMap<SourceRole, DestRole>();
var converter = converterFactory.Get<SourceEmployee, DestEmployee>();

The ExtendableCompilableTypeConverterFactory

This class basically wraps up the duplication seen above and returns a new ExtendableCompilableTypeConverterFactory instance each time that CreateMap is successfully called, the new instance having a Compilable Property Getter than can support that mapping. If the CreateMap calls was not successful then an exception will be raised - this will be case if there is no constructor on the destination type whose arguments can all be satisified by properties on the source type (this also covers cases where additional mappings are required for referenced types). This exception is equivalent to the AutoMapperMappingException that AutoMapper throws in similar circumstances.

I'm just going to jump right in with this - if you've been reading this far then this will hold no challenges or surprises.

public class ExtendableCompilableTypeConverterFactory : ICompilableTypeConverterFactory
{
    private INameMatcher _nameMatcher;
    private ITypeConverterPrioritiserFactory _converterPrioritiser;
    private List<ICompilablePropertyGetterFactory> _basePropertyGetterFactories;
    private Lazy<ICompilableTypeConverterFactory> _typeConverterFactory;
    public ExtendableCompilableTypeConverterFactory(
        INameMatcher nameMatcher,
        ITypeConverterPrioritiserFactory converterPrioritiser,
        IEnumerable<ICompilablePropertyGetterFactory> basePropertyGetterFactories)
    {
        if (nameMatcher == null)
            throw new ArgumentNullException("nameMatcher");
        if (converterPrioritiser == null)
            throw new ArgumentNullException("converterPrioritiser");
        if (basePropertyGetterFactories == null)
            throw new ArgumentNullException("basePropertyGetterFactories");

        var basePropertyGetterFactoryList = new List<ICompilablePropertyGetterFactory>();
        foreach (var basePropertyGetterFactory in basePropertyGetterFactories)
        {
            if (basePropertyGetterFactory == null)
                throw new ArgumentException("Null entry encountered in basePropertyGetterFactories");
            basePropertyGetterFactoryList.Add(basePropertyGetterFactory);
        }

        _nameMatcher = nameMatcher;
        _converterPrioritiser = converterPrioritiser;
        _basePropertyGetterFactories = basePropertyGetterFactoryList;
        _typeConverterFactory = new Lazy<ICompilableTypeConverterFactory>(
            getConverterFactory,
            true
        );
    }

    private ICompilableTypeConverterFactory getConverterFactory()
    {
        return new CompilableTypeConverterByConstructorFactory(
            _converterPrioritiser,
            new CombinedCompilablePropertyGetterFactory(_basePropertyGetterFactories)
        );
    }

    /// <summary>
    /// This will return null if a converter could not be generated
    /// </summary>
    public ICompilableTypeConverterByConstructor<TSource, TDest> Get<TSource, TDest>()
    {
        return _typeConverterFactory.Value.Get<TSource, TDest>();
    }

    ITypeConverter<TSource, TDest> ITypeConverterFactory.Get<TSource, TDest>()
    {
        return Get<TSource, TDest>();
    }

    /// <summary>
    /// This will throw an exception if unable to generate the requested mapping - it will
    /// never return null. If the successful, the returned converter factory will be able
    /// to convert instances of TSourceNew as well as IEnumerable / Lists of them.
    /// </summary>
    public ExtendableCompilableTypeConverterFactory CreateMap<TSourceNew, TDestNew>()
    {
        // Try to generate a converter for the requested mapping
        var converterNew = _typeConverterFactory.Value.Get<TSourceNew, TDestNew>();
        if (converterNew == null)
            throw new Exception("Unable to create mapping");
        return AddNewConverter<TSourceNew, TDestNew>(converterNew);
    }

    /// <summary>
    /// Generate a further extended converter factory that will be able to handle conversion
    /// of instances of TSourceNew as well as IEnumerable / Lists of them. This will never
    /// return null.
    /// </summary>
    public ExtendableCompilableTypeConverterFactory AddNewConverter<TSourceNew, TDestNew>(
        ICompilableTypeConverter<TSourceNew, TDestNew> converterNew)
    {
        if (converterNew == null)
            throw new ArgumentNullException("converterNew");

        // Create a property getter factory that retrieves and convert properties using this
        // converter and one that does the same for IEnumerable properties, where the
        // IEnumerables' elements are the types handled by the converter
        var extendedPropertyGetterFactories = new List<ICompilablePropertyGetterFactory>(
            _basePropertyGetterFactories
        );
        extendedPropertyGetterFactories.Add(
            new CompilableTypeConverterPropertyGetterFactory<TSourceNew, TDestNew>(
                _nameMatcher,
                converterNew
            )
        );
        extendedPropertyGetterFactories.Add(
            new ListCompilablePropertyGetterFactory<TSourceNew, TDestNew>(
                _nameMatcher,
                converterNew
            )
        );

        // Return a new ExtendableCompilableTypeConverterFactory that can make use of these
        // new property getter factories
        return new ExtendableCompilableTypeConverterFactory(
            _nameMatcher,
            _converterPrioritiser,
            extendedPropertyGetterFactories
        );
    }
}

Ok.. except one. I've sprung the ListCompilablePropertyGetterFactory. The ListCompilablePropertyGetter is similar to the CompilableTypeConverterPropertyGetter but will deal with properties and constructor arguments which are IEnumerable<SourceType> and IEnumerable<DestType>, resp.

This means that the ExtendableCompilableTypeConverterFactory setup code above would have worked if the SourceType and DestType were

public class SourceEmployee
{
    public string Name { get; set; }
    public SourceRole[] Role { get; set; }
}

public class DestEmployee
{
    public DestEmployee(string name, IEnumerable<DestRole> role)
    {
        Name = name;
        Role = role;
    }
    public string Name { get; private set; }
    public DestRole Role { get; private set; }
}

as the CreateMap would return a Converter Factory that could map SourceRole to DestRole and IEnumerable<SourceRole> to IEnumerable<DestRole>.

CreateMap vs AddNewConverter

The CreateMap method will try to generate a new Converter and build new Property Getter Factories using that by passing it to AddNewConverter. If you need to add any custom mapping mechanisms then AddNewConverter may be called with an ICompilableTypeConverter.

For example, if our types now looked like

public class SourceEmployee
{
    public int Id { get; set; }
    public string Name { get; set; }
    public SourceRole[] Role { get; set; }
}

public class DestEmployee
{
    public DestEmployee(string id, string name, IEnumerable<DestRole> role)
    {
        Id = id;
        Name = name;
        Role = role;
    }
    public string Id { get; private set; }
    public string Name { get; private set; }
    public DestRole Role { get; private set; }
}

then we would need a way to translate int to string when the name matcher identifies the potential "Id" to "id" mapping. We could do that with AddNewConverter and a custom ICompilableTypeConverter implementation -

var nameMatcher = new CaseInsensitiveSkipUnderscoreNameMatcher();
var converterFactory = new ExtendableCompilableTypeConverterFactory(
    nameMatcher,
    new ArgsLengthTypeConverterPrioritiserFactory(),
    new ICompilablePropertyGetterFactory[]
    {
        new CompilableAssignableTypesPropertyGetterFactory(nameMatcher),
        new CompilableEnumConversionPropertyGetterFactory(nameMatcher)
    }
);
converterFactory = converterFactory.CreateMap<SourceRole, DestRole>();
converterFactory = converterFactory.AddNewConverter<int, string>(
    new CompilableIntToStringTypeConverter()
);
var converter = converterFactory.Get<SourceEmployee, DestEmployee>();

public class CompilableIntToStringTypeConverter : ICompilableTypeConverter<int, string>
{
    public string Convert(int src)
    {
        return src.ToString();
    }

    public Expression GetTypeConverterExpression(Expression param)
    {
        if (param == null)
            throw new ArgumentNullException("param");
        return Expression.Call(
            param,
            typeof(int).GetMethod("ToString", new Type[0])
        );
    }
}

See, I promised last time that splitting ICompilableTypeConverter away from ICompilableTypeConverterByConstructor at some point! :)

Signing off

This has all turned into a bit of a saga! The final code for all this can be found at

https://github.com/ProductiveRage/AutoMapper-By-Constructor-1/

I've not done loads of performance testing but the generated Converters have consistently been around 1.1 or 1.2 times as slow as hand-rolled code (ie. approximately the same), not including the work required to generate the Converters. Compared to AutoMapper, this is quite a win (which was what originally inspired me to go on this journey). But out of the box it doesn't support all the many configurations that AutoMapper does! My main use case was to map legacy WebService objects (with parameter-less constructors) onto internal objects (with verbose constructors) which is all done. But there's currently no way to map back.. I think that's something to worry about another day! :)

Posted at 21:52