20110509

better type conversion between similar objects

Friday I posted what is about the simplest possible way to automagicly convert between two objects that have mostly the same fields and properties but have no implicit conversion or common interface or base type.  It uses reflection and a little Linq, but it uses reflection every single time so it does not perform very well under load.  You can see that post here.

I wrote that as a quick hack to solve a specific, temporary problem a few weeks ago and I knew it wasn't going to be very efficient, but it didn't matter in that instance.  The best solutions in this sort of problem are MSIL assuming you cannot just change the classes to share dependencies, but MSIL is not something very many folks can read, and even fewer can write.

In .Net 4, you get some interesting additions/better documentation to Linq show you how to build expressions on the fly, so I wanted to take a stab at re-writing the converter that way.

The general idea is the same, use reflection to look for fields and writable properties then generate some action to sync them up, but there are several differences:
  • reflection is only used the first time a pair of types is converted
  • the actions are generated as Linq that is then compiled before storing in the converters set
  • added the ability to set a property to a field or field to property
  • added type checking
This is not going to perform as well as MSIL, but for a team with little MSIL experience it is going to be far more supportable and it seems to perform reasonably well.



using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace typeoverridedemostrater
{
    static class SuperConverter<T1, T2> where T2 : new()
    {

        private static Dictionary<Tuple<System.Type, System.Type>, Func<T1, T2>> converters = new Dictionary<Tuple<System.Type, System.Type>, Func<T1, T2>>();

        static public T2 Convert(T1 i)
        {
            var key = new Tuple<System.Type, System.Type>(typeof(T2), typeof(T1));
            if (!converters.ContainsKey(key))
            {

                ParameterExpression value = Expression.Parameter(typeof(T1), "value");
                ParameterExpression result = Expression.Parameter(typeof(T2), "result");
                var exprs = new List<Expression>();
                exprs.Add(Expression.Assign(result, Expression.New(typeof(T2))));

                exprs.AddRange((
                        from z in typeof(T2).GetProperties().Where(a => a.CanWrite)
                            .Select(b => new { name = b.Name, type = b.PropertyType }).Union(
                            typeof(T2).GetFields().Select(c => new { name = c.Name, type = c.FieldType }))
                        join y in typeof(T1).GetProperties().Where(a => a.CanWrite)
                            .Select(b => new { name = b.Name, type = b.PropertyType }).Union(
                            typeof(T1).GetFields().Select(c => new { name = c.Name, type = c.FieldType }))
                        on z equals y
                        select
                            Expression.Assign(Expression.PropertyOrField(result, z.name), Expression.PropertyOrField(value, z.name))
                        ).ToArray()
                    );
                exprs.Add(result);
                BlockExpression block = Expression.Block(variables: new[] { result }, expressions: exprs.ToArray());
                converters.Add(key, Expression.Lambda<Func<T1, T2>>(block, value).Compile());
            }
            return converters[key].Invoke(i);
        }
    }
}


the general flow is:
  1. if you don't already have a good converter
  2. create an input value and output result
  3. create an action to assign a new instance to the result variable
  4. add an assign action for every param/field that has a match between the types
  5. add an action for returning the result
  6. turn those into an expression block
  7. turn the block Lambda, then compiled to a TDelegate and store in the converters dictonary
  8. run the TDelegate from the dictionary


For 100,000 conversions the before/after is:

TotalMilliseconds = 1609.4059 vs TotalMilliseconds = 93.7518

No comments:

Post a Comment

Firefox Feedly RSS option

If you use Firefox with a RSS button and want the default RSS page to offer a Feedly option here is what you need to do: go to the about:c...