Saturday, May 14, 2011

LINQ To LDAP: Dynamics and More

My last two posts covered how you can use dynamics to query an LDAP server. I'd like you to forget those posts...well mostly forget them. The more I worked with Expando, the more rigid it felt for my use case. Basically I need a dynamic wrapper around a SearchResultAttributeCollection, similar to the ViewBag in .Net MVC. I created my own DynamicObject to support this:
public sealed class DynamicSearchResultAttributeDictionary : DynamicObject, IDictionary<string, object>
{
    private readonly SearchResultAttributeCollection _collection;
    private SearchResultAttributeDictionary _dictionary;

    public DynamicSearchResultAttributeDictionary(SearchResultAttributeCollection collection)
    {
        _collection = collection;
    }

    private SearchResultAttributeDictionary Dictionary
    {
        get 
        { 
             return _dictionary ?? 
                   (_dictionary = new SearchResultAttributeDictionary(_collection)); 
        }
    }

    public override IEnumerable<string> GetDynamicMemberNames()
    {
        return Keys;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        Dictionary.TryGetValue(binder.Name, out result);

        return true;
    }

    //IDictionary<string, object> implementation down here
}

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

With this implementation I can provide dynamic members that are NOT case sensitive and will NOT throw an exception if they do not exist. I also implemented IDictionary<string, object> so you can still access it that way.

So here's the slightly newer hotness way to query:
IQueryable<dynamic> query = context
        .Query("CN=Users,CN=Employees,DC=Northwind,DC=local")
        .Select("cn", "givenname")
        .FilterWith("(cn=Andrew Fuller)");

//Dynamic
dynamic user = query.FirstOrDefault();

Console.Write(user.cn)
Console.Write(user.GIVENNAME)

Console.Write(user["cn"])
Console.Write(user["GIVENNAME"])

//Typed Dictionary
IDictionary<string, object> dictionary = query.FirstOrDefault();

Console.Write(dictionary["cn"])
Console.Write(dictionary["GIVENNAME"])


All in all, this was a pretty fun and relatively easy feature (no one asked for it, but it felt natural given the key-value nature of LDAP).

Now that this unexpected detour is out of the way, here's what's shaping up for version 1.5:
  • New property maping support for date times with any format in the directory (file time or some other).
  • Enum mapping (string or int)
  • GetByDN and GetByCN methods to go to a specific distinguished name in the directory and load the attributes (faster if you know exactly what you're looking for since a Searchscope.Base is used).
  • Dynamics of course!

I'll try to have this out next week but no promises. This project comes 100% from my spare time and I'm short on that lately.