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 https://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.

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.

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 response object.
  • PageResultResponseControl   Added to the controls collection of the search response to enable paging the search response.

To enable paging using S.DS.P, you must assign a PageResultRequestControl object to a SearchRequest object. After creating a SearchRequest object, you create a PageResultRequestControl object and add it to the directory control collection of a SearchRequest object, as this code snippet demonstrates:

PageResultRequestControl pageRequest =  new PageResultRequestControl(pageSize);
searchRequest.Controls.Add(pageRequest);

You are then ready to send the search to the server using the connection object's SendRequest method. After casting the returned directory response into a SearchResponse, you should verify that the directory server can support paging. This is important because not all directory servers support paging. If paging is supported, the directory control object will be contained in the directory control array of the SearchResponse and will contain exactly one control. The following conditional check will verify that the PageResultResponseControl object is present. If not, the code will exit:

if (searchResponse.Controls.Length != 1 ||
                !(searchResponse.Controls[0] is PageResultResponseControl))
{
    Console.WriteLine("The server cannot page the result set");
    return;
}

If a server can return results in pages, the next step is to cast the returned directory control into a PageResultResponseControl directory control type. This is similar to the pattern you follow to cast a directory response into a specific type of response object.

PageResultResponseControl pageResponse =
                (PageResultResponseControl)searchResponse.Controls[0];

The PageResultResponseControl object contains an opaque cookie used by the DirectoryServer to determine which page of data needs to be returned to the client. After returning the first page of data, you set the PageResultRequestControl Cookie property equal to the value of the PageResultResponse.Cookie, as shown:

pageRequest.Cookie = pageResponse.Cookie;

You then initiate another send request operation with the search request object containing an updated pageRequest.

SearchResponse searchResponse =  (SearchResponse)connection.SendRequest(searchRequest);

This instructs the server to request the next page of results in the search request operation. If you're familiar with using cursors to move from record to record in a database, this is similar to how the directory server uses the response cookie to retrieve the next page in the result set. As I walk through the code, I'll explicitly call out where the search request is being updated.

The following example demonstrates how to run a paged search operation to return up to 5 distinguishedName entries per page of the objects inside of an OU:

  1. Declare a pageSize variable and initialize it to 5.

    Later in this code example, pageSize is passed to the PageResultRequestControl object to instruct the directory server to return up to 5 results per page. Unless the entire result set is divisible by 5, the final page will contain fewer than 5 entries.

  2. Declare and initialize a pageCount variable.

    Later in this code example, pageCount tracks which page was returned.

  3. Construct a SearchRequest object named searchRequest.

    Just as you would for the prior search operations explained in this section of the paper, you construct a SearchRequest object by passing the starting distinguished name (base DN) for the search, an LDAP search filter and a search scope. While not necessary for this example, you can also specify one or more attributes to return.

  4. Create a PageResultRequestControl object named pageRequest and pass it the size of each page for the subsequent search request operation.

  5. Add the pageRequest object to the directory control collection of the searchRequest object.

  6. Create a SearchOptionsControl object named searchOptions and pass it the DomainScope value of the SearchOption enumeration.

    This control allows you to enable or disable two aspects of a search request: referral chasing and whether to allow subordinate referrals in a search. For more information on either of these topics, see the Referrals topic and the ADS_CHASE_REFERRALS_ENUM in the Directory Services Platform SDK.

    Instead of using the SearchOptionsControl, you can use the ReferralChasing property of the LdapSessionOptions class, as the following code snippet shows:

    LdapSessionOptions options = connection.SessionOptions;
    options.ReferralChasing = ReferralChasingOptions.None;
    

    If you choose this alternative approach, you can remove the code that creates and adds the SearchOptionsControl object to the Controls collection of the searchRequest object.

  7. Begin requesting and returning paged search results using a while loop.

    The while loop will be true as long as there are remaining pages for the server to return. As you'll see later in the code, this will be true until the length of the pageResponse cookie is 0.

  8. Increment the pageCount variable.

    The pageCount variable is initially set to 0. Each time the code returns a page, pageCount displays the page number.

  9. Cast the directory response returned by the connection object's SendRequest method as a SearchResponse object named searchResponse.

  10. Test whether the length of the searchResponse object's directory control array is not 1 or if the first control in the array is not a PageResultResponseControl object. If either condition is true, exit the code.

    This test determines whether the directory server responding to the request can actually return results in pages. If either condition is true, then the directory server does not support paging. As you'll see in a later example, conditional tests similar to this one are important for other advanced search operations.

    In addition, you might have noticed that two directory controls were added to the searchRequest: PageResultRequestControl and the SearchOptionsControl. However, the searchResponse only contains one response control. This is because there is no reason to return a directory response control for the SearchOptionsControl directory request control.

  11. Cast the directory control in the searchResponse object into a PageResultResponseControl object named pageResponse.

  12. Display the current page number and the number of entries with the returned page.

  13. Using a foreach loop, enumerate each result in the search result entry collection and display the entry number and the distinguished name of the entry within the search page.

    Use the IndexOf method of the SearchResultEntry to display the index of the returned result. I add 1 to the displayed value since this is a zero-based index.

  14. Check if the pageResponse object's cookie length is 0. If so, the last page of the search result has been returned so break out of the while loop.

  15. If the while loop continues, set the pageRequest cookie equal to the pageResponse cookie.

    The cookie in the pageResponse is an internal data structure the server uses to determine the next page to return as a result of a search request. When the code begins the next loop, the searchRequest object passed to the SendRequest method now contains the value of the cookie passed from the pageResponse object to the pageRequest object.

Example 14. Performing a paged search of the TechWriters OU to return up to 5 entries within each page

string hostOrDomainName = "fabrikam.com";
string startingDn = "ou=techwriters,dc=fabrikam,dc=com";

// for returning up to 5 entries in each page
int pageSize = 5;
// for tracking the pages returned by the search request
int pageCount = 0;

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

try
{
    Console.WriteLine("\nPerforming a paged search ...");

    // this search filter does not limit the returned results
    string ldapSearchFilter = "(objectClass=*)";

    // create a SearchRequest object
    SearchRequest searchRequest = new SearchRequest
                                    (startingDn,
                                     ldapSearchFilter,
                                     SearchScope.Subtree,
                                     null);

    // create the PageResultRequestControl object 
    // pass it the size of each page.
    PageResultRequestControl pageRequest =
        new PageResultRequestControl(pageSize);

    // add the PageResultRequestControl object to the
    // SearchRequest object's directory control collection 
    // to enable a paged search request
    searchRequest.Controls.Add(pageRequest);

    // turn off referral chasing so that data from other partitions is
    // not returned. This is necessary when scoping a search
    // to a single naming context, such as a domain or the 
    // configuration container
    SearchOptionsControl searchOptions =
        new SearchOptionsControl(SearchOption.DomainScope);

    // add the SearchOptionsControl object to the
    // SearchRequest object's directory control collection 
    // to disable referral chasing
    searchRequest.Controls.Add(searchOptions)

    // loop through the pages until there are no more 
    // to retrieve
    while (true)
    {
        // increment the pageCount by 1
        pageCount++;

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

        // verify support for this advanced search operation
        if (searchResponse.Controls.Length != 1 ||
            !(searchResponse.Controls[0] is PageResultResponseControl))
        {
            Console.WriteLine("The server cannot page the result set");
            return;
        }

        // cast the diretory control into 
        // a PageResultResponseControl object.
        PageResultResponseControl pageResponse =
            (PageResultResponseControl)searchResponse.Controls[0];

        // display the retrieved page number and the number of 
        // directory entries in the retrieved page                    
        Console.WriteLine("\nPage:{0} contains {1} response entries",
                    pageCount, searchResponse.Entries.Count);

        // display the entries within this page
        foreach (SearchResultEntry entry in searchResponse.Entries)
        {
            Console.WriteLine("{0}:{1}",
                              searchResponse.Entries.IndexOf(entry) + 1,
                              entry.DistinguishedName);
        }

        // if this is true, there 
        // are no more pages to request
        if (pageResponse.Cookie.Length == 0)
            break;

        // set the cookie of the pageRequest equal to the cookie 
        // of the pageResponse to request the next page of data
        // in the send request
        pageRequest.Cookie = pageResponse.Cookie;
    }
    Console.WriteLine("\nPaged search completed.");
}

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

Returning search results in pages is an effective way to improve the performance of search operations containing large result sets. The server can more effectively handle other tasks while addressing the relatively small search page requests. Client-side, results display much more quickly in pages than when awaiting a large result set from a single send request operation. However, while a synchronous method runs on the client, the code blocks until the method completes. By running search requests asynchronously, long running segments of code within a method do not block other methods from running.

To run an asynchronous search operation, you create a SearchRequest object just as you've seen in previous examples. However, instead of calling the SendRequest method of the connection object, you call the BeginSendRequest method to perform an asynchronous request to a directory server responding to the request. Here's an example of how you code a BeginSendRequest:

IAsyncResult asyncResult = connection.BeginSendRequest(
                                             searchRequest,
                                             PartialResultProcessing.ReturnPartialResultsAndNotifyCallback,
                                             RunAsyncSearch,
                                             null);

The BeginSendRequest method is overloaded. Both constructors require that you provide a DirectoryRequest object. In the case of an asynchronous search, you pass BeginSendRequest method a SearchRequest object (called searchRequest in the code snippet).

You also specify a value for the PartialResultProcessing enumeration when calling BeginSendRequest. This enumeration describes how results should be returned, either with no support for returning partial results (NoPartialResultSupport value) or by returning partial results (the ReturnPartialResults and ReturnPartialResultsAndNotifyCallback values). As the .NET Framework Class Library recommends, you should use NoPartialResultSupport for performance and scalability of most asynchronous search operations. The documentation mentions that partial result support is particularly useful when a search operation takes a long time to complete. For example, by performing search operations that use the DirectoryNotificationControl to return changes in the directory while a search operation is running. I demonstrate in the code download and here how to return partial results using an asynchronous callback mechanism. Note that using ReturnPartialResultsAndNotifyCallback can cause high CPU utilization. This issue is explained in the following KB: 918995 at https://support.microsoft.com/kb/918995/en-us. There is a fix referenced in the KB if you experience this issue.

To perform an asynchronous send request, you must specify a callback delegate to handle the search request (called RunAsyncSearch in the previous code snippet). This delegate runs the search operation. In the code, you pass the IAsyncResult interface to the callback delegate. Within the delegate, you use this interface to determine whether or not the asynchronous operation has completed or not.

The last parameter of both BeginSendRequest methods is an object that contains state information for the operation. You can use this parameter to distinguish this asynchronous request from other requests that might be running. Finally, one overload of BeginSendRequest contains a request time out parameter. By default, the request times out in 2 minutes unless you change the request time out. If you want the connection to run for longer, you can either specify the request timeout in your call to BeginSendRequest or in the TimeOut property of the connection, as I show here:

connection.Timeout = new TimeSpan(0, 3, 30);

This value specifies that the connection shouldn't time out for 3½ minutes. This setting is important for long running asynchronous search operations.

The following code example demonstrates how to set up the search request and call the BeginSendRequest operation. It uses a DirectoryNotificationControl to track changes occurring in a directory. Following this code example, I explore the asynchronous callback delegate:

  1. Declare and initialize variables for the asynchronous search operation.

    Notice that the startingDn is to an OU in the fabrikam domain. You could start at the root of the domain if you're interested in seeing all directory changes occurring at the root of the domain and below. Because the search scope is Subtree, the search operation returns changes starting at the specified startingDn and below.

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

  3. Create a SearchRequest object named searchRequest.

  4. Add a DirectoryNotificationControl directory control to the searchRequest object's Controls collection.

    This control instructs the directory server to watch for changes in the directory. When a change is detected, the server returns a search result.

    While this particular control isn't necessary to run most asynchronous searches, it's really useful here so that you can see how to track changes to a directory asynchronously. Typical search requests return results as soon as possible. In contrast, the search runs as long as the connection timeout hasn't been reached. As you'll see later, to test this example you make a change, such as disabling a user account, and the search will return the distinguished name of the disabled directory object.

  5. Set the Timeout property of the connection object to 3½ minutes.

    The Timeout property is a TimeSpan type.

  6. Call the connection object's BeginSendRequest method and pass it the search request, the ReturnPartialResultsAndNotifyCallback value of the PartialResultProcessing enumeration, and the name of the asynchronous call back delegate. Store the return value in the IAsyncResult interface named asyncResult.

Example 15. Setting up an asynchronous search operation using the BeginSendRequest method

string hostOrDomainName = "fabrikam.com";
string startingDn = "ou=techwriters,dc=fabrikam,dc=com";
string ldapSearchFilter = "(objectClass=*)";

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

SearchRequest searchRequest = new SearchRequest(
                                                startingDn,
                                                ldapSearchFilter,
                                                SearchScope.Subtree,
                                                null);

// this directory control allows the server to watch for changes to 
// objects in the directory
searchRequest.Controls.Add(new DirectoryNotificationControl());

// increase the connection timeout to 3½ minutes
connection.Timeout = new TimeSpan(0, 3, 30);

IAsyncResult asyncResult = 
    connection.BeginSendRequest(
                                searchRequest,
                                PartialResultProcessing.
                                    ReturnPartialResultsAndNotifyCallback,
                                RunAsyncSearch,
                                null);

The asynchronous search request is then handed off to the delegate named RunAsyncSearch and the process is free to perform other operations until the server has data to return.

In the following example, I show the entire delegate so that you can see how the asynchronous callback delegate (RunAsynchSearch) is constructed and passed into the BeginSendRequest method. The RunAsynchSearch delegate receives the asyncResult interface from the BeginSendRequest method. Each time the server has a search result for the method, RunAsynchSearch runs and displays results to the console.

The following code example demonstrates how to create the RunAsynchSearch delegate to display partial or all results from the notifications received from the directory server:

  1. Declare and initialize a PartialResultsCollection object named partialResult.

  2. Call the GetPartialResults method of the connection object and pass in the asyncResult interface to retrieve results asynchronously from the directory server.

    The asyncResult interface contains the state data returned by the directory server.

  3. If there are partial results to display, iterate through the partial results collection. For each result that is identified as a SearchResultEntry, display the distinguished name of the returned entry.

    Notice that you must cast each partialResult to a SearchResultEntry in order to get to the common properties and methods of a directory entry object.

  4. Once the search operation completes, call the connection object's EndSendRequestMethod and pass it the asyncResult interface to complete the asynchronous request.

  5. If there are any results to return following the completed request, display their values.

    Notice that in this case you cast the directory response as a SearchResponse object as you've seen in previous examples. The entries within a search response object are SearchResultEntry objects.

  6. If the server throws a directory operation exception, there might still be results to return. If so, show the error message and then return the search results.

Example 16. The RunAsyncSearch delegate for the asynchronous search operation

// execute the search when the server has data to return)
static void RunAsyncSearch(IAsyncResult asyncResult)
{

    Console.WriteLine("Asynchronous search operation called.");

    if (!asyncResult.IsCompleted)
    {
        Console.WriteLine("Getting a partial result");
        PartialResultsCollection result = null;

        try
        {
            result = connection.GetPartialResults(asyncResult);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        if (result != null)
        {
            for (int i = 0; i < result.Count; i++)
            {
                if (result[i] is SearchResultEntry)
                {
                    Console.WriteLine("A changed just occured to: {0}",
                      ((SearchResultEntry)result[i]).DistinguishedName);
                }
            }
        }
        else
            Console.WriteLine("Search result is null");
    }
    else
    {
        Console.WriteLine("The search operation has been completed.");
        try
        {
            // end the send request search operation
            SearchResponse response =
                (SearchResponse)connection.EndSendRequest(asyncResult);

            foreach (SearchResultEntry entry in response.Entries)
            {
                Console.WriteLine("{0}:{1}",
                    response.Entries.IndexOf(entry),
                    entry.DistinguishedName);
            }
        }
        // in case of some directory operation exception
        // return whatever data has been processed
        catch (DirectoryOperationException e)
        {
            Console.WriteLine(e.Message);
            SearchResponse response = (SearchResponse)e.Response;

            foreach (SearchResultEntry entry in response.Entries)
            {
                Console.WriteLine("{0}:{1}",
                    response.Entries.IndexOf(entry),
                    entry.DistinguishedName);
            }
        }
        catch (LdapException e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

Creating an Attribute Scoped Query (ASQ)

Common search operations return directory objects that are derived from class schema objects. In contrast, an attribute scoped query (ASQ) allows you to search for values within an attribute. The most common use of this feature is to search for members contained in the member attribute of a group object.

To perform an ASQ, you must set your search scope to Base and you must pass a valid attribute name to an AsqRequestControl object. If you don't follow these two rules, an ASQ search operation will return a DirectoryOperationException error informing you that the server doesn't support the control even when this might not be the case.

The following code example demonstrates how to use an ASQ to return the values stored in the member attribute of the Domain Users group:

  1. Declare and initialize variables for the ASQ search operation.

    Notice that the search filter doesn't limit the returned results. You could refine the filter further by returning just group objects with the following search filter:

    string ldapSearchFilter = "(objectClass=group)";
    

    To limit the results to group, user and foreignSecurityPrincipal objects, use the following filter:

    string ldapSearchFilter = "(|(|(objectClass=group)" +
     "(objectClass=foreignSecurityPrincipal)" +
     "(objectClass=user)))";
    

    Notice that the targetGroupObject variable specifies the distinguished name of a group. This is equivalent to the startingDN that I show in earlier code examples. In this case, the query is scoped to a leaf object rather than a container object, such as an OU or the root of a domain.

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

  3. Create a SearchRequest object named searchRequest.

    Notice the Base value of the SearchScope enumeration. You must scope an ASQ to Base because you are searching an attribute within a directory object.

  4. Create an AsqRequestControl object named asqRequest and pass it the name of an attribute within the group object for the subsequent search request operation.

  5. Add the asqRequest object to the directory control collection of the searchRequest object.

  6. Cast the directory response returned by the connection object's SendRequest method as a SearchResponse object named searchResponse.

  7. Test whether the length of the searchResponse object's directory control array is not 1 or if the first control in the array is not an AsqResponseControl object. If either condition is true, exit the code.

    This is the same pattern I demonstrated for the earlier paged search example (Example 14).

  8. Cast the directory control in the searchResponse object into an AsqResponseControl object named asqResponse.

  9. Using a foreach loop, enumerate each result in the search result entry collection and display the entry number and the distinguished name of the entry within the search page.

Example 17. Performing an attribute scoped query to list the members of the Users group

string hostOrDomainName = "fabrikam.com";
string startingDn = "cn=users,dc=fabrikam,dc=com";

// create an open search filter
string ldapSearchFilter = "(objectClass=*)";

// specify a target directory object. This is equivalent to the starting 
// distinguished name of a typical search operation
string targetGroupObject = "cn=users,cn=builtin,dc=fabrikam,dc=com";

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

// perform a search operation to return 
// the members attribute of the specified group object
try
{
    Console.WriteLine("\nPerforming an attribute scoped query");

    // create a SearchRequest object 
    SearchRequest searchRequest = new SearchRequest(
                                            targetGroupObject,
                                            ldapSearchFilter,
                                            SearchScope.Base,
                                            null);

    // create the AsqRequestControl object 
    // and specify the attribute to query
    AsqRequestControl asqRequest =
                            new AsqRequestControl("member");

    // add the AsqRequestControl object to 
    // searchReuest directory control collection. 
    searchRequest.Controls.Add(asqRequest);

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

    // verify that the server supports an attribte scoped query
    if (searchResponse.Controls.Length != 1 ||
            !(searchResponse.Controls[0] is AsqResponseControl))
    {
        Console.WriteLine("The server cannot return ASQ results");
        return;
    }

    // cast the diretory control into 
    // a AsqResponseControl object.
    AsqResponseControl asqResponse =
        (AsqResponseControl)searchResponse.Controls[0];

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

    // list the entries in this page
    foreach (SearchResultEntry entry in searchResponse.Entries)
    {
        Console.WriteLine("{0}:{1}", 
            searchResponse.Entries.IndexOf(entry) + 1, 
            entry.DistinguishedName);

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

Creating a Virtual List View

Thus far, you have seen a number of interesting ways to optimize search operations, by returning results in pages or running a search asynchronously. While these search operations allow you to create high-performance searches, neither approach provides an easy way to return a subset of values from a search operation. Imagine for a moment if every time you ran a search on the Internet, the search engine attempted to return all results to your browser. On a search phrase with a lot of matches, you would likely grind a search engine to a halt and your browser would become non-response. This is an extreme case, but it provides an excellent backdrop for introducing the virtual list view (VLV) search.

VLV allows you to control how many results from a search should be returned to the client. An address book application is a common directory services use for this capability. A user might be interested in returning just the first 10 matches on a last name search or they might want to move through name matches in small chunks. The VlvRequestControl and VlvResponseControl objects provide a structured way to code this kind of operation. Because server-side sorting is required for a VLV search to succeed, you must also make use of the SortRequestControl and SortResponseControl objects.

The trickiest part of getting a VLV search to work is properly setting the parameters you pass to the VlvRequestControl control. The VlvRequestControl constructor's first parameter is the before count, which represents the number of entries to send back before the current entry. The second parameter is the after count, which is the total number of entries to return (zero-based) that match the search request. The third parameter is the target value, which is what you are trying to match for the search. For this parameter, you can pass either a byte array, 32-bit integer or string value.

When you run a search for a target value, you might get back values that don't appear to match. Suppose, for example, you are searching for a last name (surname) of smith and you want to return 10 values. The last few values might not appear to match because they don't begin with smith. However, the results are correct. Results are sorted by the sort control, which returns all values that are greater than or equal to smith. Therefore, even if the next entry in the sorted list doesn't contain a last name starting with smith, it will be returned as the next item in sort order.

There is a lot more to the three VlvRequestControl parameters. For more in-depth coverage, refer to the ADSI SDK (netdir.chm) and The .NET Developer's Guide to Directory Services Programming. Both of these sources have excellent VLV search examples, the former using the LDAP VLV control and the latter using the S.DS DirectoryVirtualListView class. The explanation for the parameters involved in setting up a VLV-based search is perfectly relevant to this example.

The following code example demonstrates how to find the eight closest matches for user accounts with a last name starting with smith.

  1. Declare and initialize variables for the VLV search operation.

    The code uses the valueToSearch variable to specify the target search string for this search operation. The code uses the maxNumberofEntries value to limit the returned results to 10 entries.

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

  3. Create a string array and name it attribs. Store the attribute names in this array that you are interested in returning from the search operation.

    The attribs array is passed to the SearchRequest in the next line of code. While this isn't necessary, it's a good idea to limit the attributes returned by the server to the list you will display in the results. This is true for all search operations, not just this one.

  4. Create a SearchRequest object named searchRequest.

    Notice that attribs is the fourth parameter in this search request.

  5. Create a SortRequestControl object named sortRequest and pass it the name of an attribute for the sort operation and specify that the results should be returned in ascending order.

    All of the directory controls shown thus far contain a ServerSide property. Even though this is the default setting, you might want to explicitly set this property to emphasize that these are server side controls.

  6. Add the sortRequest object to the directory control collection of the searchRequest object.

  7. Create a VlvRequestControl object named vlvRequest and pass it the three required parameters.

    The first parameter is the before count, which is the number of entries to return before the first matching entry in the sorted list. This value must be greater than or equal to 0. The second parameter is the number of entries to return. The numEntries variable was declared as 10 earlier in the code so the code will return a total of 10 results. The third parameter is the target value for the search. The valueToSearch variable was declared as smith* earlier in the code. This will return any last names that start with smith. However, it will not return any values equal to smith. If you increase the before count value then you are likely to return some values equal to smith.

  8. Add the vlvRequest object to the directory control collection of the searchRequest object.

  9. Cast the directory response returned by the connection object's SendRequest method as a SearchResponse object named searchResponse.

  10. Test whether the length of the searchResponse directory control array is not 2 or that the first control is not a SortResponseControl and the second control is not a VlvResponseControl.

    The sortRequest and vlvRequest controls were added to the Controls collection of the searchRequest and were sent to the directory server in the send request. If the server does not return two response controls, or if the two response controls are not the corresponding response directory controls, then exit the code. The example demonstrates how to test a directory server for multiple directory control support. In this case, both the sort and vlv controls are necessary to support a VLV search operation.

  11. Cast the first directory control in the searchResponse object into a SortResponseControl object named sortResponse and the second control into a VlvResponseControl object named vlvResponse.

  12. Display the entry number and distinguished name of each entry returned.

  13. Attempt to return the givenName, sn, cn and telephoneNumber attributes of each entry.

    All but the cn attribute are optional for a user account object. If the code attempts to return a non-existent attribute, it throws a NullReferenceException. In a production code example, you should handle missing attributes more gracefully by testing the presence of the attribute before attempting to return a value from it.

Example 18. Performing a vlv search to return 10 entries starting with a last name (sn) of smith

string hostOrDomainName = "fabrikam.com";
string startingDn = "cn=users,dc=fabrikam,dc=com";

// create a search filter to find all user accounts that are
// security principals. 
string ldapSearchFilter = "(&(objectClass=user)(objectCategory=person))";

// specify the target value for the VLV search request
string valueToSearch = "smith*";

// specify the maximum number of entries to return from the search
string maxNumberofEntries = 10;

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

try
{
    Console.WriteLine("\r\nPerforming a VLV search operation ...");

    // create a string array to hold the attribute names
    // to be passed to the SearchRequest
    string[] attribs = {"cn", "sn", "givenName", "telephoneNumber"};

    // create a SearchRequest object
    SearchRequest searchRequest = new SearchRequest
                                            (startingDn,
                                             ldapSearchFilter,
                                             SearchScope.Subtree,
                                             attribs);

    // create a sortRequest directory control since 
    // VLV requires server-side sorting.
    // Set the name of the attribute for sorting and set
    // the sort order to ascending
    SortRequestControl sortRequest = new SortRequestControl("sn", false);

    // add the sort request to the searchRequest object
    searchRequest.Controls.Add(sortRequest);

    // create VlvRequestControl object named vlvRequest
    // the first parameter is the before count (the number of 
    // entries to send back before the current entry), the 
    // second paramter is the after count (the total number of 
    // entries to return (zero-based) that match the 
    // search request. The third parameter is the target value 
    // (nameToSearch), which is what you are trying to match for
    // this search operation. 
    VlvRequestControl vlvRequest =
        new VlvRequestControl(0, numEntries, valueToSearch);

    // add the vlv request to the searchRequest object
    searchRequest.Controls.Add(vlvRequest);

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

    // verify that there are two controls added to the 
    // searchResponse object's Controls collection
    // then verify that the first control is a
    // SortResponseControl and the second is a 
    // VlvResponseControl
    if (searchResponse.Controls.Length != 2 ||
        !(searchResponse.Controls[0] is SortResponseControl) &
        !(searchResponse.Controls[1] is VlvResponseControl))
    {
        Console.WriteLine("The server does not support VLV");
        return;
    }

    // cast the first directory control as a 
    // SortResponseControl object
    SortResponseControl sortResponse =
        (SortResponseControl)searchResponse.Controls[0];

    // cast the second directory control as a 
    // VlvResponseControl object
    VlvResponseControl vlvResponse =
        (VlvResponseControl)searchResponse.Controls[1];


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

    // Display the entries
    foreach (SearchResultEntry entry in searchResponse.Entries)
    {
        Console.WriteLine("\nEntry {0}: {1}",
            searchResponse.Entries.IndexOf(entry),
            entry.DistinguishedName);

        try
        {
            Console.WriteLine("\tfirstname:\t{0}" +
                        "\n\tlastname:\t{1}" +
                        "\n\taccount name:\t{2}" +
                        "\n\ttelephone:\t{3}",
                entry.Attributes["givenName"][0],
                entry.Attributes["sn"][0],
                entry.Attributes["cn"][0],
                entry.Attributes["telephoneNumber"][0]);
        }
        catch (NullReferenceException)
        {
            Console.WriteLine("name: {0}\n" +
                "either the first name," +
                "last name or phone number isn't available",
                entry.Attributes["cn"][0]);
        }

    }
}

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

Advanced LDAP Server Connection and Session Options

The last two sections explored management and search tasks that you can perform using S.DS.P. Another powerful capability of this namespace that isn't available via COM automation or S.DS is the fine-grained control it provides you in performing advanced authentication and session operations. For example, using S.DS.P and Windows Server 2003 or later, you can perform fast concurrent bind operations. You can also dynamically communicate with a variety of directory servers using transport layer security where part of the communication is encrypted and other parts are not. Finally, S.DS.P allows you to perform certificate-based authentication using client and server certificates. This section explores all of these capabilities.

Binding over a TLS/SSL Encrypted Connection

While the typical forms of authentication Kerberos or NTLM are the preferred approaches for Intranet-based authentication in a Windows network, oftentimes it's necessary to use Basic authentication, especially when servicing external authentication requests.

Unlike Kerberos or NTLM authentication, Basic authentication is not inherently secure unless the channel is encrypted via the Secure Sockets Layer (SSL) and Transport Layer Security (TLS) authentication protocol. You can read more about this authentication protocol and lots of other really useful information about security in The .NET Developer's Guide to Identity by Keith Brown at https://msdn2.microsoft.com/en-us/library/aa480245.aspx#dotnetidm_topic2.

SSL/TLS is also the only approach for authenticating securely to ADAM user accounts in ADAM or ADAM SP1. ADAM in Server 2003 R2 also supports digest authentication but SSL/TLS remains the most common form of secure authentication whether you are running ADAM that supports digest authentication or not.

One way to send data over an encrypted connection to a directory server is by installing a valid certificate so that the directory server can receive the secured transmission over a server specified SSL port. The default SSL port for an Active Directory server is 636. ADAM can use any valid available port that you designate for SSL communication during the installation of an ADAM instance.

You can generate a certificate request using IIS. This request will then be used to generate a certificate. It's important that the host name you provide in the request matches the host name of the directory server responding to the request. For example, if you connect to a directory server named sea-dc-02.fabrikam.com, then the host name in the certificate must match this name. The SSL certificate also specifies a valid from and valid to date. You must be using the SSL certificate in that time period for it to be considered valid.

An invalid certificate will cause any code attempting to connect to a directory server over an SSL port to fail. It's easy to tell if you're using an invalid certificate in IIS because you can evaluate the returned certificate in most Web browsers when you attempt to connect to the Web server using the https moniker. However, it's much harder to tell when an invalid certificate is causing a TLS/SSL connection to a directory to fail. Here are some general approaches for troubleshooting failed connection and binding attempts with certificates:

  1. Using LDP, attempt to connect and bind over the designated secure port to a directory server. You can test both secure connection and bind operations to Active Directory and ADAM.

  2. Use the certificate in IIS and test it through a Web browser to determine whether it's valid.

  3. Check for SChannel errors in the system event log of the directory server. These errors are indicative of an invalid certificate.

  4. Attempt to perform a similar operation using ADSI (for example, from S.DS).

  5. There are also some important configuration details you must adhere to if you want to connect to an ADAM instance. See the topic "How do I install certificates for use with ADAM and SSL?" at https://www.microsoft.com/windowsserver2003/adam/ADAMfaq.mspx#EOD.

  6. If you create an ADAM instance on a domain controller in an Active Directory domain, then the password you use for your ADAM user accounts must comply with the password policy set for the domain. If the password does not comply, the ADAM user account is automatically and silently disabled. In this case, reset the password to meet the password complexity requirements and then use ADAM ADSI Edit to set the msDS-UserAccountDisabled attribute to False or Not set.

    You will also see other useful answers to FAQs at this URL if you are working with ADAM. In addition, review the ADAM Help chm included with an ADAM installation.

While this is by no means complete, it should give you some troubleshooting techniques to get TLS/SSL connect and bind operations working. Note that you can use SSL wildcard certificates as well. A wildcard certificate is valid for any subdomains of a given domain name. This is particularly useful when you have directory servers behind a load balancer, such as the Microsoft Network Load Balancing (NLB) service.

If you need to generate a valid server certificate for your testing, you can either request an SSL certificate from one of the many third-party certificate providers or by using an existing Public key infrastructure (PKI). Microsoft includes certificate services that you can install in Windows 2000 Server or later to generate certificates. In either case, the easiest way to generate a certificate request is via IIS. You can read a really useful reference for creating a valid certificate by visiting https://www.microsoft.com/technet/prodtechnol/windows2000serv/technologies/iis/maintain/featusability/c06iis.mspx. This URL contains a reference to Chapter 6 - Managing Microsoft Certificate Services and SSL from the Microsoft® Windows® 2000 and IIS 5.0 Administrator's Pocket Consultant. In addition, Joe Kaplan's blog (September 2006) contains a comment from Tomasz Onyszko that briefly mentions the Microsoft certificate services auto-enrollment feature that can provide certificates to all domain controllers. Finally, I recommend "Configuring SSL/TLS, Securing your Web traffic isn't a trivial task" by Jan De Clercq at http://www.windowsitpro.com/Windows/Articles/ArticleID/49556/pg/3/3.html for more details on creating and installing SSL certificates.

The following code example demonstrates how to securely bind to an ADAM instance named ap1 as an ADAM user account using basic authentication over TLS/SSL:

  1. Declare and initialize variables for the SSL bind operation.

    Notice that the hostNameAndSSLPort value contains both a host name and a port value of 50001. This is a custom SSL port that I configured for an ADAM instance upon installation. In addition, a valid SSL server certificate for the host name is installed on the directory server. The userName variable specifies an ADAM user account for this simple bind operation.

  2. Create an LdapConnection object named connection for later performing bind operations over an SSL connection.

  3. Create an LdapSessionOptions object named options and set it equal to the SessionOptions property of the connection.

    The code uses the options object to configure the connection for SSL binding.

  4. Set the ProtocolVersion property of the options object to 3 to support LDAP Version 3 operations.

  5. Set the SecureSocketLayer property to True so that any attempt to perform a simple bind over an unencrypted connection will fail.

    This is critical to ensure that passwords are not sent over the network as clear text.

  6. Set the authentication type to Basic to support a simple bind to ADAM.

  7. Create a NetworkCredential object named credential and pass it a valid user name and password. Next, set the Credential property of the connection equal to the credential object.

    For a simple bind operation you do not pass in a domain name when you create a NetworkCredential object. Otherwise, the bind operation will fail with an invalid credential error message.

  8. Call the Bind method of the connection to send the credentials to the directory server.

  9. If the SecureSocketLayer property of the options object is True, then the connection is secure; therefore, display some properties of the server certificate.

  10. If the credentials are invalid or the connection is not secure, the code will raise an LdapException or DirectoryOperationException respectively.

Example 19. Binding to an ADAM instance on secure port 50001 using Basic authentication and SSL/TLS

string hostNameAndSSLPort = "sea-dc-02.fabrikam.com:50001";
string userName = "cn=User1,cn=AdamUsers,cn=ap1,dc=fabrikam,dc=com";
string password = "adamPassword01!";

// establish a connection
LdapConnection connection = new LdapConnection(hostNameAndSSLPort);

// create an LdapSessionOptions object to configure session 
// settings on the connection.
LdapSessionOptions options = connection.SessionOptions;

options.ProtocolVersion = 3;

options.SecureSocketLayer = true;

connection.AuthType = AuthType.Basic;

NetworkCredential credential =
        new NetworkCredential(userName, password);

connection.Credential = credential;

try
{
    connection.Bind();
    Console.WriteLine("\nUser account {0} validated using " +
        "ssl.", userName);

    if (options.SecureSocketLayer == true)
    {
        Console.WriteLine("SSL for encryption is enabled\nSSL information:\n" +
        "\tcipher strength: {0}\n" +
        "\texchange strength: {1}\n" +
        "\tprotocol: {2}\n" +
        "\thash strength: {3}\n" +
        "\talgorithm: {4}\n",
        options.SslInformation.CipherStrength,
        options.SslInformation.ExchangeStrength,
        options.SslInformation.Protocol,
        options.SslInformation.HashStrength,
        options.SslInformation.AlgorithmIdentifier);
    }

}
catch (LdapException e)
{
    Console.WriteLine("\nCredential validation for User " +
        "account {0} using ssl failed\n" +
        "LdapException: {1}", userName, e.Message);
}
catch (DirectoryOperationException e)
{
    Console.WriteLine("\nCredential validation for User " +
    "account {0} using ssl failed\n" +
    "DirectoryOperationException: {1}", userName, e.Message);
}

Performing Fast Concurrent Bind Operations

A common requirement in single sign on (SSO) or Web site authentication scenarios is high-performance authentication. Sometimes the goal is to simply verify that a user can authenticate to a directory. Using the fast concurrent bind feature available in S.DS.P, you can establish a single, anonymous LDAP connection, and perform multiple binding (authentication) operations over the connection or open channel. Unlike typical bind operations, fast concurrent bind does not create a security token as a result of a bind request so the connection cannot be used for further operations with the provided credentials. This lightweight bind is therefore significantly faster (approximately three to five times faster) then a typical bind and is ideal when a system must perform many bind requests (some possibly concurrently) in a short period of time but perform no other directory related tasks that require credentials.

Fast concurrent bind in S.DS.P works only against directory servers running ADAM or Windows Server 2003 or later. This is true both on the client running the code and the server responding to the fast concurrent bind operation. Therefore, be sure to run your code on the directory server or a Windows Server 2003 (or later) client.

To use a fast concurrent bind, you first create an LdapConnection object, set authentication to Basic and connect to a directory server with the specified credentials. You then create an LdapSessionOptions object, set the ProtocolVersion property to 3 and then call the FastConcurrentBind method. Once these options are specified and the method is called, you call the Bind method of the connection to complete the first authentication attempt. You can then repeatedly pass in new credentials to the connection object and call the Bind method each time.

As I mentioned earlier, fast concurrent bind requires that you use Basic authentication. The FastConcurrentBind method binds to a directory server anonymously. However, subsequent calls to the Bind method pass credentials to the directory server. Because Basic authentication transmits credentials to the directory server as plain text, for security it's important to encrypt the data prior to sending it. See the previous section, Binding over a TLS/SSL Encrypted Connection, for information on creating a certificate for secure authentication.

Another important requirement is setting the ProtocolVersion property to 3 to provide LDAP version 3 support. If you don't do this, then when you call the FastConcurrentBind method it will automatically set the protocol version to 3 for you. Without LDAP version 3, a fast concurrent bind operation will fail. I have provided some pointers to more information about LDAP v3 in the References section of this paper.

The following example demonstrates how to perform a fast concurrent bind over a secure connection. Initially the code binds to the directory server as user1 it then uses the same connection to bind to the server as user2.

  1. Declare and initialize variables for the fast concurrent bind operation.

    Notice that the hostNameAndSSLPort value contains both a host name and a port value of 636. This is the default SSL port for an Active Directory server. In addition, a valid server certificate for the host name must be installed on the directory server.

  2. Create an LdapConnection object named connection for performing fast concurrent bind operations.

  3. Set the authentication type to Basic to support concurrent binding.

  4. Create an LdapSessionOptions object named options and set it equal to the SessionOptions property of the connection.

    The code uses the options object to configure the connection for fast concurrent binding.

  5. Set the ProtocolVersion property of the options object to 3 to support LDAP Version 3 operations.

  6. Set the SecureSocketLayer property to True so that any attempt to perform a fast concurrent bind over an unencrypted connection fails.

    This is critical to ensure that credentials are not sent over the network as clear text.

  7. Call the FastConcurrentBind method.

    Notice that the code calls the FastConcurrentBind method inside of a try catch block. The only error being tested here is an LDAPException. If this exception is thrown, it's likely that an attempt was made to connect over an unencrypted connection. In that case, the exception is handled and the code terminates.

  8. Create a NetworkCredential object named credential and pass it a valid user name, password and domain name. Next, set the Credential property of the connection equal to the credential object.

  9. Call the Bind method of the connection to send the credentials to the directory server.

    An LdapException error occurs if the credentials are invalid. Also, a DirectoryOperationException occurs if the directory server is unable to complete the binding operation.

  10. Reset the credential object to a new set of network credentials and attempt to bind again using the same connection.

Example 20. Performing a fast concurrent bind operation over a secure connection first as user1 and then as user2

string hostNameAndSSLPort = "sea-dc-02.fabrikam.com:636";
string domain = "fabrikam";
string userName1 = "user1";
string password1 = "password01!";
string userName2 = "user2";
string password1 = "password02!";

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

// reset authentication type to basic to support
// concurrent binding. The default is Negotiate
connection.AuthType = AuthType.Basic;

Console.WriteLine("Authentication type reset to {0}",
    connection.AuthType);

// create an LdapSessionOptions object to configure session 
// settings on the connection
LdapSessionOptions options = connection.SessionOptions;

// set to LDAP version 3, to support enhanced authentication
// if you don't explicitly set this, when you call the 
// FastConcurrentBind method, protocol version 3 is set 
// for you
options.ProtocolVersion = 3;

// if an attempt is made to bind over a non-ssl connection, adding this
// property will prevent the bind from succeeding
options.SecureSocketLayer = true;

try
{
    // call fast concurrent bind for this connection
    options.FastConcurrentBind();
}
catch (LdapException)
{
    Console.WriteLine("\nYou did not connect to an SSL" +
        " port.\nThis connection is unsafe and is being terminated.");
        return;
}

NetworkCredential credential = 
                    new NetworkCredential(userName1, password1, domain);

connection.Credential = credential;

// send the first credential
try
{
    connection.Bind();
    Console.WriteLine("\nUser account {0} validated using " +
        "fast concurrent bind.", userName1);
}
catch (LdapException e)
{
    Console.WriteLine("\nCredential validation for User " +
        "account {0} using fast concurrent bind failed\n" +
        "LdapException: {1}", userName1, e.Message);
}
catch (DirectoryOperationException e)
{
    Console.WriteLine("\nCredential validation for User " +
    "account {0} using fast concurrent bind failed\n" +
    "DirectoryOperationException: {1}", userName1, e.Message);
}

// send the second credential using the same connection
try
{
    credential = new NetworkCredential(userName2, password2, domain);
    connection.Credential = credential;
    
    connection.Bind();
    Console.WriteLine("\nUser account {0} validated using " +
        "fast concurrent bind.", userName2);

}
catch (LdapException e)
{
    Console.WriteLine("\nCredential validation for User " +
        "account {0} using fast concurrent bind failed\n" +
        "LdapException: {1}", userName2, e.Message);
}

catch (DirectoryOperationException e)
{
    Console.WriteLine("\nCredential validation for User " +
    "account {0} using fast concurrent bind failed\n" +
    "DirectoryOperationException: {1}", userName2, e.Message);
}

The FastConcurrentBind method in the code download contains additional error checking to determine whether you are attempting to bind over an unencrypted connection.

Leveraging Transport Layer Security

Sometimes you might need to send just some data to a directory server securely via an encrypted connection while other operations don't require this level of protection. Common examples of when security is paramount is authenticating or sending credit card data to a Web server over the Internet. Other operations, such as reviewing or selecting products online might not require an encrypted connection. While it's possible to keep all communications encrypted, it places an unnecessary burden on both the server and client to encrypt and decrypt the data. The end result is non-scalable and poor performance solutions. Ideally, you use encrypted communications for data that requires it and unencrypted communications for everything else.

S.DS.P provides this capability via transport layer security (TLS). Using TLS, you can be selective about what data is sent over an encrypted connection. Note that if your communication to a directory server uses the default negotiate (Kerberos or NTLM) authentication mechanism, it's really not necessary to use TLS for authentication. If, however, you are binding using an exposed authentication mechanism, such as basic authentication, TLS comes in handy.

TLS requires LDAP v3. You enable TLS by calling the StartTransportLayerSecurity method of the LdapSessionOptions class. You can pass this method any directory controls that you want sent to the server for enhanced operations. See the advanced search operation code samples earlier in this paper if you are not familiar with using directory controls. If you aren't using any directory controls, you simply pass null to the StartTransportLayerSecurity method. Once you are finished with TLS, you stop it by calling the StopTransportLayerSecurity method.

The following code example first demonstrates how to start transport layer security, bind over the secure connection using basic authentication and complete an additional task. Second, it demonstrates how to stop TLS, rebind using an inherently secure authentication mechanism and perform a task over the connection. Note that a valid SSL certificate must be installed on the responding directory server.

  1. Declare and initialize variables for this TLS operation.

    Note that no server name is provided in this example. If you do not have SSL certificates on all of your domain controllers, you should specify the fully qualified domain name of a server containing an SSL certificate for the hostOrDomainName variable. Otherwise, the code will fail whenever the Locator service directs the client running the code to connect to a directory server not containing a valid SSL certificate.

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

  3. Create a NetworkCredential object named credential and pass it a valid user name, password and domain name. Next, set the Credential property of the connection equal to the credential object.

  4. Set the AuthType property of the connection to Basic.

    Unlike the previous Fast Concurrent Bind example, setting the AuthType property to Basic is not required. I'm showing it here to demonstrate how you can use TLS to encrypt the bind operation for an authentication method that is not inherently secure.

  5. Create an LdapSessionOptions object named options and set it equal to the SessionOptions property of the connection.

    The code uses the options object to configure the connection for TLS and call the start and stop TLS methods.

  6. Set the ProtocolVersion property of the options object equal to 3.

  7. Start TLS by calling the StartTransportLayerSecurity method.

    Calling this method sets the SecureSocketLayer property of the options object to True.

  8. Complete the bind operation over the encrypted connection using basic authentication.

  9. Complete some other task named TestTask.

    The TestTask method is a simple search operation that I don't show in this code example. However, the code download includes this TestTask so that you can successfully run the TLS sample in the code download.

  10. Stop TLS by calling the StopTransportLayerSecurity method.

    Calling this method sets the SecureSocketLayer property of the options object to False.

  11. Change the authentication type to Negotiate and rebind to the directory.

    Because the binding operation occurred using basic authentication over the encrypted connection, you must rebind to the directory server. If you bind securely from the beginning using the Negotiate authentication type, there is no need to start TLS until after you complete the bind. As a result, you will not need to rebind to the directory after stopping TLS.

  12. Complete the same TestTask over the unencrypted connection.

Example 21. How to use TLS to authenticate and perform a task

string hostOrDomainName = "fabrikam.com";
string userName = "user1";
string password = "password1";

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

NetworkCredential credential =
    new NetworkCredential(userName, password, domainName);

connection.Credential = credential;

connection.AuthType = AuthType.Basic;

LdapSessionOptions options = connection.SessionOptions;

options.ProtocolVersion = 3;

try
{
    options.StartTransportLayerSecurity(null);
    Console.WriteLine("TLS started.\n");
}
catch (Exception e)
{
    Console.WriteLine("Start TLS failed with {0}", 
        e.Message);
    return;
}

try
{
    connection.Bind();
    Console.WriteLine("Bind succeeded using basic " +
        "authentication and SSL.\n");

    Console.WriteLine("Complete another task over " +
        "this SSL connection");
    TestTask(hostName);
}
catch (LdapException e)
{
    Console.WriteLine(e.Message);
}

try
{
    options.StopTransportLayerSecurity();
    Console.WriteLine("Stop TLS succeeded\n");
}
catch (Exception e)
{
    Console.WriteLine("Stop TLS failed with {0}", e.Message);
}

 Console.WriteLine("Switching to negotiate auth type");
 connection.AuthType = AuthType.Negotiate;

 Console.WriteLine("\nRe-binding to the directory");
 connection.Bind();

// complete some action over this non-SSL connection
// note, because Negotiate was used, the bind request 
// is secure. 
// run a task using this new binding
TestTask(hostName);

Performing Certificate-Based Authentication

In the last three examples, I demonstrated how you can use an SSL certificate on a directory server to encrypt communications between a client and a server. S.DS.P also allows you to use client and server certificates to validate both sides of a connection during an authentication attempt. During a bind operation, you can selectively import a client certificate, inspect both the client and server certificate and verify the server certificate before completing the bind operation.

You use the following properties of the LdapSessionOptions class support certificate-based authentication :

  • The QueryClientCertificate property to assign a QueryClientCertificateCallback delegate for importing and optionally verifying the client-side of the connection during the bind operation. Alternatively, instead of calling the QueryClientCertificate property to assign a delegate, you can use the ClientCertificates property of the LdapConnection class directly. The following code snippet demonstrates how this can be done:

    X509Certificate cert = new X509Certificate();
    
    // select a certificate to import
    cert.Import(
       @"c:\cert\cert1.pfx",
       "password1",
       X509KeyStorageFlags.DefaultKeySet);
    
    // add the certificate to the connection
    connection.ClientCertificates.Add(cert);
    
  • The VerifyServerCertificate property to assign a VerifyServerCertificateCallback delegate for verifying the server-side of the connection during the bind operation.

  • The ProtocolVersion property must be set to 3 to use LDAP v3 for this advanced authentication mechanism.

You must then write the client and server methods that are called by their respective delegates. You are also able to inspect details of both the client and server certificates so that you can decide whether to continue the bind operation. While this isn't necessary, it's useful to increase your confidence in the authenticity of both the client and the server certificate.

Certificate-based authentication requires that you assign a client certificate to an existing user account before performing tasks against the directory. Otherwise, following certificate authentication, the directory server will deny any operation that requires a user principal to perform a task.

Here are the required steps for successfully running the associated certificate-based authentication code sample:

  1. Create or obtain a client certificate file in Personal Information Exchange (PFX) format, which will contain the certificate's private key.

    If you don't have a client certificate available or have certificate services already implemented in your instance of Active Directory, you can generate one using Microsoft Certificate Services. After you install certificate services, submit an advanced certificate request to create a client authentication certificate. If you're not familiar with this task, read the certificate services documentation, which is accessible from the home page of the CertSrv site that Certificate Services creates in IIS.

    You will assign a password to the certificate when you export it as a .pfx file. You must know the certificate file location and password to successfully run the certificate authentication example in the code download.

  2. Install the client certificate to the personal certificate store on the client machine where you intend on running the code sample.

  3. From the Certificates MMC console (or snap-in), export the certificate to a file without the private key and use the Distinguished Encoding Rules (DER) format.

  4. Copy the DER encoded certificate to the domain controller that will perform the certificate authentication.

  5. Assign the client certificate to a user account with administrative permissions, by following these steps:

    1. Open the Active Directory Users and Computers MMC.
    2. Open the View menu and verify that Advanced Features is enabled.
    3. From the context menu of a user account, click All Tasks and then click Name Mapping.
    4. Click Add and assign the certificate you copied in step 4.
    5. Verify that Use subject for alternate security identity is enabled, and then click OK.
  6. The domain controller must have a server authentication certificate installed.

    You can verify that a valid certificate is installed and working properly from LDP by attempting to connect to the server over the Active Directory SSL connection port 636. If you successfully ran the TLS example in the previous section, then a valid server authentication certificate is installed.

The following code example demonstrates how to create a certificate routine that calls the two methods to complete a certificate-based authentication operation. Following this code example, I show the methods called by the delegates and a certificate inspection method to increase your confidence in the certificates involved in the authentication operation:

  1. Declare and initialize the hostNameAndSSLPort variables for the client and server certificate-based authentication operation.

    Notice that the hostNameAndSSLPort value contains both a host name and a port value of 636. This is the default SSL port for an Active Directory server. In addition, a valid certificate for the host name must be installed on the directory server.

  2. Create an LdapConnection object for connecting to the sea-dc-02.fabrikam.com directory server over port 636.

  3. Set the AuthType property of the connection to AuthType.External.

    This is an important step because the default authentication type is Negotiate. Even though you intend on using certificate authentication, the code will try Kerberos and then NTLM authentication rather than certificate authentication unless you explicitly specify an External authorization type.

  4. Create an object named options and set it equal to the SessionOptions property of the connection.

  5. Set the SecureSocketLayer property to True and the ProtocolVersion to 3.

    You don't have to set these explicitly because if the certificates are valid and accepted, these properties will be set to True and 3 respectively prior to completing the bind operation. However, it's useful to be clear in your code about critical connection settings.

  6. Set the QueryClientCertificate property to the QueryClientCallback delegate. Pass the delegate the name of the method to call during the bind operation.

  7. Set the VerifyServerCertificate property to the VerifyServerCertificateCallback delegate. Pass the delegate the name of the method to call during the bind operation.

    The code examples following this one explore the methods passed to QueryClientCallback and VerifyServerCertificateCallback.

  8. Attempt to perform a task over the certificate authenticated connection.

    When this task needs to bind to the directory, the code calls the ClientCertificateRoutine and the ServerCertificateRoutine methods.

    This TestTask method adds and deletes a user account using the AddResponse and DeleteResponse classes I explored earlier in this paper. It also demonstrates how to return the rootDSE object from Active Directory via LDAP calls. The code download includes this TestTask method so that you can successfully run the certificate sample from the code download. However, I don't show it here.

Example 22. Creating a client and server certificate-based authentication operation

string hostNameAndSSLPort = "sea-dc-02.fabrikam.com:636";

// establish an SSL connection to the directory
LdapConnection connection = new LdapConnection(hostNameAndSSLPort);

Console.WriteLine("initial connection succeeded\n");

// set the authentication type to external since the code does not 
// rely on built-in Windows authentication mechanisms
connection.AuthType = AuthType.External;

LdapSessionOptions options = connection.SessionOptions;

options.SecureSocketLayer = true;
options.ProtocolVersion = 3;

options.QueryClientCertificate =
    new QueryClientCertificateCallback(ClientCertificateRoutine);

options.VerifyServerCertificate = new
        VerifyServerCertificateCallback(ServerCertificateRoutine);
try //perform a task over client/server certificate authentication
{
    TestTask(connection, hostName);
}

catch (Exception e)
{
    Console.WriteLine("bind with certificate failed with {0} {1}", e.Message, e.InnerException);
    if (e.Message == "The LDAP server is unavailable.")
        Console.WriteLine("You might not have specified an " +
            "SSL port for this connection.");

    Console.WriteLine("Press \"y\" to exit or anything else to continue");
    ConsoleKeyInfo key = Console.ReadKey(false);
    if (key.KeyChar.ToString().ToLower() == "y")
    {
        Console.WriteLine("\noperation terminated");
        return;
    }
    Console.WriteLine("Attempting to continue the operation " +
        "without certificate authentication");
}

The really interesting part of certificate verification appears in the methods called by the delegates, ClientCertificateRoutine and ServerCertificateRoutine. These two method calls appear in the previous code example when the two corresponding callback objects are created. I've also added the GetCertInfo method to demonstrate how you can inspect the certificates before continuing with certificate-based authentication. In the next three code examples, I show the method signatures because they are called from the code in Example 22.

The following code example shows the ClientCertificateRoutine and how you import a certificate in this routine.

  1. This method receives the LdapConnection and the trusted certificate authorities from the callback.

  2. Declare and initialize the certificate file path and name and the password string variables.

    The corresponding code download does not require that you hard code the certificate or password as I show in this code example. This delegate provides a facility for loading a client certificate during a particular LDAP session. Therefore, it makes sense to allow the user to specify a certificate when running the code.

  3. Create an X509Certificate object named cert.

    You use this cert object to import, inspect and add the certificate to the LDAP connection.

  4. Call the Import method of the cert object and pass it the client certificate, certificate password and the X509KeyStorage enumeration with the DefaultKeySet value.

    The X509KeyStorage enumeration allows you to control exactly how the certificate key is handled following an import operation. The DefaultKeySet value specifies that the default private key should be used for the import.

  5. Call the GetCertInfo method.

    I created this method to demonstrate how to return some certificate information. The code for this method appears in Example 25.

  6. Add the imported certificate to the ClientCertificates collection of the connection object.

  7. Return null because there is no use in returning the X509Certificate that this method returns.

    This QueryClientCertificateCallback delegate requires that you return an X509Certificate from the method call. However, since this delegate is called during the bind operation and the bind operation doesn't return anything, there is no need to return the certificate. If you were using the delegate from your own method, you could potentially make use of the returned certificate.

Example 23. The ClientCertificateRoutine called by QueryClientCertificateCallback during a bind operation

private static X509Certificate ClientCertificateRoutine(
    LdapConnection connection, byte[][] auth)
{
    string certFilewithPathSpec = @"c:\certs\myCert.pfx";
    string certPassword = "myCertPassword";

    Console.WriteLine("Inside ClientCertificateRoutine");

    X509Certificate cert = new X509Certificate();

    // select a certificate to import
    cert.Import(
        certFilewithPathSpec,
        certPassword,
        X509KeyStorageFlags.DefaultKeySet);

    GetCertInfo(cert);

    // add the certificate to the connection
    connection.ClientCertificates.Add(cert);

    return null;
}

The following code example shows the ServerCertificateRoutine and how you use it to verify the server certificate:

  1. This method receives the LdapConnection and the server certificate from the callback.

  2. Call the GetCertInfo method.

    I created this method to demonstrate how to return key certificate information. The code for this method appears in Example 25.

  3. Return True if the verification was successful.

    This line of code is the only required line in this method. If the method runs successfully, a value of True is passed back to the calling method — the Bind method in this case. If this method returns False, then the bind operation raises an error and the server cannot complete the authentication request.

Example 24. The ServerCertificateRoutine called by VerifyServerCertificateCallback during a bind operation

private static bool ServerCertificateRoutine(LdapConnection connection, X509Certificate cert)
{
    // client can verify the server certificate here
    Console.WriteLine("\nVerifying the server certificate in the
                      ServerCertificateRoutine");

    GetCertInfo(cert);

    // by returning true, the server certificate is validated
    return true;
}

Another powerful part of client and server certificate verification is having an operator inspect the certificate to enhance the verification process. Once the certificate is exposed in the last two methods, you can run code to inspect the data in the certificate, such as the date and time of certificate validity and details about the certificates issuer and subject.

The following code example shows the GetCertInfo method and how you use it to inspect the client and server certificates:

  1. This method receives an X.509 certificate from both the client and server methods appearing in Examples 23 and 24.
  2. Call the GetEffectiveDateString and GetExpirationDateString methods to return the time period when this certificate is valid and display the information to the console.
  3. Display the Subject and Issuer properties of the certificate.

Figure 25. The GetCertInfo routine called by the ClientCertificateRoutine and ServerCertificateRoutine delegates

private static void GetCertInfo(X509Certificate cert)
{
    // return some information about this certificate
    Console.WriteLine("Valid from {0} to {1}",
        cert.GetEffectiveDateString(),
        cert.GetExpirationDateString());
    Console.WriteLine("subject: {0}", cert.Subject);
    Console.WriteLine("issuer: {0}", cert.Issuer);
}

References

For information on when S.DS.P is the right choice, see "A Tale of Two LDAP Stacks": http://www.joekaplan.net/ATaleOfTwoLDAPStacks.aspx. This is Joe Kaplan's blog. This is a great place to go if you are digging deeply into MS LDAP programming.

For information about ADAM, see the ADAM Resource Site at https://www.microsoft.com/adam and read the introductory reviews on this technology.

To see key advantages for using LDAP directly, see: https://msdn2.microsoft.com/en-us/library/ms806997.aspx.

To read about Lightweight Directory Access Protocol (v3), see the Extension for Transport Layer Security rfc: http://tools.ietf.org/html/rfc2830.

To read the Lightweight Directory Access Protocol (v3) rfc, go to http://www.ietf.org/rfc/rfc2251.txt.

For general information about LDAP v3, see: http://java.sun.com/products/jndi/tutorial/ldap/models/v3.html.

For information about LDAP authentication mechanisms, see http://www.rfc-editor.org/rfc/rfc2829.txt.

For information on Identity, see the The .NET Developer's Guide to Identity: https://msdn2.microsoft.com/en-us/library/aa480245.aspx.

Conclusion

As you can see, S.DS.P opens a whole world of possibilities for performing advanced LDAP programming tasks against directory servers. This namespace exposes capabilities that were previously unavailable to managed code programmers. Hopefully, this information helps you enhance or build new powerful directory services solutions for your customers.

About the author

Ethan Wilansky is a contributing editor for Windows IT Pro, an enterprise architect for EDS in its Innovation Engineering practice, and a Microsoft MVP. He has authored or coauthored more than a dozen books for Microsoft and more than 70 articles.