.Net Directory Services Programming – C# - Part 3
Topics
DirectorySearcher – the other critical class in the DirectoryServices namespace.
Review
Because a lot of your Directory Services (DS) development will involve querying DS for data, it makes sense that this is a powerful class offered in the namespace, and below are some of the features:
- DirectorySearcher – Performs the initial queries against AD
- SearchResult – A single object reference from a search performed by DirectorySearcher
- ResultPropertyCollection – A collection of properties of the SearchResult instance
- ResultPropertyValueCollection – The values of properties in a SearchResult instance.
- SearchResultCollection – Basically a collection of SearchResult instances returned from a query by DirectorySearcher
- SortOption – Allows a means to sort a result set.
We have briefly touched on DS by introducing some basic DirectoryEntry information and a couple simple examples, and we have introduced some basic properties and examples on how to interact with those.
I have not taken a lot of time to review a directory structure – basically assuming that you should have some of this understanding already. As I delve into the next lesson, this assumption holds fast. If you have any issues or are dealing with something that I am not elaborating enough on, please feel free to email me directly at (mikeh AT thedataworks.net) and I will respond as quickly as I can. Please forgive the notation there – but we all are familiar with the latest in SPAM bots and whatnot.
Show me the Money!!!
Or at least the code. Lets get started…
Parent – Child Relationships
DirectoryEntry objects (DE’s) can be a root object in AD, or an object within another – hence, a child object of another. We reviewed in Part 1 a few of the common references to object types (CN, OU, DC). These are the most prevalent you will deal with.
DC is the root level context. OU’s can be off the root, or a child of other OU’s and even Containers. CN’s can be off the root, within OU’s and often are found within Containers.
A word about Containers and Security
When planning your DS application, keep this in mind: Containers are non-secure objects! Let me explain. AD security is multi-faceted, yes, but the real security is managed using Group Policy (something we are not going to review here).
Containers are basically folders in your directory tree. AD comes with a couple by default. If you promote a server and check your directory tree, you’ll find a Users, a Built-In, and a couple other folders off the root tree (or context/server name) object. These are not OU’s – they are containers.
Group Policy (GP) cannot be applied to a Container object in AD. You can apply GP to objects within Containers – depending on what those objects are – but not the Container itself. This is important because a lot of administration can be simplified by applying the GP to the entire Container. So if not the Container, then what? Organizational Units – OU’s. Alas, OU’s are going to be your preferred folder or container (for lack of a better way to put it) for object storage.
Basic Searches
In Part 1 we introduced a server name and basic OU’s. To re-cap that:
Server: developer.hamilton.com
OU=Accounts – off the root of developer.hamilton.com
OU=Developers – within the OU=Accounts
CN=Mike Hamilton – within the OU=Developers
Within the OU=Accounts I might also create service accounts – basic user accounts that are used to run services such as IIS Application Pools, SQL Server, or other server / service applications. I might also have OU’s that represent other departments that pertain to development: BA’s for the Business Analyst, QA’s, etc. You can nest OU’s and accounts in just about any way you want to. NOTE: In AD – if you have not already – you will run into one of the inherent limitations of the AD Users & Computers (dsa.msc) snap-in: It can only display the first 2,000 objects within a Container or OU. So if you anticipate a large volume of users for a system you are designing, keep this in mind. You will want to present 1) a more suitable customized user interface piece for administration of this DS, or 2) design your object hierarchy so that users are more spread out within the DS. I will touch base on this topic in a later Part and introduce a couple ideas for addressing this limitation.
DirectorySearcher and the SearchResult
When using the DirectorySearcher class, you have the FindOne() and FindAll() methods that build your result set. As they infer, one finds one occurrence, the other finds all occurrences.
Let’s say I had more than one Mike on my team, and I wanted to find all entries that began with Mike?
DirectoryEntry rootEntry = new DirectoryEntry(); // Binds root context
// Now find every CN that has Mike at the beginning of the name…
DirectorySearcher searcherResults = new DirectorySearcher(rootEntry,”(CN=Mike*)”);
This is a simple example, and the result set will be one or more object references that begin with Mike. Notice that I did a bind on the root of the DS. I could have narrowed the focus of the search to only the developer’s OU by doing the following:
DirectoryEntry developersEntry = new DirectoryEntry(“LDAP://developer.hamilton.com/OU=Accounts,OU=Developers,DC=developer,DC=hamilton,DC=com”);
This would return an entry object where the parent would be the OU=Developers, and not the default root of the DS tree.
Lets say my DS was structured like the following:
developer.hamilton.com à root of the DS
OU=Accounts à First OU of accounts of users.
OU=Developers à Key development personnel
OU=Bas à Business Analyst
OU=QA à Quality Assurance / Test personnel
OU=Users à General users on this server.
Now, we want to find all users that are in the Users OU, not returning the developers or other personnel within the directory. That’s pretty straight forward.
DirectoryEntry developersEntry = new DirectoryEntry(“LDAP://developer.hamilton.com/OU=Accounts,OU=Users,DC=developer,DC=hamilton,DC=com”);
// Now return all users in this OU…
DirectorySearcher searcherResults = new DirectorySearcher(rootEntry,”(CN= *)”);
As you get more familiar with DS programming, you will find there are other ways to accomplish the same result – I am simply trying to open those doors for you.
It is important that you understand how to narrow your search filter in this way because you will find it greatly improves the performance of your application when you know where to search for content and you keep your search filter narrowly defined. For example, if I have 15,000 employees, they would very likely be grouped in AD by their department, and possibly further grouped by sub-departments. I would want to know this information beforehand so I can plan my DS application to be as efficient as possible. I would not want to search the rootDSE each time for a given employee or group of employees, if I know the departments those employees are in.
More on the Search Filter
The filter is a LDAP string that allows you to search for objects based on specific criteria. You will also use relational operators to better refine your search. The following list those operators:
= Equal to
~= Is approximately equal to (or like)
<= Less than or equal to
>= Greater than or equal to
NOTE: You cannot use ‘<’ or ‘>’ individually in LDAP – you must use the notation <= or >=.
The * character works as a wildcard in your search, as in the example above we searched for CN=Mike* or CN=* - in this case, get all Canonical (common) Names beginning with Mike, or get all names period (respectively).
To find all users with a surname of Hamilton you would use
(sn = Hamilton)
To find all users where the email ends in @hamilton.com
(&(objectCategory = person) (objectClass = user) (mail = *@hamilton.com))
Notice the Polish notation used here – the operator comes before the condition. LDAP uses this notation in the Filter. The above example would be more familiar possibly as
(objectCategory = person) AND (objectClass = user) AND (mail = *@hamilton.com)
The following are allowed logical operators:
& AND
| OR
! NOT
You set the filter for a search using the DirectorySearcher.Filter property. The following code will create a binding to the root context, and return everything from the root down:
DirectoryEntry rootEntry = new DirectoryEntry(); // Binds root context
DirectorySearcher rootSearcher = new DirectorySearcher(rootEntry);
Let’s say we have a binding to the rootDSE as above, but only want to return all users with a surname of Hamilton? Change the DirectorySearcher to the following:
DirectorySearcher rootSearcher = new DirectorySearcher(“(sn=Hamilton)”);
You can either pass the DirectoryEntry object, or you can specify a filter in this fashion when creating a DirectorySearcher instance.
Loading Specific Properties
By default, when you create a DirectorySearcher instance, you return all properties for that binding (which can be a lot of data if you have a lot of items in your returned result set).
You can refine your result set by specifying the exact properties to be returned. This is especially important when you could be returning large collections of users.
Following the above example I could create my rootSearcher and then do:
rootSearcher.PropertiesToLoad.Add(“name”);
rootSearcher.PropertiesToLoad.Add(“sn”);
rootSearcher.PropertiesToLoad.Add(“mail”);
rootSearcher.PropertiesToLoad.Add(“telephoneNumber”);
Here I have specifically limited the result set to only include the name, sn (surname), mail and telephoneNumber properties.
Finally, when we define our searcher, we can stipulate the search scope.
Search Scope
The search scope determines the extent of the search in a directory, defaulting to beginning at the root.
In the System.DirectoryServices.SearchScope you will find the following 3 enumerations specified:
Base – Limits the scope to the base, or only 1 object.
OneLevel – Searches 1 level of the immediate child objects, excluding the base object.
Subtree – Searches all child objects under the base object, including the base object.
When creating a DirectorySearcher instance, the following table lists default constructor behaviors:
| PROPERTY | DEFAULT VALUE | DESCRIPTION |
| SearchRoot | Null | Searches the root of the domain controller (rootDSE). |
| Filter | (objectClass=*) | The search retrieves all objects. |
| PropertiesToLoad | An empty string array | The search retrieves all properties. |
| SearchScope | Subtree | The search will be performed on the complete subtree of the base object (or entire domain in this case). |
| | | |
Constructors of the DirectorySearcher Class
The above table displays the default constructor when you create a DirectorySearcher instance like:
DirectoryEntry rootEntry = new DirectoryEntry(); // Binds root context
DirectorySearcher rootSearcher = new DirectorySearcher(rootEntry);
The above specifies the Search Root.
Specifying the Filter Only
We showed this earlier,
DirectorySearcher rootSearcher = new DirectorySearcher(“(sn=Hamilton)”);
Specifying the Search Root and Filter
Create our base / root reference:
DirectoryEntry rootEntry = new DirectoryEntry(“LDAP://developer.hamilton.com/OU=Accounts,OU=Developers,DC=developer.DC=Hamilton,DC=com”);
// Now specify the filter…
DirectorySearch rootSearcher = new DirectorySearcher(rootEntry, “(sn=Hamilton)”);
Specifying the Filter and a List of Properties to load
// Create the set of properties to load…
string [] searchProperties = new string[4];
searchProperties[0] = “name”;
searchProperties[1] = “sn”;
searchProperties[2] = “mail”;
searchProperties[3] = “telephoneNumber”;
// Get all users that have an email ending in hamilton.com…
DirectorySearcher rootSearcher = new DirectorySearcher(“(mail=*hamilton.com)”, searchProperties);
The above example will return all users from the rootDSE with an email ending in hamilton.com. Let’s refine that.
Specifying the Root, Search Filter, and Properties to Load
// Specify the properties to load… Here we get Full Name, Company, Phone…
string [] searchProperties = new string[3];
searchProperties[0] = “name”;
searchProperties[1] = “co”;
searchProperties[2] = “telephoneNumber”;
// Now bind to a more refined root – developers only…
DirectoryEntry rootEntry = new DirectoryEntry(“LDAP://developer.hamilton.com/OU=Accounts,OU=Developers,DC=developer,DC=hamilton,DC=com”);
// No get all users from this root with an email ending in hamilton.com…
DirectorySearcher rootSearcher = new DirectorySearcher(rootEntry, “(mail=*hamilton.com)”, searchProperties);
You should have a better idea now how to use the various constructors of the DirectorySearcher class.
Methods of the DirectorySearcher
When you have created a binding and set filter values, you will use 1 of the 2 DirectorySearcher methods:
FindOne() – Returns the first, and only 1 object of the search criteria.
FindAll() – Returns a SearchResultCollection of all objects that matched the search criteria.
We’ll do a brief sample for each method and wrap up this series with the Sorter class.
Return using FindOne()
// The following demonstrates that FindOne() returns only the first entry that matches the search critieria…
DirectoryEntry rootEntry = new DirectoryEntry(“LDAP://developer.hamilton.com/OU=Accounts,OU=Developers,DC=developer,DC=hamilton,DC=com”);
DirectorySearcher rootSearcher = new DirectorySearcher(rootEntry);
// What to search for…
rootSearcher.Filter = “(CN=*)”;
// Specific properties to load…
rootSearcher.PropertiesToLoad.Add(“name”); // Full name…
rootSearcher.PropertiesToLoad.Add(“mail”); // Primary email addy…
rootSearcher.PropertiesToLoad.Add(“telephoneNumber”); // Phone #...
SearchResult searchResults = rootSearcher.FindOne();
// Extract out the properties returned and display in a console…
ResultPropertyCollection propertiesCollection;
propertiesCollection = searchResults.Properties;
// Cycle through and display (or add to a listbox, etc)…
Foreach (string currentProperty in propertiesCollection.PropertyNames)
{
foreach (Object thisCollection in propertiesCollection[currentProperty])
{
Console.WriteLine(currentProperty + “ = “ + thisCollection);
}
}
Return using FindAll()
Unlike above, the ResultPropertyCollection being returned with FindOne(), FindAll() comes back in a SearchResultCollection object.
We would process the result set similar to the following:
// Get all objects returned…
foreach (SearchResult searchResults in rootSearcher.FindAll())
{ // Process…
foreach (string propertName in searchResults.Properties.PropertyNames)
{
foreach (Object retEntry in searchResults.Properties[propertyName])
{
Console.WriteLine(propertyName + “ = “ + retEntry);
}
}
Console.WriteLine(“”);
}
If the root of the search is not properly set, a InvalidOperationException exception will be thrown. Also, if the provider you are trying this against is not supported, a NotSupportedException exception is thrown.
Sorting the Result Sets
Finally, the SortOption class specifies how to sort the result set of a search. The class also allows you to specify either ascending or descending sort order.
Let’s do a search on all users that have an email ending in @hamilton.com and that exist in the Developers OU. Further, restrict the properties returned, sort the list, and using the code snippet above, list the results in a console window.
DirectoryEntry rootEntry = new DirectoryEntry(“LDAP://developer.hamilton.com/OU=Accounts,OU=Developers,DC=developer,DC=hamilton,DC=com”);
DirectorySearcher rootSearcher = new DirectorySearcher(rootEntry);
// Set our search filter and properties to load…
rootSearcher.Filter = “(mail=*hamilton.com)”;
rootSearcher.PropertiesToLoad.Add(“name”); // Full name…
rootSearcher.PropertiesToLoad.Add(“mail”); // Primary email addy…
rootSearcher.PropertiesToLoad.Add(“telephoneNumber”); // Phone #...
// Now sort the result set…
SortOption sortedResults = new SortOption();
sortedResults.PropertyName = “name”; // Sort by full name…
sortedResults.Direction.SortDirection.Ascending;
// Perform the search and output the results..
rootSearcher.Sort = sortedResults;
foreach (SearchResult searchResults in rootSearcher.FindAll())
{ // Process…
foreach (string propertName in searchResults.Properties.PropertyNames)
{
foreach (Object retEntry in searchResults.Properties[propertyName])
{
Console.WriteLine(propertyName + “ = “ + retEntry);
}
}
Console.WriteLine(“”);
}
Final notes on the following classes: SearchResult, SearchResultCollection, ResultPropertyCollection, and ResultPropertyValueCollection.
The SearchResult class consist of the first entry returned during a search. It retrieves the data from the SearchResultCollection, and the FindOne() method of the DirectorySearcher class returns an instance of the SearchResult class.
The SearchResultCollection class is actually a collection of SearchResult instances and these are returned using the FindAll() method of the DirectorySearcher class. You use the Count property to get the number of items returned in the collection. Use the Item property to index through the SearchResultCollection to enumerate each property. Finally, the PropertiesLoaded property returns a string array containing the names of the properties loaded for the original search criteria.
The ResultPropertyCollection class is accessed as a Properties property of the SearchResult class. It has an Item property you use to index the collection, which is a string value. This index retrieves the values of the property matching the specified index name as a ResultPropertyValueCollection. The following snippet will demonstrate getting all returned properties and outputting the result to a console window:
DirectoryEntry rootEntry = new DirectoryEntry(); // Entire domain…
DireectorySearcher rootSearcher = new DirectorySearcher(rootEntry, “(sn=Hamilton)”);
SearchResult searchResults = rootSearcher.FindOne();
// Get the properties returned…
ResultPropertyCollection propertyCollection = searchResults.Properties;
// Output…
foreach (string thisProperty in propertyCollection.PropertyNames)
{
foreach (Object propertyValue in propertyCollection[thisProperty])
{
Console.WriteLine(thisProperty + “ = “ + propertyValue);
}
}
In the future I will introduce ADSI API and application code and we will see about developing a small application that will take some of what we have reviewed in these three parts and create a directory browser sample application.
I want to note that for the sample code so far, if you are not executing this on an actual AD domain controller, you will need to change the new DirectoryEntry() code to include the root, username and password of the context you are trying to connect as (we have reviewed this in previous lessons).
Finally, please forgive any typo’s – I try hard to make sure there are none.
I have received a great deal of feedback over the little I have posted so far, and I hope this posting is helpful for some. I apologize again for taking so long to get this series posted.