Sunday, July 31, 2011

LINQ To LDAP: GetValues Is Your Friend

I was finally able to put some time towards tackling a strange bug in LINQ to LDAP. Given this directory structure:
  • Company\Sites\DIV_A\Accounts\users\Users 1       -> 387 users
  • Company\Sites\DIV_A\Accounts\users\Users 2       -> 303 users
  • Company\Sites\DIV_A\Accounts\users\...                -> 21 users
  • Company\Sites\DIV_A\Accounts\users           Total -> 711 users

This query would perform horribly (as long as 20 seconds) when querying for all entries:
string namingContext = "OU=Users,OU=Accounts,OU=DIV_A,OU=Sites,DC=company,DC=local";
var example = new
{
 DistinguishedName = "",
 Name = "",
 Cn = "",
 ObjectGuid = default(Guid),
 ObjectSid = default(SecurityIdentifier)
};

var query = context.Query(example, SearchScope.Subtree, namingContext, objectClass: "Person")
  .Select(u => u);

var users = query.PageAll(10000);

If I remove ObjectGuid and ObjectSid then performance goes back to normal. This led me to the code that accesses the attributes. I was using the default index accessor when I should have been using GetValues for multi-valued attributes:
SearchResultAttributeCollection collection = //code to get collection from entry
//bad for multi-valued attributes
object guid = collection["objectguid"][0];

//much better
object guid = collection["objectguid"].GetValues(typeof(byte[]))[0];

So what gives? Why does GetValues perform so much better? It's recommended in the Introduction to System.DirectoryServices.Protocols, but other than that the Internet is pretty quiet on the subject. I fired up reflector and it looks like it tries to parse the bytes to a string, which fails so it catches the exception and returns the bytes as is. The performance issue is being caused by an exception being thrown for every byte array that tries to be retrieved...ouch.

The way mapping is done in LINQ to LDAP makes it easy to fix this, but dynamic queries are a little trickier. The duct tape fix is to hard code a list of popular guid / sid names and check for those:
public class SearchResultAttributeDictionary : Dictionary<string, object="">
{
    private const string ObjectGuid = "objectguid";
    private const string ObjectSid = "objectsid";

    private static readonly Dictionary<string, string> ByteProperties =
        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
            {
                {"objectguid", "objectguid"},
                {"objectsid", "objectsid"},
                {"Ibm-entryuuid", "Ibm-entryuuid"},
                {"guID", "guID"},
                {"Orclguid", "Orclguid"},
                {"Nsuniqueid", "Nsuniqueid"}
            };

    public SearchResultAttributeDictionary(SearchResultAttributeCollection collection)
        : base(StringComparer.OrdinalIgnoreCase)
    {
        if (collection == null) return;
        foreach (var attribute in collection.AttributeNames.Cast<string>())
        {
            var value = collection[attribute];
            if (value.Count == 1)
            {
                Add(attribute,
                    ByteProperties.ContainsKey(attribute) 
                        ? value.GetValues(typeof (byte[]))[0] 
                        : value[0]);
            }
            else if (value.Count > 1)
            {
                var type = value[0].GetType();
                Add(attribute, value.GetValues(type));
            }
            else
            {
                Add(attribute, null);
            }
        }
    }
}

This is the best solution I could come up with for now. If someone has a better one I'm open to suggestions.

No comments:

Post a Comment