Sunday, December 5, 2010

LINQ To LDAP: string to int

Currently I'm working on the transform portion of Linq to LDAP. This is where you take what comes back from a directory search (SearchResponse with a collection of SearchResultEntry) and transform that into mapped objects. I currently have a class that allows me to cache property getters and setters as Action and Func delegates.

Why even go through the trouble of building custom logic for setters and getters? Well I learned reflection is pretty optimized up to about 1000 iterations. After that point it's performance is downhill from there. Also, it's normal for LDAP entries to have 50 to 100 attributes and calling set or get on all of the properties can get very slow.

I created a performance test that for 1000 iterations sets 30 string properties via reflection, standard setters, and my custom delegates.

Here are the numbers:
Raw Set: 7ms
Action Delegate: 17ms
Reflection: 46 ms

Not a huge difference in the scheme of things, but if a large result set comes back, I don't want to worry about transforming the objects being a bottleneck.

So now that that's out of the way, let's get to my real post. My original DelegateBuilder implementation looked like this:

public static Action<T, object> BuildSetter<T>(PropertyInfo propertyInfo)
{
    var instance = Expression.Parameter(typeof(T), "i");
    var argument = Expression.Parameter(typeof(object), "a");
    var setterCall = Expression.Call(
        instance,
        propertyInfo.GetSetMethod(),
        Expression.Convert(argument, propertyInfo.PropertyType));
    var setter = (Action<T, object>)Expression.Lambda(setterCall, instance, argument)
                                                    .Compile();
    return setter;
}

public static Func<T, object> BuildGetter<T>(PropertyInfo propertyInfo)
{
    var instance = Expression.Parameter(typeof(T), "i");
    var getterCall = Expression.Call(
        instance,
        propertyInfo.GetGetMethod());
    var conversion = Expression.Convert(getterCall, typeof(object));
    var getter = (Func<T, object>)Expression.Lambda(conversion, instance).Compile();
    return getter;
}

This was all fine and dandy until I wrote a unit test for my transform that looks like this:
[Test]
public void Transform_NonAnonymousTypeWithAllAttributesPresentInEntry_ReturnsMappedObjectWithAllPropertiesSet()
{
    //prepare
    var searchResultAttributesCollection =
        typeof (SearchResultAttributeCollection)
             .Create<SearchResultAttributeCollection>();
    searchResultAttributesCollection.CallMethod("Add",
                                                new object[]
                                                    {"Property1", new DirectoryAttribute("Property1", "prop1")});
    searchResultAttributesCollection.CallMethod("Add",
                                                new object[] { "Property2", new DirectoryAttribute("Property2", "2") });
    var searchResultsEntry = typeof(SearchResultEntry).Create<SearchResultEntry>(new object[]{"dn", searchResultAttributesCollection});
    var queriedProperties = new Dictionary<string, string>
                                {
                                    {"Property1", "Property1"},
                                    {"Property2", "Property2"}
                                };
            

    //act
    var instance = searchResultsEntry.Transform(queriedProperties, _mapping).Cast<SearchResultEntryExtensionsIntegrationTests>();

    //assert
    instance.Property1.Should().Be.EqualTo("prop1");
    instance.Property2.Should().Be.EqualTo(2);
}

See the second CallMethod that Adds "2" to the searchResultAttributesCollection? That has to be a string since Protocols will throw an exception if a value other than a string, uri, or byte[] is passed in. Well that breaks my BuildSetter since you can't do an explicit cast from a string to an int. You have to use int.Parse("2") to get the int value from the string. So how did I solve this? I'm not a fan of huge if blocks so I'll try to refactor this another time, but for now it works.

private static readonly Func<object, Type, object> ConvertStringToValueTypeIfNecessaryFunction =
    (o, t) =>
        {
            if (o is string && t.IsValueType)
            {
                if (t == typeof(short) || t == typeof(short?))
                {
                    return short.Parse(o as string);
                }
                if (t == typeof(int) || t == typeof(int?))
                {
                    return int.Parse(o as string);
                }
                if (t == typeof(long) || t == typeof(long?))
                {
                    return long.Parse(o as string);
                }
                if (t == typeof(double) || t == typeof(double?))
                {
                    return double.Parse(o as string);
                }
                if (t == typeof(float) || t == typeof(float?))
                {
                    return float.Parse(o as string);
                }
                if (t == typeof(decimal) || t == typeof(decimal?))
                {
                    return decimal.Parse(o as string);
                }
            }
            return o;
        };

public static Action<T, object> BuildSetter<T>(PropertyInfo propertyInfo)
{
    var instance = Expression.Parameter(typeof(T), "i");
    var argument = Expression.Parameter(typeof(object), "a");
    var type = Expression.Constant(propertyInfo.PropertyType, typeof(Type));

    var converterFunctionCall = Expression.Call(ConvertStringToValueTypeIfNecessaryFunction.Method, argument, type);
    var setterCall = Expression.Call(
        instance,
        propertyInfo.GetSetMethod(),
        Expression.Convert(converterFunctionCall, propertyInfo.PropertyType));
    var setter = (Action<T, object>)Expression.Lambda(setterCall, instance, argument)
                                                    .Compile();
    return setter;
}

No comments:

Post a Comment