Wednesday, August 24, 2011

LINQ To LDAP: Bye Bye Dynamics

Well, it was a fun ride while it lasted. Given the performance penalty for blindly accessing byte arrays and the guessing game that I have to handle to avoid it, it only makes sense to peel away the abstraction and give control back to the user.

So in place of dynamics I created IDirectoryAttributes and DirectoryAttributes. If you've ever worked with IDataReader, it will feel very familiar. Here are the get methods for the interface:
string DistinguishedName { get; }

object GetValue(string attribute);

DirectoryAttribute Get(string attribute);

byte[] GetBytes(string attribute);

string[] GetStrings(string attribute);

string GetString(string attribute);

byte? GetByte(string attribute);

int? GetInt(string attribute);

long? GetLong(string attribute);

double? GetDouble(string attribute);

decimal? GetDecimal(string attribute);

short? GetShort(string attribute);

float? GetFloat(string attribute);

bool? GetBoolean(string attribute);

DateTime? GetDateTime(string attribute, string format = "yyyyMMddHHmmss.0Z");

Guid? GetGuid(string attribute);

SecurityIdentifier GetSecurityIdentifier(string attribute);

IEnumerable<byte[]> GetByteArrays(string attribute);

Pretty standard. Attribute is case insensitive. If it's not found then null will be returned. Since everything is either a string or byte array from the directory, if it fails to convert then a FormatException will be thrown. GetValue will try to guess the type like before and is only here to support the implementation of IEnumerable<string, object>. Of course if there's a datatype here that's missing, just use Get and you'll get direct access to the DirectoryAttribute.

So what about change tracking? Since DirectoryAttributes is just a wrapper for SearchResultEntry, setting values is tracked in an internal dictionary. Just call SetValue(attributeName, value) and it will take care of the rest.
var factory = new LdapConnectionFactory("localhost");
string namingContext = "CN=Users,CN=Employees,DC=Northwind,DC=local";

using (IDirectoryContext context = new DirectoryContext(factory.GetConnection(), true))
{
    var query = context.Query(namingContext, objectClass: "Person")
        .Where("cn=Andrew Fuller")
        .Select("cn");

    var user = query.FirstOrDefault();

    user.SetValue("employeeid", 1);

    var updated = context.Update(user);

    Console.WriteLine(updated.GetInt("employeeid"));
    Console.WriteLine(updated.GetGuid("objectguid"));
}

That's it. I hope this helps with performance and satisfies anyone who can't / won't map classes.