Tuesday, May 24, 2011

What's Next?

For anyone who has downloaded the trunk for LINQ to LDAP, you may have noticed a project called DirectoryViewer. This is a WPF project I'm building for my own skill and knowledge improvement. Right now it points to my local LDS server, but I've been playing around with pointing it to Penn State's public server. I'll have a series of blog posts on that development.

I'll also be working on LINQ to LDAP 2.0 to support Add, Update, Delete, and Move operations. I don't have a time frame for a release but keep an eye on the trunk if you want to try anything out. I have zero experience with making updates to directories (read only for me) so suggestions on the API would be much appreciated.

Right now I plan to support Add and Update for mapped objects as well as just key-values. Delete will support passing in a mapped object or simply a DN. Move will take an old DN and a new DN. That's about it for now. I'll try to post examples as I develop for maximum visibility and feedback. Stay tuned!

Sunday, May 22, 2011

LINQ To LDAP: Version 1.5

New version over at codeplex. I also added a C# and VB example to the front page. It took me about 30 minutes just to figure out how to write the VB example, haha.

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.

Sunday, May 1, 2011

LINQ To LDAP: Dynamics! Part 2

In my previous post I talked about adding dynamics support. Now I want to cover how I did it. The ExpandoObject added in .Net 4.0 is the type that I return for all dynamic queries. The cool thing about this object is you can cast it to IDictionary<string, object> and do something like this:
dynamic user = context.Query("CN=Users,CN=Employees,DC=Northwind,DC=local")
    .Select("cn", "givenname", "sn", "telephonenumber")
    .FilterWith("cn=Andrew Fuller")
    .FirstOrDefault();

foreach (var attribute in (IDictionary<string, object>)user)
{
    Console.WriteLine(attribute.Key + ": " + attribute.Value);
}

This is especially nice if you prefer to work with static types rather than dynamic and for characters that are invalid for .Net property names but valid for attribute names (such as hyphens).

So other than that, here are a few things you should know about dynamic querying:
  • Dynamic mappings are never cached.
  • Selected attributes will always be present in the result with a value of null, even if the attribute does not exist in the directory.
  • All LINQ extension methods that are currently supported will work with dynamic querying as long as they don't require an expression (i.e. Where conditions)

Well that's about it. I'll end with the result transformer for dynamic objects:
internal class DynamicResultTransformer : IResultTransformer
{
    private readonly IEnumerable<string> _attributesToLoad;

    public DynamicResultTransformer(IEnumerable<string> attributesToLoad)
    {
        _attributesToLoad = attributesToLoad;
    }

    public object Transform(SearchResultEntry entry)
    {
        dynamic expando = new ExpandoObject();
        var attributes = expando as IDictionary<string, object>;

        var names = (_attributesToLoad == null || !_attributesToLoad.Any())
                        ? entry.Attributes.AttributeNames.Cast<string>()
                        : _attributesToLoad;
        foreach (var attribute in names)
        {
            var value = entry.Attributes[attribute];
            if (value == null || value.Count < 1)
            {
                attributes.Add(attribute, null);
            }
            else if (value.Count == 1)
            {
                attributes.Add(attribute, value[0]);
            }
            else if (value.Count > 1)
            {
                var type = value[0].GetType();
                attributes.Add(attribute, value.GetValues(type));
            }
        }

        return expando;
    }

    public object Default()
    {
        return default(ExpandoObject);
    }
}