"How I Lost 1180 Seconds" or "Putting S.DS Code on a Diet" – Conclusion.

(Note: this is the conclusion of a 4-part post).

The last bit of optimization in my code, again, involved an understanding of data layering. The S.DS DirectoryEntry class is itself a layer on top of a layer on top of another layer. DirectoryEntry is a wrapper around Microsoft ADSI (Active Directory Services Interface). ADSI, in turn, is a wrapper around basic LDAP protocols. Each of these layers is providing services to another while trying to hide the details of its operation. In order to optimize code that relies on S.DS and, hence, on ADSI and LDAP, it is important, however, to understand what these layers do and to have some sense of how they do it.

ADSI is a Microsoft COM interface (available from C++ or script) that presents an object oriented interface to LDAP. Rather than having programmers worry about LDAP binding and searches, ADSI dispenses directory objects when provided with an LDAP path for the object. The objects, in turn, make available LDAP attributes via COM property accessors. The S.DS DirectoryEntry object is a .NET wrapper for an ADSI COM object.

What’s most important to know about DirectoryEntry and ADSI is that they try to delay binding to LDAP objects for as long as possible. When they do, they also provide mechanisms for you to specify the minimum set of attributes that you want to view. Let’s consider how this comes into play in the example that we’ve been working with.

When the code uses DirectorySearcher to search AD, S.DS and ADSI don’t actually bind to found objects. All they really do is to retrieve the LDAP distinguishedNames (paths) of the results. When we get the DirectoryEntry associated with a SearchResult, we have an “unbound” DirectoryEntry (and ADSI) object. Only when we actually retrieve a Properties value from the DirectoryEntry does S.DS actually bind to the necessary LDAP path.

How can we take advantage of this operational knowledge?

Let’s go back to the UI and consider what we actually need to show. I’ve been glossing over the details here – my list view example has only been showing the name of each retrieved object. In the real code, I show the name of the Group and its UNIX gidNumber. Since this is all I need, why not take advantage of this?

Here’s what we do: instead of having the data layer return a full blown GroupInfo object, let’s define a new class:

class GroupInfo0 { public string Name; public int gidNumber; private GUID guidObject; public GroupInfo0(string Name, int gidNumber, GUID guidObject) { this.Name = Name; this.gidNumber = gidNumber; this.guidObject = guidObject; } public GroupInfo GetGroupInfo() { // S.DS code to retrieve an object // via its GUID (details omitted) DirectoryEntry de = new DirectoryEntry(GetLDAPPath(uidObject)); return new GroupInfo(de); } }

What we have is a class that provides only the minimal data needed for our list view and then provides a function to retrieve the full GroupInfo class when we actually need it (for example, when the user chooses the group in the list view and decides to perform an operation on it). How can we make use of this class? Let’s return these objects from our directory search instead of full GroupInfo objects. The advantage of doing this is that we can restrict the attributes that S.DS (and ADSI) need to retrieve from LDAP:

DirectorySearch ds = new DirectorySearcher(...); ... set up ds ... ds.PropertiesToLoad.Add("gidNumber"): ds.PropertiesToLoad.Add("objectGuid"): ... SearchResultCollection src = ds.FindAll(); return new SearchResults(src);

By specifically setting the PropertiesToLoad property we inform S.DS that it doesn’t need to “load” all attributes when performing the search. We also need to change our SearchResults class, in order that it returns the simpler GroupInfo0 objects:

class SearchResults { private object[] ao; public SearchResults(SearchResultCollection src) { ao = new object[src.Count]; foreach(SearchResult sr in src) ao[i] = sr.GetDirectoryEntry(); } public GroupInfo0 this[int i] { get { if( ao[i] is DirectoryEntry) { DirectoryEntry de = ao[i] as DirectoryEntry; string sName = de.Name; int gidNumber = (int)de.Properties["gidNumber"].Value; Guid guidObject = de.Guid; GroupInfo0 gi0 = new GroupInfo0(sName, gidNumber, guidObject); ao[i] = gi0; return gi0; } else return ao[i] as GroupInfo0; } } }

My use of the “0” notation is not arbitrary. There are several examples of API in Windows that return different levels of data. The 0 level is minimal – 1, 2 and other levels provide increasing amounts of information.

Our UI code will now call the data layer to get a SearchResults object. When the virtual mode list view needs to show an item, the UI code will index into SearchResults and will get a GroupInfo0 object that contains enough information to populate the list view. When the user actually selects an item in the list view and needs to operate on, the UI code will call GroupInfo0.GetGroupInfo() to get a “real” GroupInfo object which it can manipulate.

This last set of changes, reducing the amount of work S.DS and LDAP had to perform, had a huge impact on performance. Collectively, all the techniques that I employed (plus another that I haven’t covered) reduced my original task (populating a list view) from 1200 seconds to 20 seconds. Pretty good, I think!

I could have gotten the same performance numbers by throwing away my careful layer separation. I could have had my data layer return a SearchResultCollection with the reduced attribute set and I could have had my UI code pluck the necessary attributes directly from the underlying DirectoryEntry objects. This would have defeated the main objective of n-tier design: isolating layers from changes to other layers. If I later needed an additional attribute, for example, I might change the UI code to retrieve the needed data from DirectoryEntry without realizing that the data layer had only provided a reduced set of attributes for performance reasons. By adhering to a strong discipline of layer separation, it is clear that, if the UI needs another attribute, it needs to be added to GroupInfo0. Since this class is defined in the data layer, it is much clearer where the changes would be necessary in order to provide the requested information.

I don’t guarantee that a similar set of analyses will speed up your own S.DS code. Sometimes, operations are fundamentally slow. It is always best to look for algorithmic improvements instead of clever coding techniques. Nevertheless, my advice is to use S.DS with care and to understand the ADSI and LDAP layers as well as you can.