Click to Rate and Give Feedback
MSDN
MSDN Library
.NET Development
.NET General
 Introduction to System.DirectorySer...
Introduction to System.DirectoryServices.Protocols (S.DS.P)
 

by Ethan Wilansky

March 2007

Summary: System.DirectoryServices.Protocols (S.DS.P), first introduced in the .NET Framework 2.0, is a powerful namespace that brings LDAP programming to managed code developers. This paper provides you with an introduction to programming with S.DS.P by describing common directory management tasks and how you code those tasks using this namespace. (48 printed pages)

Contents

Introduction
   What to Expect
   How to Prepare for Running the Code
   What Not to Expect
System.DirectoryServices.Protocols Architecture
Common Patterns
   Establishing an LDAP Connection
   Request and Response Classes
Management Tasks
   LDAP Directory Management Tasks
Search Operations
   Performing a Simple Search
   Returning Attribute Values
   Running a Paged Search
   Running an Asynchronous Search
   Creating an Attribute Scoped Query (ASQ)
   Creating a Virtual List View
Advanced LDAP Server Connection and Session Options
   Binding over a TLS/SSL Encrypted Connection
   Performing Fast Concurrent Bind Operations
   Leveraging Transport Layer Security
   Performing Certificate-Based Authentication
References
Conclusion

Introduction

In the first white paper of this series, I explored System.DirectoryServices.ActiveDirectory (S.DS.AD) to help you understand how you can use this namespace for administering Active Directory and ADAM instances. S.DS.AD is a specialized namespace specifically targeted at programmers working with Microsoft directory services. In contrast, System.DirectoryServices.Protocols (S.DS.P) is a namespace designed for LDAP programming in general. In addition, it provides capabilities that were previously unavailable to managed code programmers.

This paper provides you with an introduction to S.DS.P by describing common tasks and how you code those tasks with this namespace first introduced in the .NET Framework 2.0: for example, how to perform simple tasks, like creating a user account, to more complex tasks, such as running an attribute scoped query against an LDAP directory or binding to a directory server using X.509 client and server certificates. While some of these types of programming tasks are possible to complete with S.DS alone, Microsoft has opened the full power of directory programming to managed code developers through S.DS.P.

If you are already familiar with directory services programming using managed code, you will immediately see that many of the directory services programming tasks I introduce you to in this paper can be completed using the System.DirectoryServices namespace. However, these examples are a great way to get familiarized with common S.DS.P programming patterns. In addition, S.DS.P provides raw LDAP access, meaning that it is designed specifically to reach beyond Active Directory and ADAM to other LDAP compliant directories. Therefore, if you plan to use .NET managed code against other LDAP directories, a great place to focus is on S.DS.P.

The example companies, organizations, products, domain names, e-mail addresses, logos, people, places, and events depicted herein are fictitious. No association with any real company, organization, product, domain name, email address, logo, person, places, or events is intended or should be inferred.

What to Expect

This paper includes copious code examples and an associated code download so that you can test everything explored here. The examples are intentionally simple so that you can quickly grasp their purpose and begin to detect patterns that repeat throughout this namespace exploration. For example, by the time you complete this introduction, you'll have an excellent understanding of how to connect to a directory using the LdapConnection class and how the various classes derived from the DirectoryRequest and DirectoryResponse base classes allow you to interact with an LDAP directory.

The code download is a console application solution containing the DirectoryServices.Protocols project. The code examples include some error handling. However, for brevity, I've left out a lot of error handling that I consider essential for production code. Use the error handling examples in the code as a starting point. Also, refer to each member of a class that you use in your code to see what exception types it exposes. Code examples are similar to the snippets here, but many of them allow you to supply parameters for items that appear hard-coded in the simple examples in this paper.

How to Prepare for Running the Code

To run these examples, have at least one Active Directory domain available for testing. Optionally, have an ADAM instance available and know the designated port numbers applied to the instance. The advanced authentication operations also require a valid SSL certificate and one example requires a client certificate. The Advanced LDAP Server Connection and Session Options section includes reference information to help you configure certificates. After compiling the solution, your output will be the DS.P program. Typing the program name at the command line will return a list of available commands and their parameters, as shown here:

  • Common management tasks

    CreateUsers server_or_domain_name targetOu numUsers
    AddObject server_or_domain_name dn dirClassType
    AddAttribute server_or_domain_name dn attributeName attributeValue
    AddAttribute2 server_or_domain_name dn attributeName attributeValue
    AddAttributeUri server_or_domain_name dn attributeName attributeUriValue
    AddMVAttribStrings server_or_domain_name dn attribName "attribVal1,...attribValN"
    DeleteAttribute server_or_domain_name dn attributeName
    EnableAccount server_or_domain_name dn
    DeleteObject server_or_domain_name dn
    MoveRenameObject server_or_domain_name originalDn newParentDn objectName

  • Search operations

    SimpleSearch server_or_domain_name startingDn
    AttributeSearch server_or_domain_name startingDn "attribName1,...attribNameN"
    TokenGroupsSearch server_or_domain_name DnofUserAccount
    PagedSearch server_or_domain_name startingDn numbericPageSize
    AsyncSearch server_or_domain_name startingDn
    Asq server_or_domain_name groupDn
    Vlv server_or_domain_name startingDn maxNumberOfEntries nameToSearch

  • Advanced authentication operations

    Sslbind fullyQualifiedHostName:sslPort userName password
    FastConBind server_or_domain_name user1 pword1 user2 pword2 domainName
    Tls fullyQualifiedHostName_or_domainName userName password domainName
    cert fullyQualifiedHostName:sslPort clientCert certPassword

Be sure that you install the .NET Framework 2.0 or later wherever you are going to compile and run the sample, and also be sure to reference the System.DirectoryServices.Protocols assembly.

What Not to Expect

S.DS.P is a robust namespace and while I attempt to provide you with useful introductory examples, it is not a complete survey of its members. For example, I don't explore how this namespace provides members to perform Directory Services Markup Language (DSML) operations. I also do not provide guidance on best practices for directory services programming or guidance on best practices for configuration settings in ADAM or Active Directory. The .NET Developer's Guide to Directory Services Programming by Joe Kaplan and Ryan Dunn will provide the directory programming best practices not included in this introduction and I'll reference a number of good online resources for configuring Active Directory. Also, I'm hoping to write about DSML programming with S.DS.P in the future. Finally, all the examples are in C#. Even if C# isn't your language of choice, I think you will find the examples simple enough to rewrite/convert into your preferred managed code language.

System.DirectoryServices.Protocols Architecture

S.DS.P is one of three namespaces Microsoft has created for directory services programming in managed code. Unlike the other two namespaces, System.DirectoryServices and System.DirectoryServices.ActiveDirectory, S.DS.P provides raw access to underlying LDAP-based directories, such as Active Directory and ADAM.

The darker boxes in Figure 1 show the essential S.DS.P components. The lighter boxes provide a relative mapping to the other components associated with directory services programming. Why bother showing the lighter boxes? To demonstrate exactly where S.DS.P resides in the hierarchy of directory services programming namespaces. S.DS.P exclusively relies on the LDAP APIs in wldap32 to access underlying LDAP directories.

Figure 1. S.DS.P Architectural Block Diagram

Preparing to Use S.DS.P

As the architectural diagram depicts, the S.DS.P namespace is distinct from S.DS and S.DS.AD and resides in its own assembly, System.DirectoryServices.Protocols.dll. Thus, to use S.DS.P, you must reference the System.DirectoryServices.Protocols.dll assembly in your project. In contrast, System.DirectoryServices.dll contains both S.DS and S.DS.AD and referencing this single assembly gives you access to both namespaces.

Common Patterns

What I've come to appreciate with .NET programming is the patterns that emerge when you work with a namespace for a while. Calling out those patterns is really helpful when you begin coding in a namespace. In S.DS.P, you will almost always begin a code task by establishing a connection and eventually sending a request through that connection to a directory server and receiving a response. I outline how to complete these common tasks here and you will see them repeated throughout this paper and in the code download.

Establishing an LDAP Connection

The first step in all of the code examples in this paper and the associated code download is making an initial connection to a directory server. Making a connection does not bind to objects in the directory. Binding either occurs automatically as a result of a directory service operation that requires it, or you can perform an explicit bind operation by calling the Bind method from your code. In either case, binding to a directory sends credentials to a directory server.

The following key classes are involved in making an initial connection to a directory server:

  • LdapConnection   To create an TCP or UDP connection to a directory server.

    For example, the following code snippet creates a connection to an available directory server using the default connection port, which is 389:

    LdapConnection connection = new LdapConnection("fabrikam.com");
  • NetworkCredential   To pass credentials to the LdapConnection object.

    For example, the following code snippet creates a credential object with a username of user1, a password of password1 to a domain named fabrikam:

    NetworkCredential credential = 
    new NetworkCredential("user1", "password1", "fabrikam");

    You then set the Credential property of the connection equal to the credential object, like so:

    connection.Credential = credential

    These credentials are not sent to a directory server until you bind to a directory. If you don't specify credentials, when a bind occurs, the current user's credentials are sent.

  • LdapSessionOptions   To configure a connection for advanced operations like client server certificate authentication, fast concurrent binding and page search operations.

    A number of code examples in this paper and in the code download use this class. Therefore, I won't show an example of using it here.

  • LdapDirectoryIdentifier   To pass connection, host name and server information to the LdapConnection object.

    For example, the following code snippet creates an identifier object to a directory server named sea-dc-02.fabrikam.com using the Active Directory SSL port:

       LdapDirectoryIdentifier identifier = 
    new LdapDirectoryIdentifier("sea-dc-02.fabricom.com:636");

    You then pass the identifier to the connection object when you create the connection, like so:

    LdapConnection connection = new LdapConnection (identifier);

    I use this object in a single code example, but it can be useful if you want to establish a connection over UDP or separate the identifying information about a connection from the creation of the LdapConnection object.

Request and Response Classes

A fundamental part of interacting with a directory service via LDAP is creating and sending requests and receiving responses. The synchronous S.DS.P method for sending a request is SendRequest. A directory server then returns a response that you can cast into the appropriate response object.

When you call the SendRequest method of an LdapConnection, the method ships an LDAP operation to a directory server and the server returns a DirectoryResponse object. The object returned aligns in structure with the type of request. For example, if you supply the SendRequest method with an AddRequest object, the directory server returns a DirectoryResponse object that is structurally equivalent to an AddResponse object. You must then cast the returned DirectoryResponse base class into an AddResponse object before you inspect the response. The pattern for this is:

DirectoryRequestType request = new DirectoryRequestType(parameters…);
DirectoryResponseType response = 
       (DirectoryResponseType)connection.SendRequest(request);

The following code snippet demonstrates how to implement this pattern using the AddRequest and AddResponse objects. The values of the dn and dirClassType are defined elsewhere and are not shown here to avoid obscuring the pattern:

// build an addrequest object 
AddRequest addRequest = new AddRequest(dn, dirClassType);
                
// cast the response into an AddResponse object to get the response
AddResponse addResponse = (AddResponse)connection.SendRequest(addRequest);

The following request classes map to the listed response classes appearing in Table 1:

Table 1. DirectoryRequest and Corresponding DirectoryResponse Classes

The .NET Framework SDK Class Library Reference describes the purpose of each request and response class. In addition, I demonstrate how to use all of these request objects except the last two DSML request objects. For more information on S.DS.P architecture, see "System.DirectoryServices.Protocols Architecture" at http://msdn2.microsoft.com/en-us/library/ms257187.aspx.

Management Tasks

Common directory services management tasks include creating, adding, moving, modifying and deleting directory objects. While S.DS provides all of these capabilities, S.DS.P allows you to use common LDAP programming constructs to perform the same tasks. S.DS is easier for these code tasks, but seeing how to complete these familiar tasks with S.DS.P is a great way to introduce key members of this namespace.

LDAP Directory Management Tasks

Code examples in this section will build on one another to familiarize you with the common patterns. For instance, the first example will show you how to create 100 user accounts in just a few lines of code by using the AddRequest object, but it won't show you how to get a response back about the task from a directory server. The next example returns to the essence of the first create users task by demonstrating how to add any valid object to the directory, and it also shows how to get a response back about the task. A later example introduces you to the ModifyRequest object for managing an attribute, but it doesn't demonstrate how to get a response back about whether the attribute was successfully modified. Immediately following that example, I introduce the ModifyResponse object. This incremental approach, I believe, will help you better understand how to build on the examples to create more complex and useful code.

Creating Users Accounts

A classic initial demonstration of directory services programming techniques often involves generating many user accounts with only a few lines of code. As S.DS.P is arguably the most radical departure from traditional directory services coding in the .NET Framework, I think a multi-user creation example is a good starting point. I think you would agree that it's more useful than writing Hello World to an attribute!

The following code example demonstrates how to create 100 user accounts in just a few lines of code:

  1. Establish an LDAP connection to a directory server.

    In an Active Directory domain, the Locator service provides the host name of a domain controller in the specified domain for the connection.

  2. Declare and initialize the dn variable with the distinguishedName value of each user account to create.
  3. Call the SendRequest method of the connection object to transport each request to a directory server.

    You pass a directory request (in this case an AddRequest) to the SendRequest method. The SendRequest method then automatically binds to a domain controller in the targeted domain using the current user's credentials.

Example 1. Creating 100 user accounts

LdapConnection connection = new LdapConnection("fabrikam.com");
for (int i = 1; i <= 101; i++)
{
    string dn = "cn=user" + i + ",ou=UserAccounts,dc=fabrikam,dc=com";
    connection.SendRequest(new AddRequest(dn, "user"));
}

If you were to run this code, you wouldn't get any return results and the user accounts created in the fabrikam.com Active Directory domain would be disabled. Obviously, this is a pedantic example, but it effectively demonstrates that even a namespace as sophisticated as S.DS.P provides a simple and elegant model to complete significant directory management tasks.

Adding an Object to a Directory

As you saw in the previous create user example, when you call the SendRequest method, you pass the method an AddRequest object to create a user by the specified name. The second parameter in the AddRequest can either be an array of attributes to assign to the object or the lDAPDisplayName of the class schema object from which the object should be derived.

In order to get a response from a directory server about the success or failure of the requested operation, you cast the returned DirectoryResponse base class into the proper response type based on the type of DirectoryRequest object you pass to the SendRequest method.

The following code example demonstrates how to add a directory object named Seasoned derived from the organizationalUnit class schema object to the directory below the techwriters ou in the fabrikam.com domain:

  1. Declare and initialize some string variables used later in the example.

    The corresponding code download allows you to pass these and other values in as command line arguments.

  2. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.

    Because a specific domain controller was not declared for the hostordomainName variable, the Active Directory Locator will find an available domain controller for the binding operation. This is referred to as serverless binding.

  3. Create an AddRequest object and pass it the distinguished name stored in the dn variable and the lDAPDisplayName of the class schema object to instantiate.
  4. Call the SendRequest method of the connection object and cast the returned DirectoryResponse as an AddResponse object.

    An implicit bind occurs here.

  5. Display information about the request. The response from the directory server is contained in the ResultCode property of the addResponse object.

    The AddResponse class contains an ErrorMessage response property that you can display for more information on any error that might be returned from the directory server.

Example 2. Adding an OrganizationalUnit object

string hostOrDomainName = "fabrikam.com";
string dn = "ou=Seasoned,ou=techwriters,dc=fabrikam,dc=com";
string dirClasstype = "organizationalUnit";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

try
{
    // create an addrequest object 
    AddRequest addRequest = new AddRequest(dn, dirClassType);

    // cast the returned DirectoryResponse as an AddResponse object
    AddResponse addResponse =
                            (AddResponse)connection.SendRequest(addRequest);

    Console.WriteLine("A {0} with a dn of\n {1} was added successfully " +
        "The server response was {2}",
        dirClassType, dn, addResponse.ResultCode);
}
catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}

Adding an Object to a Directory Using a Different AddRequest Constructor

Before delving into another code example, let's step back for a moment and consider how the definition of class schema objects plays an important role in directory object creation. This is essential to understand before you try to use the alternative AddRequest constructor to create directory objects.

The attributes of a class schema object define the object. A key part of that definition is the attributes that the object must or may contain. When you instantiate a directory object from a class schema object, you or the directory service must provide values for any attributes that the directory object must contain (mandatory attributes) when it is created.

In the prior code example (Example 2), I demonstrate how to add an OrganizationalUnit object to the directory by using the AddRequest constructor. In that case, the constructor takes the distinguishedName of the object to create and the type of object class from which the object is derived. If you take a close look at the organizationalUnit class schema object in an Active Directory or ADAM schema, you will see that the instanceType, objectCategory, nTSecurityDescriptor, objectClass and ou attributes must be defined for the object in order for it to be created. The organizationalUnit class inherits from the Top schema class object, which defines the first four of those attributes as mandatory and the organizationalUnit class defines the ou attribute as mandatory. You must provide values for the ou attribute and the objectClass attribute, and directory services takes care of providing the other values.

Now that you know the mandatory attributes and who has to set what, you can make use of the AddRequest constructor that takes the distinguished name of the object you want to create and an array of DirectoryAttribute objects. The array of objects must include values for any mandatory attributes that directory services will not set for you or that are not defined as part of the distinguished name of the new object. Considering the previous organizationalUnit example, the following code snippet shows how you can define the one required directory attribute (objectClass) by creating a DirectoryAttribute object:

DirectoryAttribute objectClass =  new DirectoryAttribute("objectClass", "organizationalUnit");

You can then pass that to the AddRequest object, like so:

addRequest = new AddRequest(dn, objectClass);

You might notice that this doesn't add much to the prior code example (Error! Reference source not found.). It gets more interesting when you encounter a directory object that contains more mandatory attributes, such as an object derived from the User class schema object, which also requires other mandatory attributes (i.e., sAMAccountName), or you want to add additional optional attributes to an object when it's created. In the following code snippet I define two optional attributes, the city ("l") directory attribute and description directory attribute, and pass those along with the objectClass mandatory directory attribute when I call the AddRequest constructor to create an OU:

DirectoryAttribute l = 
    new DirectoryAttribute("l", "Redmond");

DirectoryAttribute description = 
    new DirectoryAttribute("description", "Writers with 3 years of experience");

DirectoryAttribute objectClass =
    new DirectoryAttribute("objectClass", "organizationalUnit");

// create a DirectoryAttribute array and pass in three directory attributes           
DirectoryAttribute[] dirAttribs = new DirectoryAttribute[3];
dirAttribs[0] = l;
dirAttribs[1] = description;
dirAttribs[2] = objectClass;

// create an addrequest object
addRequest = new AddRequest(dn, dirAttribs);

Note that there is not a corresponding code sample with the code download for this variation on creating an AddRequest object. Start with the AddObject method in the code sample and this information to create a method that uses this AddRequest constructor.

Adding an Attribute to a Directory Object

After creating an object in a directory, you might want to add optional attributes to it. For all attributes that an object may contain (optional attributes), you can add them using the ModifyRequest object.

To add an attribute to an existing directory object, create a ModifyRequest object and in that object specify the distinguished name of the object you want to modify along with the Add value of the DirectoryAttributeOperation enumeration, the attribute name and value to add.

The DirectoryAttributeOperation enumeration contains three values: Add, Delete and Replace. If an attribute already exists in an object, specifying an Add DirectoryAttributeOperation will throw a DirectoryOperationException error. Therefore, if you want to update an existing attribute, use the Replace DirectoryAttributeOperation value instead.

The following code sample demonstrates how to add a department attribute and value of Human Resources to a user account object named John Doe in the TechWriters OU of the fabrikam.com domain:

  1. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  2. Create a ModifyRequest object and pass it the distinguished name stored in the dn variable, the Add value of the DirectoryAttributeOperation enumeration, the lDAPDisplayName of the attribute to add and the value to assign the attribute.
  3. Call the SendRequest method of the connection object and pass it the modRequest object.

    If the attribute has not been assigned to the user account, the send request will succeed. Otherwise, a DirectoryOperationException will be thrown.

  4. If a DirectoryOperationException occurs, it might be because the attribute has already been assigned to the object. Therefore, create a new ModifyRequest object and leave all parameters the same except call the Replace value of the DirectoryAttributeOperation enumeration.

    An additional try catch block appears inside the request to modify an existing attribute in case this attempt also throws a DirectoryOperationException error.

  5. If no errors are thrown, report that the operation was successful.

    This result might not be correct, as the code does not consult the server to verify that the LDAP operation was successful or not. The next section explores how to get a response back from a directory server.

Example 3. Adding or replacing the department attribute of a user account

string hostOrDomainName = "fabrikam.com";
string dn = "cn=john doe,ou=techwriters,dc=fabrikam,dc=com";
string attributeName = "department";
string attributeValue = "Accounting";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

try
{
    ModifyRequest modRequest = new ModifyRequest(
            dn, DirectoryAttributeOperation.Add,
            attributeName, attributeValue);
    
    // example of modifyrequest not using the response object...
    connection.SendRequest(modRequest);
    Console.WriteLine("{0} of {1} added successfully.", 
        attributeName, attributeValue);
}
catch (DirectoryOperationException)
{
    try
    {
        ModifyRequest modRequest = new ModifyRequest(
                dn, DirectoryAttributeOperation.Replace,
                attributeName, attributeValue);

        connection.SendRequest(modRequest);
        Console.WriteLine("The {0} attribute in:\n{1}\nreplaced " +
            "successfully with a value of {2}",
            attributeName, dn, attributeValue);
    }
    catch (DirectoryOperationException e)
    {
        Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                          e.GetType().Name, e.Message);
    }

}

catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}
Important   Consider using the code example appearing next (Example 4) as a starting point for building robust code for adding or replacing attributes. That example uses the directory response object to determine if the attribute has already been set and to check if the directory operation was successful.

Getting Feedback from a Directory Server from an Object Modify Request

The example appearing in Example 3 does not demonstrate the pairing of the ModifyRequest and ModifyResponse classes or how you can leverage the DirectoryOperationException class to determine more about an error response. While the code catches errors, it doesn't directly display responses from a directory server as a result of modifying an object. The pattern for using a ModifyResponse object to properly cast a returned directory response from a ModifyRequest is identical to the pattern I demonstrated for casting a directory response from an AddRequest into an AddResponse. The code download with this article contains the AddAttribute2 method so that you have a complete example using the ModifyRequest and ModifyResponse classes. The following code snippet shows how you use the ModifyResponse object in an example similar to Example 3:

// build a modifyrequest object 
ModifyRequest  modRequest =
        new ModifyRequest(dn, DirectoryAttributeOperation.Add,
        attributeName, attributeValue);

// cast the returned directory response into a ModifyResponse type named modResponse
ModifyResponse modResponse = (ModifyResponse)connection.SendRequest(modRequest);

Console.WriteLine("The {0} attribute in {1} added successfully " +
        "with a value of {2}. The server response was {3}",
        attributeName, dn, attributeValue, modResponse.ResultCode);

When an add operation fails, you can determine why by examining the server's directory response more closely. The SendRequest method throws a DirectoryOperationException if the directory server returns a DirectoryResponse object containing an error. This directory response is packaged in the Response property of the exception.

The ResultCode of the directory response returns a value contained in the ResultCode enumeration. This enumeration is rich with a plethora of error values. For example, an error equivalent to the AttributeOrValueExists value is returned if an attribute is assigned to an object.

The following code example demonstrates how to use the ModifyResponse class to verify a directory object modification and how to use a directory response object containing an error code to handle a DirectoryOperationException. This code sample is similar to Error! Reference source not found., but provides a better starting point for building code that adds or replaces an attribute value:

  1. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  2. Declare the ModifyRequest and ModifyResponse objects and name them modRequest and modResponse respectively.

    These two objects are declared here because they could be used within two try catch blocks. This is more efficient than the code example appearing in Error! Reference source not found. where there is a potential of creating two ModifyRequest objects, one for the attempted add operation and another for the replace operation.

  3. Initialize modRequest by passing it the distinguished name stored in the dn variable, the Add value of the DirectoryAttributeOperation enumeration, the lDAPDisplayName of the attribute to add and the value to assign the attribute.
  4. Cast the returned directory response object into a ModifyResponse object named modResponse.

    If the attribute has not been assigned to the user account, the send request will succeed. Otherwise, the SendRequest throws a DirectoryOperationException.

  5. Catch the DirectoryOperationException and name the returned object doe.
  6. Check the ResultCode property of the directory response object. If the result code is equivalent to the AttributeOrValueExists value in the ResultCode enumeration, then attempt to replace the attribute value.

    The Response property of the DirectoryOperationException object named doe contains the directory response object.

  7. Create a new ModifyRequest object and leave all parameters the same except call the Replace value of the DirectoryAttributeOperation enumeration.

    An additional try catch block appears inside the request to modify an existing attribute in case other errors are thrown. However, you can more elegantly handle errors using other values of the ResultCode enumeration. For example, if the object specified, cn=john doe,ou=techwriters,dc=fabrikam,dc=com in this example does not exist, the directory response will be equivalent to the NoSuchObject value of the ResultCode enumeration.

Example 4. A more robust example demonstrating how to add or replace an attribute of a directory object

string hostOrDomainName = "fabrikam.com";
string dn = "cn=john doe,ou=techwriters,dc=fabrikam,dc=com";
string attributeName = "department";
string attributeValue = "Accounting";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

// declare the request and response objects here
// they are used in two blocks
ModifyRequest modRequest;
ModifyResponse modResponse;

try
{
    // initialize the modRequest object 
    modRequest =
        new ModifyRequest(dn, DirectoryAttributeOperation.Add,
        attributeName, attributeValue);

    // cast the returned directory response into a ModifyResponse type 
    // named modResponse
    modResponse =
        (ModifyResponse)connection.SendRequest(modRequest);

    Console.WriteLine("The {0} attribute of {1} added successfully " +
        "with a value of {2}. The server response was {3}",
        attributeName, dn, attributeValue, modResponse.ResultCode);

}

// if the code enters this catch block, it might be 
// caused by the presence of the specified attribute. 
// The DirectoryAttributeOperation.Add enumeration fails
// if the attribute is already present.
catch (DirectoryOperationException doe)
{
    // The resultcode from the error message states that 
    // the attribute already exists
    if (doe.Response.ResultCode == ResultCode.AttributeOrValueExists)
    {
        try
        {
            modRequest = new ModifyRequest(
                                dn,
                                DirectoryAttributeOperation.Replace,
                                attributeName,
                                attributeValue);

            modResponse =
                (ModifyResponse)connection.SendRequest(modRequest);

            Console.WriteLine("The {0} attribute of {1} replaced " +
                "successfully with a value of {2}. The server " +
                "response was {3}",
                attributeName, dn, attributeValue, modResponse.ResultCode);
        }
        // this catch block will handle other errors that you could
        // more elegantly handle with other values in the 
        // ResultCode enumeration.
        catch (Exception e)
        {
            Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
              e.GetType().Name, e.Message);
        }
    }

}

catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}

To keep the remaining code examples as simple as possible, I show just a few of the most common interrogations of the ResultCode property in a response object. In production code, you will want to examine many more result codes contained in a DirectoryOperationException. Use the examples I show as a starting point for handling other directory response result codes. Carefully review the ResultCode enumeration for other common directory responses.

In addition, the prior code example can be simplified with the PermissiveModifyControl directory control, which is explored in the next section.

Adding Values to a Multi-Valued Attribute

Many attributes can hold more than one value. The classic example of this is the member attribute of a group. In the prior examples of ModifyRequest, I show you how to add an attribute or replace a value in an existing attribute of an object. If you review the ModifyRequest constructor in the .NET Class Library or through Visual Studio, you'll notice that the fourth parameter of the constructor I use in the previous examples is actually an object array that can take a variable number of arguments. Therefore, you can pass an array of values to populate a multi-valued attribute with many entries.

Another important detail about using the ModifyRequest constructor with a multi-valued attribute is that the second parameter, the DirectoryAttributeOperation enumeration, behaves differently based on whether you are working with a single or multi-valued attribute. The following table describes how this enumeration behaves for single-valued and multi-valued attributes.

Table 2. How the DirectoryAttributeOperation Enumeration Operations Interact With Single and Multi-Valued Attributes

This is a lot to keep in mind and leads to writing lengthy error handling code. To more gracefully modify both single- and multi-valued attributes, you can add the PermissiveModifyControl directory control to the Controls collection of the modify request. An LDAP modify request will normally fail if it attempts to add an attribute that already exists or if it attempts to delete an attribute that does not exist. With this control the modify operation succeeds without throwing a DirectoryOperationException error. The only negative consequence of using this directory control is that you won't be able to tell whether the attribute or value being modified was present or not.

Later in the search examples, I demonstrate more examples of directory controls. The following code snippet demonstrates how to add this control to a ModifyRequest object named modRequest before calling the SendRequest method:

// create the PermissiveModifyControl to better control modification behavior
PermissiveModifyControl permissiveModify =  new PermissiveModifyControl();

// add the directory control to the modifyRequest
modRequest.Controls.Add(permissiveModify);

// cast the returned directory response into a ModifyResponse and 
// store the response in the modResponse object
modResponse = (ModifyResponse)connection.SendRequest(modRequest);

The following code example demonstrates how to pass a string array to populate the Url multi-valued attribute of a user account object:

  1. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  2. Create a ModifyRequest object and pass it the distinguished name of a user account stored in the dn variable, the Add value of the DirectoryAttributeOperation enumeration, the lDAPDisplayName of the attribute to add and the values in the attribVals string array for assigning to this multi-valued attribute.
  3. Create the PermissiveModifyControl object named permissiveModify and add it to the DirectoryControlCollection of the modRequest object.
  4. Call the SendRequest method of the connection object and pass it the modRequest object. Cast the directory response into a ModifyResponse object named modResponse.

    Whether or not the attribute and value has been assigned to the user account, the send request will succeed with the help of the permissiveModify directory control object.

Example 5. Adding or replacing the Url multi-valued attribute of a user account

string hostOrDomainName = "fabrikam.com";
string dn = "cn=john doe,ou=techwriters,dc=fabrikam,dc=com";
string attributeName = "url";

String[] attribVals = new String[2];
attribVals[0] = "www.microsoft.com";
attribVals[1] = "msdn.microsoft.com";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

try
{
    // initialize the modifyrequest object. Note the fourth
    // parameter is a string array in this instance
    ModifyRequest modRequest = new ModifyRequest(
        dn, DirectoryAttributeOperation.Add,
        attributeName, attribVals);

    // create the PermissiveModify control
    // to better control modification behavior.
    PermissiveModifyControl permissiveModify =
        new PermissiveModifyControl();

    // add the directory control to the modifyRequest
    modRequest.Controls.Add(permissiveModify);

    // cast the returned directory response into a ModifyResponse 
    // object named modResponse    
    ModifyResponse modResponse =
       (ModifyResponse)connection.SendRequest(modRequest);

    Console.WriteLine("The {0} attribute of {1} added successfully\n" +
        "The server response was {2}. The following values were added:",
        attributeName, dn, modResponse.ResultCode);
    foreach (string attribVal in attribVals)
    {
        Console.WriteLine(attribVal);
    }
}
catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}

Considering the classic example, managing the member attribute of a group, the only changes you would have to make in the previous code example (Example 5) would be:

  1. Change the distinguished name of the object you are modifying to an existing group.
  2. Add one or more distinguished names of security principals, such as user accounts, to the attribute values.
  3. Specify the member attribute of a group.

For instance, the following code snippet shows how you would change the variable declarations in the previous code example to add the John Doe user account to the administrators group:

string hostOrDomainName = "fabrikam.com";
string dn = "cn=administrators,cn=builtin,dc=fabrikam,dc=com";
string attributeName = "member";

String[] attribVals = new String[1];
attribVals[0] = "cn=john doe,ou=techwriters,dc=fabrikam,dc=com";

The code download with this article does not include an example of setting the member attribute as it is very similar to Example 5. However, it does include an example of using the Uri data type with the ModifyRequest constructor. You will typically use this data type when you are performing DSML operations with S.DS.P.

Deleting Attributes

Programming directory services delete operations using ADSI or managed code namespaces like S.DS and S.DS.AD are relatively simple coding tasks and deleting attributes within objects using S.DS.P is no different. You use the ModifyRequest class and pass it a directory object, the Delete value of the DirectoryAttributeOperation enumeration and the name of the attribute you want to delete.

The following code example demonstrates how to delete the Url attribute from a user account object:

  1. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  2. Create a ModifyRequest object and pass it the distinguished name of a user account stored in the dn variable, the Delete value of the DirectoryAttributeOperation enumeration and the lDAPDisplayName of the attribute to delete.
  3. Call the SendRequest method of the connection object and pass it the modRequest object. Cast the directory response into a ModifyResponse object named modResponse.

    If the attribute has been assigned to the user account, the send request will succeed. Otherwise, a DirectoryOperationException will be thrown.

  4. Check the ResultCode property of the directory response object. If the returned result code is equivalent to the NoSuchAttribute value in the ResultCode enumeration, then report that the attribute is not assigned to the object.

    If a DirectoryOperationException occurs, it's probably because the attribute value has not been assigned to the object.

  5. If the LDAP delete request is successfully sent, display the result code from the responding directory server.

Example 6. Deleting the Url attribute from a user account object

string hostOrDomainName = "fabrikam.com";
string dn = "cn=john doe,ou=techwriters,dc=fabrikam,dc=com";
string attributeName = "url";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

try
{
    ModifyRequest modRequest = new ModifyRequest(
            dn, DirectoryAttributeOperation.Delete,
            attributeName);

    ModifyResponse modResponse = 
                     (ModifyResponse)connection.SendRequest(modRequest);

    Console.WriteLine("{1} delete operation sent successfully. " + 
                      "The directory server reports {1}",
                      attributeName, modResponse.ResultCode);
}

catch (DirectoryOperationException doe)
{
    if (doe.Response.ResultCode == ResultCode.NoSuchAttribute)
        Console.WriteLine("{0} is not assigned to this object.", 
            attributeName);
}

catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}

Final Observations about ModifyRequest

As I mentioned earlier, the fourth parameter of ModifyRequest takes an object[] to contain one or more values. An object array can accommodate the wide variety of data types that directory attributes store for directory objects. It's your responsibility to specify the proper data type for an attribute value when you declare it. For an excellent examination of Active Directory syntax and corresponding .NET data types, see The .NET Developer's Guide to Directory Services Programming by Joe Kaplan and Ryan Dunn.

The ModifyRequest class also includes a constructor that allows you to package a series of attribute operations for an object using the DirectoryAttributeModification class. Using this approach, you could add, replace and delete a variety of attributes of an object in a single send request.

The following example demonstrates how you could package an attribute replace operation of a user's givenName and an attribute add operation of a user's Url multi-valued attribute. I've left out all error checking so that you can easily see how to use this ModifyRequest constructor.

  1. Create DirectoryAttributeModification objects for each attribute operation.

    For each DirectoryAttributeModification object you must specify the type of operation: Add, Replace or Delete for the Operation property; the name of the attribute for the Name property; and the Add or AddRange method for a single or an array of values respectively.

  2. Create a DirectoryAttributeModification array and then store the two DirectoryAttributeModification objects created in the previous step.
  3. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  4. Create a ModifyRequest object and pass it the distinguished name stored in the dn variable and the DirectoryAttributeModification array.
  5. Call the SendRequest method of the connection object and pass it the modRequest object.
  6. If the LDAP send request is successful, display the result code from the responding directory server.

Example 7. Using the DirectoryAttributeModification class to send multiple attribute changes to a directory

string hostOrDomainName = "fabrikam.com";
string dn = "cn=john doe,ou=techwriters,dc=fabrikam,dc=com";
string attributeUrl = "url";

string[] attribVals = new String[2];
attribVals[0] = "www.microsoft.com";
attribVals[1] = "msdn.microsoft.com";

string attributeGn = "givenName";
string attribVal = "John";

// create a DirectoryAttributeModification object for 
// adding the url values to the url attribute
DirectoryAttributeModification mod1 = new DirectoryAttributeModification();
mod1.Operation = DirectoryAttributeOperation.Add;
mod1.Name = attributeUrl;
mod1.AddRange(attribVals);

// create a DirectoryAttributeModificaton object for
// replacing the first name stored in the givenName attribute
DirectoryAttributeModification mod2 = new DirectoryAttributeModification();
mod2.Operation = DirectoryAttributeOperation.Replace;
mod2.Name = attributeGn;
mod2.Add(attribVal);

// create a DirectoryAttributeModification array to hold the 
// DirectoryAttributeModification objects
DirectoryAttributeModification[] mods = 
                                     new DirectoryAttributeModification[2];

// add each DirectoryAttributeModification object to the array
mods[0] = mod1;
mods[1] = mod2;

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

// pass the DirectoryAttributeModification array as the second parameter
// of the ModifyRequest object
ModifyRequest modRequest = new ModifyRequest(dn, mods);

// cast the directory response as a ModifyResponse object named modResponse
ModifyResponse modResponse =
       (ModifyResponse)connection.SendRequest(modRequest);

Console.WriteLine("The result was: {0}", modResponse.ResultCode);

Note that this code example is exceptionally brittle as there is no error checking included. In addition, because you are packaging multiple directory attribute operations, the conditions in the directory must be just right or the entire send request will fail. For example, if the Url attribute contains one of the two values that the add operation is attempting to insert, the replace operation on the givenName and the add operation on the Url will fail. You can also make the code less likely to fail by adding the PermissiveModifyControl to the DirectoryControlCollection of the modRequest object. See Example 5 for an example of using this control.

An effective code example using the DirectoryAttributeModification class would require a significant number of command line parameters so I have not included an example of this in the code download. Use the previous code example (Example 7) as a starting point for packaging multiple attribute change operations against a directory object.

Examining Attribute Values

Some of the previous examples of managing attributes make assumptions about the state of the attribute, for example, whether a certain value is present or not. One way to avoid making this assumption is by inspecting an attribute to determine its value. The CompareRequest and CompareResponse classes provide this facility by allowing you to compare an attribute's value to a value you supply. Using the directory response returned from a CompareRequest operation, you can then make decisions on how you might want to manipulate the attribute.

The following code example demonstrates how to use the CompareRequest and CompareResponse classes to compare the userAccountControl attribute to a given value before attempting to modify the attribute value.

  1. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  2. Create a DirectoryAttribute object and pass it the userAccountControl attribute and a value of 546.

    The second bit of the userAccountControl attribute should be off to enable the user account. A value of 546 is the default value for the userAccountControl attribute immediately after a user account is created using the CreateUsers method in the code download. The CreateUsers method is similar to the code appearing in Example 1. A value of 546 means that the account is disabled because the second bit is on.

    Following this code example, I'll explore a more accurate way to represent the underlying data type of the userAccountControl attribute. In this example, it's represented as a string, but it's stored in the directory as an integer. For now, representing it as a string simplifies the code example.

  3. Create a CompareRequest object named compRequest and pass it the distinguished name of a user account stored in the dn variable and a directory attribute stored in the userAccountControl object.
  4. Call the SendRequest method of the connection object and pass it the compRequest object.
  5. Cast the returned directory response object into a CompareResponse object named compResponse.
  6. Compare the ResultCode property in compResponse with the CompareTrue value of the ResultCode enumeration.

    If the test is true (CompareTrue) then the userAccountControl attribute in the tested user account is equal to 546.

  7. Use a ModifyRequest object and pass it the distinguished name of the user account, the Replace value of the DirectoryAttributeOperation enumeration, the name of the attribute to change (userAccountControl) and the new value (544).
  8. Cast the returned directory response into a ModifyResponse object and return the ResultCode property to determine if the replace request was successful.
  9. If the compResponse ResultCode property returns CompareFalse, report that the account is already enabled.
  10. If any other ResultCode is returned, display the result code.

Example 8. Enabling a disabled user account

string hostOrDomainName = "fabrikam.com";
string dn = "cn=john doe,ou=techwriters,dc=fabrikam,dc=com";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

// create a DirectoryAttribute object for the 
// userAccontControl attribute 
DirectoryAttribute userAccountControl =
    new DirectoryAttribute("userAccountControl", "546");

try
{
    // create a CompareRequest object and pass it the distinguished name
    // of a directory object and the attribute to compare
    CompareRequest compRequest =
        new CompareRequest(dn, userAccountControl);

    // cast the returned directory response into a CompareResponse object 
    CompareResponse compResponse =
         (CompareResponse)connection.SendRequest(compRequest);

    if (compResponse.ResultCode.Equals(ResultCode.CompareTrue))
    {
        Console.WriteLine("The account is currently disabled." +
                            " The result code is: {0}",
                            compResponse.ResultCode);

        ModifyRequest modRequest = new ModifyRequest(
                            dn, DirectoryAttributeOperation.Replace,
                            "userAccountControl", "544");

        ModifyResponse modResponse =
                   (ModifyResponse)connection.SendRequest(modRequest);

        Console.WriteLine("Modification of userAccountControl for" +
                            " user{0} returned {1}\n" +
                            "The account is now enabled.",
                            dn, modResponse.ResultCode);

    }
    else if (compResponse.ResultCode.Equals(ResultCode.CompareFalse))
    {
        Console.WriteLine("The account is already enabled." +
                            " The result code is: {0}",
                            compResponse.ResultCode);
    }
    else
    {
        Console.WriteLine("The directory server reported {0}",
                            compResponse.ResultCode);
    }

}

catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}

As I mentioned in the code walkthrough for the CompareRequest/CompareResponse example, representing userAccountControl as a string isn't an accurate depiction of the attribute syntax in the directory. The userAccountControl attribute in the directory is stored as an integer. The integer is evaluated by Active Directory as a bit mask for configuring various settings of a user account, including whether the account is enabled or disabled. Therefore, while the previous example demonstrates how simple it is to compare and set the value as a string, it's more accurate to represent the code value as an integer. The following code snippet shows how to use an integer variable to represent the userAccountControl attribute and then perform a bitwise AND to set the second bit of the variable to 0:

// create a 32 bit integer containing the default value of the userAccountControl attribute of a
// user account when it is initially created with the LDAPManagement.CreateUsers method
uint uAC = 546;

// store the starting value of userAccountControl as a string array because the attribute
// compare operation expects to compare a string value with the value in the underlying directory
string[] uACStartingVal =
    new string[] { uAC.ToString() };

// clear bit 2 of uAC to represent an enabled user account
uAC &= 0xFFFFFFFD;

// make a string array to store the value, which is what SendRequest
// transmits to the directory server
string[] uACEndVal = 
    new string[] { uAC.ToString() };

The code download shows a complete example using this code snippet for manipulating the userAccountControl attribute.

If you've worked with ADSI methods (i.e., Get and GetEx in the IADs core interface) and DS properties (i.e., Contains in the PropertyCollection class), you know that these approaches allow you to quickly determine whether a value or values are present in an attribute. In both cases, ADSI runs a search operation to return attribute values. Later in this paper, I'll demonstrate how you can perform a similar operation using the S.DS.P SearchRequest, SearchResponse and SearchResultEntry classes.

Deleting an Object from a Directory

Deleting a directory object is even simpler than deleting an attribute of an object. S.DS.P includes the DeleteRequest and DeleteResponse classes to perform and report on the result of a delete operation. The following example demonstrates how to delete a user account object from a directory:

  1. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  2. Create a new DeleteRequest object named delRequest and pass it the distinguished name of the user account to delete.
  3. Cast the returned directory response into a DeleteResponse object and display the result code of the requested delete operation.

Example 9. Deleting a user account object from a directory

string hostOrDomainName = "fabrikam.com";
string dn = "cn=john doe,ou=techwriters,dc=fabrikam,dc=com";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

try
{

    // create a deleterequest object 
    DeleteRequest delRequest = new DeleteRequest(dn);

    // cast the returned directory response into a DeleteResponse object 
    DeleteResonse delResponse = 
                     (DeleteResponse)connection.SendRequest(delRequest);

    // display the result of the delete operation
    Console.WriteLine("The request to delete {0} was sent" + 
        " successfully.\nThe server response was {1}",
        dn, delResponse.ResultCode);

}
catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}

Moving and/or Renaming a Directory Object

Performing move or rename operations using S.DS.P is almost as simple as an object delete operation. In this case you use the ModifyDNRequest and ModifyDNResponse classes for performing and reporting on the requested operation. The ModifyDNRequest object takes the distinguished name of the object, the distinguished name of the container where you want to move the object and a new object name.

If you aren't moving the object, the distinguished name of the parent container is the current name. For example, if a user account named User10 resides in the TechWriters OU and you want to rename the user account to User11 but keep the user account in the TechWriters OU, then the second parameter in the ModifyDNRequest is the distinguished name of the TechWriters OU while the third parameter is the new relative distinguished name (RDN) you want to assign the user. Similarly, if your goal is to move the user account without renaming it, the second parameter specifies the new OU and the third parameter contains the current relative distinguished name of the user.

The following code example demonstrates how to move and rename a user account:

  1. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  2. Create a ModifyDNRequest object named modDnRequest and pass it the distinguished name of the user account to move, the new parent container where you want to move the object and a new RDN for the object.
  3. Send the directory request and cast the returned response into a ModifyDNResponse object named modDnResponse.
  4. Display the result of the requested operation.

Example 10. Moving and renaming a user account

string hostOrDomainName = "fabrikam.com";
string dn = "cn=user10,ou=techwriters,dc=fabrikam,dc=com";
string newParentDn = "ou=techeditors,dc=fabrikam,dc=com";
string newObjectName = "cn=user11";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

try
{
    // create a ModifyDNRequest object 
    ModifyDNRequest modDnRequest = 
                     new ModifyDNRequest(dn, newParentDn, newObjectName);

    // cast the returned directory response into a ModifyDNResponse object
    ModifyDNResponse modDnResponse = 
                  (ModifyDNResponse)connection.SendRequest(modDnRequest);

    Console.WriteLine("The {0} was moved successfully to\n" +
        "{1} with the object name of {2}.\n" +
        "The server response was {3}",
        dn, newParentDn, newObjectName, modDnResponse.ResultCode);

}
catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}

Search Operations

While the patterns you saw in the Management Tasks section of this paper repeat themselves in this section, search operations are so fundamental to directory services programming that they warrant the beginning of a new section. To further emphasize their importance, the directory services programming team has provided search capabilities using S.DS.P that were previously unavailable via COM automation interfaces. Note that all of these operations can be performed using S.DS, but S.DS.P gives you more control over complex operations such as asynchronous search queries.

If you're already familiar with writing search operations using the DirectorySearcher class in S.DS, using ADSI OLEDB or by using the IDirectorySearch interface from C/C++ you will quickly see a familiar approach to running a search. In essence, you bind to a directory location, specify a base distinguished name for a search, create an LDAP search filter (note that a search filter isn't necessary when using the IDirectorySearch interface), scope the query and run it. When the results are returned, you enumerate the result set and perform some action against one or more items in the enumeration, such as displaying results or leveraging other classes to modify items in the result set.

Performing a Simple Search

There are three search classes involved in a simple search operation: SearchRequest, SearchResponse and SearchResultEntry. To run a search, you pass the SendRequest method of the connection a SearchRequest object. Next, you cast the returned result as a SearchResponse object. The Entries property of the SearchResponse object contains a SearchResultEntryCollection object. You enumerate this object using the SearchResultEntry class.

The following example demonstrates how to search the Builtin container to return the distinguished names of all objects in the container:

  1. Declare and initialize variables, including a search filter that returns all objects.

    While it might seem unnecessary to initialize a search filter that doesn't actually filter anything (objectClass=*), it is necessary. Passing the SearchRequest class a null value causes the compile to fail and while you can declare an empty string for a search filter, the directory server will throw an LdapException stating that the search filter is invalid.

    If you aren't familiar with the LDAP dialect for creating a search filter, refer to the "LDAP Dialect" topic and the "Search Filter Syntax" topic in the Directory Services SDK (part of the platform SDK). SQL search Syntax is not supported in S.DS.P.

  2. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  3. Create a SearchRequest object named searchRequest and pass it the base distinguished name for the search (stored in the targetOU variable), the LDAP search filter, a scope of OneLevel from the SearchScope enumeration and any attributes to return. To keep this example simple, the fourth parameter passes a null value to searchRequest, which is equivalent to specifying no attributes. As a result, searchRequest returns all attributes associated with each directory object.
  4. Call the SendRequest method of the connection object and cast the returned directory response as a SearchResponse object named searchResponse.
  5. Enumerate the Entries property of the searchResponse and display the entry number and the DistinguishedName property of the entry.

    The Entries property contains a SearchResultEntryCollection object.

Example 11. Performing a simple search to return the distinguished name of each object in the Builtin container

string hostOrDomainName = "fabrikam.com";
string targetOu = "cn=builtin,dc=fabrikam,dc=com";

// create a search filter to find all objects
string ldapSearchFilter = "(objectClass=*)";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

Console.WriteLine("\r\nPerforming a simple search ...");

try
{
    SearchRequest searchRequest = new SearchRequest
                                    (targetOu,
                                      ldapSearchFilter,
                                      SearchScope.OneLevel,
                                      null);

    // cast the returned directory response as a SearchResponse object
    SearchResponse searchResponse = 
                (SearchResponse)connection.SendRequest(searchRequest);

    Console.WriteLine("\r\nSearch Response Entries:{0}",
                searchResponse.Entries.Count);

    // enumerate the entries in the search response
    foreach (SearchResultEntry entry in searchResponse.Entries)
    {
        Console.WriteLine("{0}:{1}", 
            searchResponse.Entries.IndexOf(entry), 
            entry.DistinguishedName);    }
}
catch (Exception e)
{
    Console.WriteLine("\nUnexpected exception occured:\n\t{0}: {1}",
                      e.GetType().Name, e.Message);
}

Returning Attribute Values

Each SearchResultEntry contains a number of properties. In the previous code example, I only show how to display the DistinguishedName property of the object. However, using the Attributes property of the SearchResultEntry, you can return one or more values of other attributes in the directory object.

By passing null as the fourth parameter in the construction of a SearchRequest object, as shown in Example 11, each SearchResultEntry contains all non-constructed attributes of the returned objects. Constructed attributes are not stored in the directory, but are calculated by directory servers when requested. For performance and code clarity, you will usually want to limit the number of attributes returned. To do this, create a string array containing the lDAPDisplayNames of the attributes you want returned for each SearchResultEntry and then pass that into the SearchRequest constructor, as shown in this code snippet:

// specify attributes to return
string[] attributesToReturn = new string[] { "givenName", "sn", "description", "cn", "objectClass" };

// create a SearchRequest object and specify baseDn,  ldap search filter, attributes to return and 
// search scope. Note, the targetOu and ldapSearchFilter variables are strings whose values do
// not appear in this code example.
SearchRequest searchRequest = new SearchRequest(targetOu, ldapSearchFilter, SearchScope.Subtree,
                                                                          attributesToReturn);

Once you have defined which attributes you want to return, you then have to work with the return types contained in the Values property of the SearchResultAttributeCollection. The two recommended ways to do this is either use the DirectoryAttribute indexer or the GetValues method of a DirectoryAttribute to return a specific type. Even though attributes are stored in a variety of types referred to as attribute syntax, S.DS.P always attempts to convert each value it retrieves into a string; otherwise it returns a byte array. Therefore, if you know the return type of a specific attribute value you want to return, use GetValues. If not, use the indexer and test the return type prior to displaying the values.

If you're not familiar with attribute syntax, you can get a handle on it by reading The .NET Developer's Guide to Directory Services Programming by Joe Kaplan and Ryan Dunn. In addition, review the ActiveDirectorySyntax enumeration in the .NET Framework Class Library.

The code download with this paper includes an example of how to selectively return and display attribute names and values. The key differences between the code download and the prior code example (Example 11) are:

  • How you specify the subset of attributes you want to return.
  • How you display the values contained in these attributes.

The following code example is part of the AttributeSearch method in the code download. It emphasizes how you return attribute values using the DirectoryAttribute indexer once you have obtained a set of objects using the Entries property of the SearchResultEntryCollection:

  1. Enumerate each SearchResultEntry contained in the SearchResultEntryCollection object.
  2. Display the entry number and the DistinguishedName property of each entry (returned directory object).
  3. Enumerate each DirectoryAttribute contained in the Values property of the SearchResultAttributeCollection.
  4. Display the Name property of the current attribute being enumerated.
  5. Use a for loop to enumerate each value within the attribute.

    The attribute.Count property contains a count of all values with an attribute. A single-valued attibute will contain exactly one entry while a multi-valued attribute can contain one or more entries. Using this count, the code then uses the DirectoryAttribute indexer to return the value.

  6. If the return type is a string, write the value to the screen.

    Notice that if the count is equivalent to 1, then the attribute is written to the screen immediately following the attribute name. Otherwise, tabs are added to move the multiple values off the console's left margin.

  7. If the return type is a byte array, determine how many values are stored in the attribute. If there is only one value, then the Count property will be equivalent to 1.
  8. If there is more than one attribute, then use the innerCount variable to determine whether this is the first time through this loop.

    If this is the first time through the loop, display a message indicating that the returned values are byte arrays; otherwise just display the byte array data.

  9. Call a helper method named ToHexString to attempt to convert the returned value into a hex string format for display.

    This helper method appears in the code download within the LDAPCommon class. This is not part of S.DS.P but is useful when you need to convert a byte array, such as a SID value, to a hex string.

Example 12. How to display values of attributes returned in a SearchResultEntryCollection

foreach (SearchResultEntry entry in searchResponse.Entries)
{
    Console.WriteLine("\n{0}:{1}",
        searchResponse.Entries.IndexOf(entry),
        entry.DistinguishedName);
    SearchResultAttributeCollection attributes = entry.Attributes;

    foreach (DirectoryAttribute attribute in attributes.Values)
    {
        Console.Write("{0} ", attribute.Name);

        if (attribute.Count != 1)
        {
            Console.WriteLine();
        }
        // used to track where we are in the loop when displyaing 
        // byte arrays stored in multi-valued attributes
        int innerCount = 0;
        // count the number of values associated with this attribute
        for (int i = 0; i < attribute.Count; i++)
        {
            if (attribute[i] is string)
            {
                if (attribute.Count == 1)
                { 
                    Console.WriteLine("{0}", attribute[i]); 
                }
                else
                {
                    Console.WriteLine("\t\t{0}", attribute[i]);
                }
            }
            else if (attribute[i] is byte[])
            {
                if (innerCount == 0)
                {
                    Console.WriteLine("is a byte array. " +
                        "Converting value to a hex string.");
                }
                if (attribute.Count == 1)
                {
                    Console.WriteLine(
                        LDAPCommon.ToHexString((byte[])attribute[i]));
                }
                else
                {
                    Console.WriteLine(
                        LDAPCommon.ToHexString((byte[])attribute[i]));
                    innerCount++;
                }
            }
            else
            {
                Console.WriteLine("Unexpected type for attribute value:{0}",
                    attribute[i].GetType().Name);
            }
        }
    }
}

You might ask, "Why bother using the GetValues method to return values when I can simply use the DirectoryAttribute indexer shown in the previous example?" As I mentioned earlier in this section, you can use the GetValues method when you know the type of data you want to retrieve, either string or byte array. There are two distinct advantages GetValues gives you:

  1. It provides better performance because you don't need to check the return type.
  2. It handles the rare instance where S.DS.P returns some values in a multi-valued attribute as string data while others are returned as byte array data.

To reinforce the second advantage of the GetValues method, consider the tokenGroups attribute of a user account. If you were to use the DirectoryAttribute indexer to return the tokenGroups value of a user account, you could see output similar to the following:

This output shows that S.DS.P always attempts to convert values to strings when a conversion is possible. When it isn't possible, byte values are returned. Specifically, S.DS.P successfully returned the first three objectSID values in the tokenGroups attribute as strings while the remaining two were returned as byte arrays.

Note that if you want to test these results using the AttributeSearch method in the code download, you must set the SearchScope to Base to successfully return the tokenGroups attribute.

The following code example shows how you use the GetValues method to return the tokenGroups attribute of the user account:

  1. Declare and initialize variables, including a search filter that limits the search to user account objects.
  2. Create an LdapConnection object for connecting to a domain controller in the fabrikam.com domain.
  3. Create a SearchRequest object named searchRequest and pass it the distinguished name of the user account for the search (stored in the targetUser variable), the LDAP search filter, a scope of Base from the SearchScope enumeration and an lDAPDisplayName of tokenGroups to return the attribute from the search.

    To successfully return the tokenGroups attribute from the search, you must set the SearchScope to Base.

  4. Call the SendRequest method of the connection object and cast the returned directory response as a SearchResponse object named searchResponse.
  5. Enumerate the Entries property of the searchResponse and display the distinguishedName of the user account object.
  6. Enumerate each DirectoryAttribute contained in the Values property of the SearchResultAttributeCollection.
  7. Call the GetValues method of the DirectoryAttribute object and return the value as an object array named values.

    To instruct the GetValues to return a byte array, you pass the method the type you want returned. You achieve this by calling the GetType method of the Type class and pass the method the name of the type you want returned. In this case, you want to return a byte array, so the code specifies the System.Byte[] type.

  8. Enumerate the values object array and call the helper method named ToHexString to convert the return value into a hex string format for display.

Example 13. Performing a search to return the tokenGroups attribute using the GetValues method

string hostOrDomainName = "fabrikam.com";
string targetUser = "cn=Jane,ou=TechWriters,dc=fabrikam,dc=com";

// create a search filter to limit the results to user account objects
string ldapSearchFilter = "(&(objectCategory=person)(objectClass=user))";

// establish a connection to the directory
LdapConnection connection = new LdapConnection(hostOrDomainName);

Console.WriteLine("\r\nPerforming an attribute search of tokenGroups...");

// create a SearchRequest object and specify the dn of the target user account, 
// ldap search filter, a Base search scope, and the tokenGroups attribute
SearchRequest searchRequest = new SearchRequest
                                        (targetUser,
                                         ldapSearchFilter,
                                         SearchScope.Base,
                                         "tokenGroups");

// cast the returned directory response as a SearchResponse object
SearchResponse searchResponse =
    (SearchResponse)connection.SendRequest(searchRequest);

Console.Write("\ntokenGroups in: ");

foreach (SearchResultEntry entry in searchResponse.Entries)
{
    Console.WriteLine("{0}", entry.DistinguishedName);

    SearchResultAttributeCollection attributes = entry.Attributes;

    foreach (DirectoryAttribute attribute in entry.Attributes.Values)
    {

        object[] values = attribute.GetValues(Type.GetType("System.Byte[]"));


        for (int i = 0; i < values.Length; i++)
        {
            Console.WriteLine(
                   LDAPCommon.ToHexString((byte[])values[i]));
        }

    }
}

In contrast to the earlier example showing how the tokenGroups attribute is displayed using the DirectoryAttribute indexer, the following output shows how GetValues returns the results in a readable format:

Note that to make this example more useful, you should search for the corresponding friendly name of the returned groups or use the DsCrackNames API that Ryan Dunn describes in his blog at http://dunnry.com/blog/DsCrackNamesInNET.aspx. Ryan and Joe also cover approaches for converting values in the tokenGroups attribute into user friendly names in The .NET Developer's Guide to Directory Services Programming.

Running a Paged Search

Search operations that return a large result set in a single response can consume a lot of memory on the directory server responding to the request and will fail if the size of the requested result set is larger than the size limit configured for the responding directory server. If a search request is processed successfully by the server, sending the results to the client making the request can cause a spike in network utilization. Another negative consequence is poor response time on the client awaiting the large result set. In order to throttle both directory server memory utilization and network bandwidth and ensure that you don't exceed server-side result set size limits, you can code a search operation to return results in chunks or pages.

The key classes for running a paged search operation are:

  • SearchRequest   Passed to the SendRequest method of the connection object to run a search operation.
  • PageResultRequestControl   Added to the controls collection of the search request to enable a paged search operation on the directory server.
  • SearchResponse   Added to cast the directory response from the search request as a search re