Once we have chosen the search root, scope, filter, attribute list, and number of results to return, we are actually ready to execute the query and examine the results. The FindOne and FindAll methods do this.
Finding a Single Object with FindOne
FindOne is a convenient shortcut when we want only one result back and do not want to write the extra code to enumerate the SearchResultCollection. If no object matches the query, FindOne simply returns null.
FindOne internally calls FindAll under the hood to perform the actual query and returns the first result found. As such, it is never faster than FindAll and we can never guarantee that only one object matched the query. FindOne is best used in conjunction with a filter designed to match only a single object in the query scope. Listing 4.11 demonstrates a typical use of FindOne.
Listing 4.11. A Typical Invocation of FindOne
DirectoryEntry root = null; DirectorySearcher ds = null; SearchResult result = null; using (root = new DirectoryEntry( "LDAP://CN=Users,DC=domain,DC=com", null, null, AuthenticationTypes.Secure )) { using (ds = new DirectorySearcher(root)) { ds.Filter = "(sAMAccountName=jkaplan)"; ds.SizeLimit = 1; result = ds.FindOne(); } } if (result != null) Console.WriteLine(result.Path); |
Listing 4.11 demonstrates how we might use FindOne to locate a user whose log-on name is jkaplan by doing a subtree search starting in our domain's Users container using the current thread's Windows credentials to access the directory. Here, the filter uses the Active Directory sAMAccountName attribute, which happens to be unique within a given domain, so we have some certainty that using the = filter type will match at most only one object in the scope of a single domain. We have taken the defaults for most of the properties, such as SearchScope and PropertiesToLoad, but we can obviously change those as needed.
We have also applied the C# using statement to wrap both of the objects that implement the IDisposable interface. We discussed this in Chapter 3 several times, including in the Close or Dispose? sidebar. It is important to apply this technique to all IDisposable objects to ensure timely resource cleanup, especially in long-running server processes like web applications, even if the code is slightly more verbose.
Getting Multiple Results with FindAll
FindAll allows us to return multiple results that match our query. Its usage is similar to FindOne, except that FindAll returns a SearchResultCollection that we must enumerate to retrieve the individual results. Let's modify Listing 4.11 to find the first 100 objects in the same container that have an email address, and then dump the addresses to the console (see Listing 4.12).
Listing 4.12. Finding Objects with Email Addresses Using FindAll
DirectoryEntry root = null; DirectorySearcher ds = null; SearchResultCollection results = null; using (root = new DirectoryEntry( "LDAP://CN=Users,DC=domain,DC=com", null, null, AuthenticationTypes.Secure )) { using (ds = new DirectorySearcher(root)) { ds.Filter = "(mail=*)"; ds.SizeLimit = 100; ds.PropertiesToLoad.Add("mail"); using (results = ds.FindAll()) { foreach(SearchResult result in results) { Console.WriteLine(result.Properties["mail"][0]); } } } } |
In Listing 4.12, we have added the mail attribute to the list of attributes to return. This causes the search to return this single attribute instead of returning all of the object's nonconstructed attribute values. A tiny change like this can have enormous performance impacts on a search, especially if the objects matched by the query contain large amounts of data.
We have once again wrapped the use of the SearchResultCollection in a using statement, as it too implements the IDisposable interface. Have we harped on this enough?
Enumerating the Results
We tend to use the foreach statement to enumerate the results. Unlike some other collection classes in the .NET Framework, SearchResultCollection internally implements its own private IEnumerator object that pulls the results from the directory. Any access to the SearchResultCollection properties, such as inspecting the Count property, will cause the entire result set to be retrieved from the directory and then enumerated. This can have a large impact, especially when returning many results, for a couple of reasons. First, using a foreach loop would allow us to break off the search if necessary, without enumerating all the results completely. This is more efficient, as we break off the search early and do not force the server to return all of the results first. Second, we can process each result as it comes from the server using a foreach loop without waiting for the entire search to complete. Using a for loop in conjunction with the Count property prevents both of these scenarios. As such, we generally recommend using the foreach loop when enumerating our results.
Since we knew that each result returned in Listing 4.12 would have a mail attribute (our filter dictated that), we can safely dig into the collection results without fear of getting a NullReferenceException or Index-OutOfRangeException. If we were not sure whether the SearchResult actually contained a particular attribute value, we would want to check first, using the Contains method.
Returning Many Results with Paged Searches |
Part I: Fundamentals
Introduction to LDAP and Active Directory
Introduction to .NET Directory Services Programming
Binding and CRUD Operations with DirectoryEntry
Searching with the DirectorySearcher
Advanced LDAP Searches
Reading and Writing LDAP Attributes
Active Directory and ADAM Schema
Security in Directory Services Programming
Introduction to the ActiveDirectory Namespace
Part II: Practical Applications
User Management
Group Management
Authentication
Part III: Appendixes
Appendix A. Three Approaches to COM Interop with ADSI
Appendix B. LDAP Tools for Programmers
Appendix C. Troubleshooting and Help
Index