While LDAP allows us to do many things, searching is really the primary activity of LDAP programming. As such, it makes sense that we spend two whole chapters talking about it. Before we can execute a search, we need to answer some important questions.
If we take each of these questions in turn, we can start to formulate a simple plan for how to design and perform a search in the most efficient manner. If all of this is starting to look too complicated, don't worry, it will become second nature as your experience using SDS grows.
Deciding Where to Search
Our search starts at the location in the directory defined by the SearchRoot property. SearchRoot contains a DirectoryEntry that represents both the binding context that will be used for connecting to the directory as well as the starting location for the search.
For example, let's say we want to do a search starting at the normal Users container of our Active Directory domain, and the distinguished name (DN) of the domain naming context is as follows:
We would first create a DirectoryEntry object pointing to the Users container and pass that to the SearchRoot of the DirectorySearcher, as Listing 4.1 demonstrates.
Listing 4.1. Initializing the SearchRoot of the DirectorySearcher
DirectoryEntry users = new DirectoryEntry( "LDAP://CN=Users,DC=domain,DC=com", null, null, AuthenticationTypes.Secure ); DirectorySearcher ds1 = new DirectorySearcher(users); //this is equivalent DirectorySearcher ds2 = new DirectorySearcher(); ds2.SearchRoot = users;
The DirectorySearcher will search in some part of the directory tree, starting at the CN=Users container.
Note: Use the Default SearchRoot with Caution
The default value for SearchRoot is null. This has several implications of which we should be aware. First, the LDAP provider will be assumed. Next, the RootDSE of the current domain will be contacted and the default naming context will be queried to determine the starting search location (which means we are searching the entire domain). Finally, the security credentials of the current thread will be used to determine our permissions. Needless to say, these are a lot of assumptions to make, and the result might not be exactly what we wanted. For the sake of clarity, we recommend specifying a SearchRoot using either this property or one of the DirectorySearcher constructors. This makes it much easier to decipher our intent.
How Security Affects Searching
The binding context of the DirectoryEntry object set as our SearchRoot determines the security context for all of the related searching operations. This means that whatever credentials our SearchRoot uses will determine what our DirectorySearcher can and cannot return. Since not all accounts in the directory may have rights to see every object and attribute value, choosing different DirectoryEntry credentials for our SearchRoot may result in completely different search results.
This really comes into play when using nonprivileged accounts and searching for attributes that only more privileged accounts can normally view. In these scenarios, it will appear as though the attribute does not exist on the object. In some cases, we may not have permission to see the whole object or even list the contents of a specific container.
It is important to note that no error will occur when searching for an attribute or location for which the user does not have the proper read privileges. Instead, the returned result set will simply be empty of the prohibited objects or attributes and no indication will be given as to the reason.
One thing a developer can do to troubleshoot these types of issues is to use one of the many freely available LDAP browsers (see Appendix B for our LDAP tools list). By binding as a standard user in such a tool and browsing to an object, we will immediately see what is and is not available in our targeted context.
Controlling Depth of Search with SearchScope
The SearchScope property controls the depth of our search below the SearchRoot object in the directory hierarchy. SearchScope uses an enumeration (also called SearchScope) to define the three possible depths: Subtree, OneLevel, and Base. The default value is SearchScope.Subtree.
The Subtree option is typically the one that most developers will use, as it will search the current SearchRoot and all children below it, including any child containers. This is the largest of the scope settings and we typically use it when we do not know where objects are located, or when we want to search for similar objects across disparate containers.
The OneLevel option searches all immediate children of the current SearchRoot, excluding the SearchRoot itself. Unlike the Subtree scope, descendants below the immediate children will not be included. This option is best used when we know which container holds our target objects and we want to tighten our scope.
A Base-scoped search looks within the SearchRoot object itself. This scope is generally used for retrieving constructed attributes of a particular object from the directory. The search does not select any objects below the SearchRoot in the directory hierarchy. As such, we typically set the Filter property to (objectClass=*), which will match any object, as all objects by definition must have an objectClass.
We rarely use this option in SDS to retrieve constructed attributes, because the DirectoryEntry object mirrors this functionality with the RefreshCache method. Given that we need to create the DirectoryEntry anyway to populate the SearchRoot property, using RefreshCache often requires less code and is cleaner (see Chapter 6).
The question becomes, when should we use this scope? It turns out that one of our advanced searches, called an attribute scope query (see Chapter 5), requires this option. We might also use this scope to take advantage of the different data marshaling behaviors of DirectorySearcher compared to DirectoryEntry (see Chapter 6).
Figure 4.1 illustrates the three scope options. In the diagram, we see a root object with three immediate children. Two of the child objects have children as well.
Figure 4.1. Search Scope Options Illustrated
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
Part III: Appendixes
Appendix A. Three Approaches to COM Interop with ADSI
Appendix B. LDAP Tools for Programmers
Appendix C. Troubleshooting and Help