System.DirectoryServices Search Performance – Part 3

NOTE: This post is part of a series.

Advanced Binding Options

We’re going to take a little detour from search performance to talk about high performance binding to the directory.  The first thing a search does is bind to the directory.  And presumably you’ll bind to the objects that your search returns.  So even the most efficient search might not mater if your binding the slowest way possible.  We’ll also talk about how to perform the fastest search possible . . . . not searching.

Connect with FastBind

System.DirectoryServices is a .Net wrapper around ADSI.  When ADSI binds to an object it determines what kind of object it is (eg User, Computer, Group).  This is necessary to map it to a strongly typed ADSI type (eg IADsUser, IADsGroup).  However the DirectoryEntry object does not use any of the functionality of the strongly typed ADSI classes.  The only exception to this is when using the Invoke methods (primarily used to interact with passwords).

Using FastBind in our AuthenticationType prevents ADSI from determining the object type on creation of the DirectoryEntry object.  This means that we can prevent one round trip to the server by using FastBind.  The downsides to using FastBind are:

  • Does not check for existence of object.  This can complicate error handling.
  • Invoke method is unavailable.

FastBind can be combined with any other AuthenticationType  Here we are combining it with AuthenticationType.Secure.

DirectoryEntry de = new DirectoryEntry();
de.AuthenticationType =
   AuthenticationTypes.Secure | AuthenticationTypes.FastBind;

Find Users with Global Catalog Servers

In a multi-domain forest we need a way to find users across the different domains without looking in each individual domain.  There is where the global catalog server comes in.  Global catalog servers contain a read-only subset of information about every object in the forest.  To find a user in the GC and then connect to it’s R/W object in LDAP you need to do the following:

//Connect to Root of GC
DirectoryEntry gc = new DirectoryEntry("GC:");
foreach (DirectoryEntry root in gc.Children)
{
   //we know there's only one child of GC:
   gc = root; break;
}
//search GC for user
DirectorySearcher searcher = new DirectorySearcher();
searcher.SearchRoot = gc;
searcher.Filter =
   "(&(objectClass=user)(sAMAccountName=scevans))";
SearchResult result = searcher.FindOne();
//retrieve DN of user object
string path =
   result.Properties["distinguishedName"][0].ToString();
//connect to user object via LDAP instead of GC
DirectoryEntry user =
   new DirectoryEntry("LDAP://" + path);
Console.WriteLine(user.Properties["sAMAccountName"].Value);

Cache LDAP Connections

The first time you create a DirectoryEntry under the covers a LDAP connection is created.  Future DirectoryEntries will use that some LDAP connection as long as the following criteria are true:

  • The same server and port are used
  • The same credentials are used
  • The same AuthenticationFlags are used (with the exception of the FastBind and ServerBind flags)
  • That a previous DirectoryEntry (original or subsequent) that has been opened is still open.

So for example the following code will only create a single LDAP connection:

DirectoryEntry rootDSE = new DirectoryEntry(LDAP://rootDSE);
//force connection to LDAP
Object temp = rootDSE.NativeObject;
DirectoryEntry user = new DirectoryEntry
   (LDAP://CN=jdoe,OU=Staff,DC=domain,DC=com);
//read attribute from LDAP
//Same LDAP connection used
string username =
   user.Properties["sAMAccountName"].Value.ToString();

However  if the RootDSE object had been disposed before the user object had been created the LDAP connection would have been closed between the two objects.

Creating a connection to LDAP and authenticating is a significant overhead in the process.  The fewer times we create LDAP connections the better our performance will be.  Also repeated LDAP connection creations can cause the servers TCP stack to run out of wildcard ports.

GUID Binding

The fastest way to search for an object is to not have to search for an object.  Active Directory supports what is called GUID binding. You can retrieve the GUID from a DirectoryEntry object, store it somewhere (SQL, file, memory) and use that GUID to rebind to that.  The GUID never changes, so even if the object is moved across domains you will still be able to bind to the object.

//retrieve user via search
DirectorySearcher searcher = new DirectorySearcher();
searcher.Filter =
   "(&(objectClass=user)(sAMAccountName=jdoe))";
SearchResult result = searcher.FindOne();
DirectoryEntry user = result.GetDirectoryEntry();
//get users GUID and dispose object
Guid guid = user.Guid;
user.Dispose();
//bind to user via GUID
user =
   new DirectoryEntry(string.Format("LDAP://<GUID={0}>", guid));
Console.WriteLine(user.Properties["sAMAccountName"].Value);

System.DirectoryServices.Protocols

.Net 2.0 includes the System.DirectoryServices.Protocols namespace.  The major difference between S.DS and S.DS.P is that Protocols does not use ADSI.  This means you have more control over your connections, you get better performance, etc.  However it’s a lot harder to write code in the Protocols namespace.  If performance is the most essential thing though you may want to look at the Protocols namespace.

Advertisements