Using System.DirectoryServices to Search the Active Directory

 

Duncan Mackenzie
Microsoft Developer Network

November 1, 2002

Summary: Duncan Mackenzie describes how to use the System.DirectoryServices namespace to search for information in Microsoft Active Directory. (18 printed pages)

Applies to:
   Microsoft® .NET
   Microsoft Windows®
   Microsoft Active Directory®

Contents

Introduction
Moving from ADSI to System.DirectoryServices
Making a Connection
Binding to a Directory Path
Authenticating to Your Directory
Performing a Search
Additional Options
Creating a Quick Search Demo
Moving the Search to a Background Thread
Data Binding with DirectoryServices Objects
Summary

Introduction

Active Directory is a special-purpose database that is replicated across an organization and is easily extensible, making it perfect for use in storing user information, network configuration, and other data that needs to be globally accessible across a company. It is one of the most powerful operating system features, well worth leveraging when building your applications if it is available in your organization.

In this article, I am going to detail how you can use the System.DirectoryServices namespace to connect to the Active Directory, search for objects, and display the results of your search. Of course, System.DirectoryServices is not just for use with Active Directory—it can be used against several different services, including the widely adopted LDAP protocol—but I will be focusing on Active Directory for my samples.

I will not, however, be covering Active Directory in detail. For that I would like to direct you to a few of the relevant reference articles:

Moving from ADSI to System.DirectoryServices

Those new to programming with Active Directory might wonder about all the references to ADSI on MSDN and in other resources. ADSI (which stands for Active Directory Service Interfaces) is a COM library that allows you to interact with Active Directory from non-.NET languages. It is the most common method for accessing directory information from Microsoft® Visual Basic® 6.0, Visual Basic for Applications (VBA), and scripting languages. As such, many samples and newsgroup postings about Active Directory will be focused on ADSI.

The good news is that many of the concepts used in ADSI will be easily converted for use in System.DirectoryServices—as will any ADSI paths or property names shown in an ADSI sample. It is also possible to mix code between the COM and .NET libraries. Several method and properties within the System.DirectoryServices namespace will accept native ADSI objects as parameters. Of course, even if you have never used ADSI, System.DirectoryServices is still easy to learn and use.

Making a Connection

There are two main classes within the System.DirectoryServices namespace, DirectoryEntry and DirectorySearcher. (Several additional classes exist, but these are the ones you need to use first.) Searching is performed using the aptly named DirectorySearcher class. This can be used either without setting any options or by providing a root DirectoryEntry object.

When creating a DirectoryEntry object for use as a root, you will need to specify a path that describes the service you are connecting to. You will also specify your security credentials. Creating this single object establishes a connection to your directory. You can use this connection for your queries by providing this DirectoryEntry object as the root of your search. If, alternatively, you don't specify a root object, the DirectorySearcher will automatically bind to the current domain using your Microsoft® Windows® credentials for authentication.

Binding to a Directory Path

To connect to Active Directory, you can either specify a path using Global Catalog (GC://) syntax, or you can use a standard LDAP path (LDAP://). The syntax and path to use depends on your network environment. For example, working with my internal network, I decided to use the Global Catalog syntax because it allows me to search across my entire enterprise network (the entire Active Directory forest). Instead of a path that includes a server name, I specify just "GC://dc=home,dc=duncanmackenzie,dc=net" (assuming my domain is called home.duncanmackenzie.net), which tells ADSI to connect to a global catalog server for the specified domain. If you wish, you can also connect without specifying any path at all. If you use a DirectorySearcher object without any root object supplied, it will automatically use the current domain for its search. For more information on determining the correct path for your network, check out these references:

Authenticating to Your Directory

Once you have an LDAP or GC path, the next concern is your security credentials. The constructor for the DirectoryEntry class allows you to specify a user ID and password, or you can set the user ID and password properties after you have created your instance of the object. Avoid storing the actual password/user ID in your code. Instead, retrieve this information from the user, or, better yet, use Integrated Authentications.

Dim rootEntry _
    As New DirectoryEntry("GC://dc=home,dc=duncanmackenzie,dc=net", _
                          userID.Text, _
                          password.Text)

Note   For this code to work, you need to reference System.DirectoryServices in your project and add an Imports System.DirectoryServices (Visual Basic .NET) or a using System.DirectoryServices; (C#) line to the top of your source file.

Alternatively, if you don't specify a user ID and password at all, System.DirectoryServices will attempt to connect using Windows Integrated Authentication. In general, I prefer to use the Integrated Authentication option, at least in Microsoft® Windows Forms applications, because each user ends up with only their already existing set of permissions against Active Directory. Using a hard-coded User ID and Password could result in my application giving a user more access to Active Directory than they are supposed to have, which is therefore a security risk.

Once you have your initial (root) DirectoryEntry created, you can start using the DirectorySearcher class to perform queries. Performing a simple search entails only a few steps:

  • Create an instance of DirectorySearcher, optionally using a root DirectoryEntry object.
  • Set the search filter to a string that describes your search criteria.
  • Populate the PropertiesToLoad collection with the list of Active Directory properties that you wish to retrieve for each search result.
  • Execute the FindAll method and retrieve the results.

If desired, you can actually skip almost all of these steps to perform a search with default settings. If you don't provide a root DirectoryEntry object, the DirectorySearcher will bind to the current domain. If you don't provide a search filter, the default is to retrieve all objects, and if you don't specify any PropertiesToLoad values, then all properties (that you have permission to read) are retrieved. These default settings can make it very easy to use the DirectoryServices classes. I would, however, suggest restricting your search using at least a filter and constraining the set of properties loaded, unless you really need all properties and all objects for your application.

Dim searcher As New DirectorySearcher(rootEntry)
searcher.PropertiesToLoad.Add("cn")
searcher.PropertiesToLoad.Add("mail")
'searcher.PropertiesToLoad.AddRange(New String() {"cn", "mail"})
'would also work and saves you some code


searcher.Filter = "(&(anr=duncan)(objectCategory=person))"

Dim results As SearchResultCollection
results = searcher.FindAll()

Additional Options

There are many additional search options that are not required, but can greatly affect the behavior of the query. Here are a few of these additional options with a brief description of their use:

  • CacheResults determines if the search results are stored locally on the client computer. If you use this setting, you will be able to navigate the result set back and forth; otherwise you are in forward-only mode in navigating the results.
  • ClientTimeout, ServerTimeLimit, ServerPageTimeLimit are all timeout values to prevent a search from running too long. The first value, ClientTimeout, controls how long the client waits. The other two time limits are imposed by the server. I suggest setting at least the ClientTimeout, to avoid the possibility of your application waiting indefinitely. It is important to note, though, that if one of the server-based time limits is hit, the entries retrieved up to that point are returned. Nothing is returned if the client time limit is exceeded first. Keep in mind that the server itself has a time limit that can be configured by the administrator, so it may time out (and return the incomplete results) before the time limit you have specified.
  • PageSize determines the number of entries that should be returned at a time. Not setting the PageSize property, or setting it to 0, indicates that all of the results should be returned all at once. Nevertheless, using paging can make your application seem more responsive. Keep in mind that the server will decide the maximum number of objects returned in a search to make sure that a user won't overtax the system. So paging is always recommended, especially if you expect a large result set. In a demo near the end of this article, I will show you how to use paging along with multi-threading to produce a search form that never makes the user wait.

Working with the Results

Once you execute your search, you will be returned an instance of the SearchResultCollection class, which allows you to enumerate through the results. It doesn't matter whether you used paging or not; you still access the search results in the same way. If the next page of results is not yet available, your enumeration will be blocked until the results are ready.

Dim result As SearchResult

For Each result In results
    MessageBox.Show(result.Properties("cn")(0))
Next

Creating a Quick Search Demo

To put all of the steps together, I created a simple application that performs a search against Active Directory. This sample is included in the download for this article, (click the link at the top of this article), or you can just follow along if you wish to build it yourself.

To properly configure the search, including security options, I had to set up a few controls on the blank Windows Form I started with. I ended up with four TextBox controls (the root path, search string, user ID and password), a CheckBox to allow me to toggle between integrated authentication and using a user ID and password, a button to execute the search, and a ListBox to hold the results. I spent a few moments arranging my controls and setting anchor properties to achieve a moderately pleasing appearance and an interface that can handle resizing, but that is certainly optional.

Figure 1. The completed form allows you to perform a simple search.

Now, in the click event of my button, I take the values entered in those four TextBoxes and the CheckBox and perform my search.

Dim rootEntry _
    As New DirectoryEntry(rootPath.Text)

If Not integratedAuth.Checked Then
    rootEntry.Username = userID.Text
    rootEntry.Password = password.Text
End If

Dim searcher As New DirectorySearcher(rootEntry)
searcher.PropertiesToLoad.Add("cn")
searcher.PropertiesToLoad.Add("telephoneNumber")
'searcher.PropertiesToLoad.AddRange(New String() {"cn", "mail"})
'would also work and saves you some code

searcher.PageSize = 5
searcher.ServerTimeLimit = New TimeSpan(0, 0, 30)
searcher.ClientTimeout = New TimeSpan(0, 10, 0)

searcher.Filter = searchString.Text

Dim queryResults As SearchResultCollection
queryResults = searcher.FindAll()

Once I have my results, I add each one to the ListBox.

Dim result As SearchResult

For Each result In queryResults
    results.Items.Add(result.Properties("cn")(0))
Next

Note   Knowing which properties to use, and how to retrieve them, is one of the more complicated System.DirectoryServices concepts. A good source of Active Directory property names, at least for when you are dealing with users and accounts, is the SDK reference that maps property names to information from the User and Groups snap-in in Windows. It is also important to note that many properties can be multi-valued, so the value of a property is exposed as an array that could have any number of values within it. In the sample, I retrieve the value of the cn property by accessing the first member of the property values array. This only works because I happen to know that the cn property contains only a single value. When working with a variety of different properties, you will likely need to research each one to determine if it is single- or multi-valued. The Active Directory Schema reference on MSDN is a good resource for this purpose, giving details on each property (also called an attribute), including their data type and whether they are single- or multi-valued.

When it is completed, the sample above illustrates how you can perform a search against Active Directory, but there are two issues that I would still like to address. The first is that this search is performed on the same thread as my user interface (the Form), which means that whenever my code is waiting on a response from the DirectoryServices objects, my user interface becomes unresponsive. The second issue is that I am manually adding my retrieved results to the ListBox, but many programmers are more familiar with data binding to connect a set of data to a user interface. In the remainder of this article, I will show you how a responsive UI can be provided by using a background thread, and how you can create a DataTable at run time to provide data-binding functionality.

Moving the Search to a Background Thread

Whenever I want to run a task (any task, not just DirectoryServices work) on a background thread, I always end up following the exact same pattern. First I'll describe the pattern, and then I will show you how I applied it to the specific task of a Directory Search.

I create a class to encapsulate my background task. This class includes:

  • Properties that are used to configure the task.
  • A method that cannot take any parameters of its own (which is why I have properties available), which will be executed on the new thread
  • An event or events that fire to communicate the progress (and end) of the background task.

Note   If I use this pattern from a Windows Form, I need to avoid updating the Form itself from the events raised by the background task and use the Form's Invoke method to correctly marshal between the two threads.

Applying this pattern to the search sample created earlier (creating the "DS Background" project in the download), requires the addition of a new class, BackgroundSearch. BackgroundSearch has several properties so that the search can be properly configured, and it has a StartSearch method. As each result is found, a ResultFound (creativity seems to have no place in my naming conventions) event is raised. When the entire search is completed, a SearchCompleted event occurs.

Imports System
Imports System.DirectoryServices

Public Class BackgroundSearch

    Dim m_FilterString As String
    Dim m_PageSize As Integer
    Dim m_RootPath As String
    Dim m_PropertiesToLoad() As String
    Dim m_IntegratedAuthentication As Boolean
    Dim m_UserID As String
    Dim m_Password As String

    Public Property IntegratedAuthentication() As Boolean
        Get
            Return m_IntegratedAuthentication
        End Get
        Set(ByVal Value As Boolean)
            m_IntegratedAuthentication = Value
        End Set
    End Property
    Public Property UserID() As String
        Get
            Return m_UserID
        End Get
        Set(ByVal Value As String)
            m_UserID = Value
        End Set
    End Property
 
    Public Property Password() As String
        Get
            Return m_Password
        End Get
        Set(ByVal Value As String)
            m_Password = Value
        End Set
    End Property

    Public Property PropertiesToLoad() As String()
        Get
            Return m_PropertiesToLoad
        End Get
        Set(ByVal Value As String())
            m_PropertiesToLoad = Value
        End Set
    End Property

    Public Property FilterString() As String
        Get
            Return m_FilterString
        End Get
        Set(ByVal Value As String)
            m_FilterString = Value
        End Set
    End Property
    Public Property PageSize() As Integer
        Get
            Return m_PageSize
        End Get
        Set(ByVal Value As Integer)
            m_PageSize = Value
        End Set
    End Property
    Public Property RootPath() As String
        Get
            Return m_RootPath
        End Get
        Set(ByVal Value As String)
            m_RootPath = Value
        End Set
    End Property

    Public Event ResultFound(ByVal result As SearchResult)
    Public Event SearchCompleted(ByVal entriesFound As Integer)

    Public Sub StartSearch()
        Dim rootEntry _
            As New DirectoryEntry(RootPath)

        If Not IntegratedAuthentication Then
            rootEntry.Username = UserID
            rootEntry.Password = Password
        End If

        Dim searcher As New DirectorySearcher(rootEntry)
        searcher.PropertiesToLoad.AddRange(PropertiesToLoad)
        searcher.PageSize = PageSize
        searcher.ServerTimeLimit = New TimeSpan(0, 10, 0)
        searcher.Filter = FilterString

        Dim queryResults As SearchResultCollection
        queryResults = searcher.FindAll()

        Dim result As SearchResult
        Dim resultCount As Integer = 0
        For Each result In queryResults
            RaiseEvent ResultFound(result)
            resultCount += 1
        Next
        RaiseEvent SearchCompleted(resultCount)
    End Sub
End Class

Now, as I alluded to earlier, any code placed into an event handler for these events will be executing on the background thread, not on the same thread as the Form itself. If you wish to modify your Form (or a control on your form), you will need to marshal the call back to the Form's thread. My sample handles this issue by providing an extra procedure to perform the actual insert into the ListBox, and using the Form's Invoke method to call that procedure from within the event-handling routine.

Private Sub startSearch_Click( _
    ByVal sender As Object, _
    ByVal e As EventArgs) Handles startSearch.Click

    With bkg
        .RootPath = rootPath.Text
        .FilterString = searchString.Text
        If Not integratedAuth.Checked Then
            .UserID = userID.Text
            .Password = password.Text
        End If
        .PageSize = 5
        .PropertiesToLoad = _
            New String() {"cn", "mail", "telephoneNumber"}
        Dim search As New _
            Threading.Thread(AddressOf .StartSearch)
        search.Start()
    End With
End Sub

Private Sub bkg_ResultFound( _
            ByVal result As SearchResult) _
        Handles bkg.ResultFound

    If result.Properties.Contains("mail") Then
        Dim emailAddress As String
        emailAddress = CStr(result.Properties("mail")(0))
        Dim display As New displayResult _
             (AddressOf AddTextToListBox)
        Me.Invoke(display, New Object() {emailAddress})
    End If

End Sub

Private Delegate Sub displayResult(ByVal textEntry As String)

Private Sub AddTextToListBox(ByVal textEntry As String)
    results.Items.Add(textEntry)
End Sub

Private Sub bkg_SearchCompleted( _
        ByVal entriesFound As Integer) _
    Handles bkg.SearchCompleted
    MessageBox.Show( _
         String.Format("{0} Entries Found", entriesFound))
End Sub

If you choose to execute your search on a background thread, you may wish to play with the page size, as this will affect the size of each update to your user interface.

Data Binding with DirectoryServices Objects

Directory entries have dynamic properties, which is to say they do not have a predefined set of properties, but rather, a properties collection that is determined at run time. This is a good model for a DirectoryServices object, since the set of available properties on any particular object is not fixed. Nevertheless, it causes a problem when you attempt to data bind with these objects. Without a fixed set of properties, you cannot directly bind a control like the DataGrid to a collection of SearchResult objects. This is not the end of the game, though. If you do wish to data bind, it is still possible; it just takes a little work.

Essentially, you have to wrap your search results from DirectoryServices into some other object with static properties, and then bind to this new object. For my purposes, I decided that I had two choices: I could write a custom class that exposed the specific properties I wanted, and then change the class every time I added or removed a property from my DirectoryServices search, or I could dynamically create a DataTable and use that as my bindable object. The DataTable approach is much easier and still functions well, so it was the clear winner for this situation. The third version of my original sample (the "DS DataBinding" project) creates a DataTable and binds the results to a DataGrid. It is included in the code download for this article. To create a DataTable without a database (or XML file), create a new DataTable instance and add DataColumns to it for each of your properties.

Dim myTable As New DataTable("Results")
Dim colName As String

For Each colName In searcher.PropertiesToLoad
    myTable.Columns.Add(colName, GetType(System.String))
Next

Once you start receiving results, just fill up your DataTable and you are ready to go.

Dim result As SearchResult

For Each result In queryResults
Dim dr As DataRow = myTable.NewRow()
    For Each colName In searcher.PropertiesToLoad
        If result.Properties.Contains(colName) Then
            dr(colName) = CStr(result.Properties(colName)(0))
        Else
            dr(colName) = ""
        End If
    Next
    myTable.Rows.Add(dr)
Next
results.SetDataBinding(myTable.DefaultView, "")

Summary

System.DirectoryServices allows you to easily connect to Active Directory and leverage its functionality in your applications, opening up an enormous set of possibilities. The samples in this article detailed only one use of this technology—searching the user directory—but the System.DirectoryServices namespace offers a good deal of additional utility, including network administration, server configuration, and more.

   

For background on the author, see Duncan Mackenzie's profile on GotDotNet.