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.

Wednesday, July 27, 2011

LINQ To LDAP: (C)RUD

Just like any other data store you can create, update, and delete data in a directory.

Here's how you create entries using S.DS.P:

LdapConnection connection = new LdapConnection("localhost");

string distinguishedName = "CN=John Doe,CN=Users,CN=Employees,DC=Northwind,DC=local";
AddRequest request = new AddRequest(distinguishedName, "User");

request.Attributes.Add(new DirectoryAttribute("givenname", "John"));
request.Attributes.Add(new DirectoryAttribute("sn", "Doe"));
request.Attributes.Add(new DirectoryAttribute("employeeid", "1"));

connection.SendRequest(request);

Looks pretty straightforward. You give your entry a primary key (distinguished name), an object class, and then populate the attributes for the new entry and submit it to the directory.

So here's how you do it with mapped classes:
public abstract class DirectoryObject
{
    [DistinguishedName]
    public string DistinguishedName { get; set; }

    [DirectoryAttribute(StoreGenerated = true)]
    public DateTime? WhenChanged { get; set; }

    [DirectoryAttribute("cn")]
    public string CommonName { get; set; }

    [DirectoryAttribute(StoreGenerated = true)]
    public DateTime? WhenCreated { get; set; }

    [DirectoryAttribute("objectguid", StoreGenerated = true)]
    public Guid Guid { get; set; }

    [DirectoryAttribute]
    public string Name { get; set; }
}

[DirectorySchema(NamingContext, ObjectCategory = "Person", ObjectClass = "user")]
public class User : DirectoryObject
{
    private const string NamingContext = "CN=Users 1,CN=TestContainer,CN=Employees,DC=Northwind,DC=local";

    [DirectoryAttribute("objectsid", StoreGenerated = true)]
    public SecurityIdentifier SID { get; set; }

    [DirectoryAttribute("givenname")]
    public string FirstName { get; set; }

    [DirectoryAttribute("sn")]
    public string LastName { get; set; }

    [DirectoryAttribute("directreports")]
    public string[] Employees { get; set; }

    [DirectoryAttribute]
    public string Title { get; set; }

    [DirectoryAttribute]
    public string PostalCode { get; set; }

    [DirectoryAttribute(ImageFormat = ImageType.Png)]
    public Bitmap Photo { get; set; }

    [DirectoryAttribute("l")]
    public string City { get; set; }

    [DirectoryAttribute("c")]
    public string Country { get; set; }

    [DirectoryAttribute]
    public string EmployeeID { get; set; }

    [DirectoryAttribute]
    public string TelephoneNumber { get; set; }

    [DirectoryAttribute("pwdlastset", DateTimeFormat = null, StoreGenerated = true)]
    public DateTime? PasswordLastSet { get; set; }

    [DirectoryAttribute]
    public string Street { get; set; }

    public void SetDistinguishedName()
    {
        DistinguishedName = "CN=" + CommonName + "," + NamingContext;
    }
}

There are a few items to note here. First is the StoreGenerated property of DirectoryAttribute. This allows the DirectoryContext to only update attributes that the store doesn't manage. The second is the DistinguishedName attribute which mainly helps when calling Add and Update. The third is the DirectoryObject which isn't a part of LINQ to LDAP, but is an example abstract base class that all directory objects can sub-type.
User user = new User
                {
                    City = "Some City",
                    CommonName = "John Doe",
                    Country = "US",
                    EmployeeID = "1",
                    FirstName = "John",
                    LastName = "Doe",
                    Name = "Doe, John",
                    Street = "1234 Street",
                    Title = "Unknown",
                    PostalCode = "12345",
                    TelephoneNumber = "123-456-7890"
                };

user.SetDistinguishedName();
var factory = new LdapConnectionFactory("localhost");
using (var context = new DirectoryContext(factory.GetConnection(), true))
{
    User added = context.Add(user);
}

I create a new user and initialize its properties. Whenever an object is added or updated, a fresh version is retrieved using GetByDN.

Alternatively, you can perform the same operation using a dictionary:
string dn = "CN=John Doe,CN=Users 1,CN=TestContainer,CN=Employees,DC=Northwind,DC=local";
var attributes = new Dictionary<string, object>
                        {
                            {"l", "Some City"},
                            {"cn", "John Doe"},
                            {"c", "US"},
                            {"EmployeeID", "1"},
                            {"givenname", "John"},
                            {"sn", "Doe"},
                            {"Name", "Doe, John"},
                            {"Street", "1234 Street"},
                            {"Title", "Unknown"},
                            {"PostalCode", "12345"},
                            {"TelephoneNumber", "123-456-7890"}
                        };

var factory = new LdapConnectionFactory("localhost");
using (var context = new DirectoryContext(factory.GetConnection(), true))
{
    IDictionary<string, object> added = context.Add(dn, "User", attributes);
}

Or by converting an anonymous object to a dictionary:
string dn = "CN=John Doe,CN=Users 1,CN=TestContainer,CN=Employees,DC=Northwind,DC=local";
var user = new
                {
                    City = "Some City",
                    CommonName = "John Doe",
                    Country = "US",
                    EmployeeID = "1",
                    FirstName = "John",
                    LastName = "Doe",
                    Name = "Doe, John",
                    Street = "1234 Street",
                    Title = "Unknown",
                    PostalCode = "12345",
                    TelephoneNumber = "123-456-7890"
                };

var factory = new LdapConnectionFactory("localhost");
using (var context = new DirectoryContext(factory.GetConnection(), true))
{
    IDictionary<string, object> added = context.Add(dn, "User", user.ToDictionary());
}

ToDictionary is just an extension method that reflects over an attribute and creates a dictionary from its properties.

So I think that covers how to add new entries. Questions, thoughts, improvements?

Sunday, July 10, 2011

Bitmap Mapping

Between a busy job, house hunting and a much needed vacation I've fallen out of the habit of blogging. My first post back I want to talk about an undocumented feature for LINQ to LDAP and why I'm thinking of removing it.

Currently LINQ to LDAP supports mapping images stored in a directory as Bitmaps. I thought this was cool since all you had to do was map it with an image type and it just worked (updates as well in the new 2.0 changes). However, I've left it undocumented since Bitmaps use GDI+ and if you're not careful you can introduce a memory leak. Imagine blindly querying for all users in your directory and suddenly there's 10,000 (assuming you're in a large organization or university) bitmaps being loaded up.

So if this feature gets removed here is how you could still support images:

[DirectorySchema("OU=Users,DC=mycompany,DC=com", ObjectClass = "User")]
public class User : IDisposable
{
     [DirectoryAttribute("Photo")]
     public byte[] PhotoBytes { get; set; }
     
     private Bitmap _photo;
     public Bitmap Photo
     {
          get
          {
               if (PhotoBytes == null) return null;
               
               if (_photo == null)
               {
                    _photo = new Bitmap(new MemoryStream(PhotoBytes));
               }
               return _photo;
          }
          set
          {
               if (_photo != null) _photo.Dispose();
               if (value == null)
               {
                    _photo = null;
                    PhotoBytes = null;
               }
               else
               {
                    _photo = value;
                    using (var stream = new MemoryStream())
                    {
                         _photo.Save(stream, ImageFormat.Png); //whatever format
                         PhotoBytes = stream.ToArray();
                    }
               }
          }
     }

     public void Dispose()
     {
          if (_photo != null) 
          {
               _photo.Dispose();
               _photo = null;
          }
     }
}

So is this even a big deal? Will anyone care if I removed bitmap mapping?