Programming the Active Directory

 
Chapter 13 - Working with the Active Directory
bySimon Robinsonet al.
Wrox Press 2002
  

To develop programs for the Active Directory, we use classes from the System.DirectoryServices namespace and have to reference the System.DirectoryServices assembly. With these classes we can query objects, view and update properties, search for objects, and move objects to other container objects. In the following code segments we use a simple C# console application to demonstrate how the classes in the System.DirectoryServices namespace can be used.

In this section, we'll cover:

  • Classes in the System.DirectoryServices namespace

  • The process of connecting to the Active Directory - binding

  • Getting directory entries, and creating new objects and updating existing entries

  • Searching the Active Directory

Classes in System.DirectoryServices

The following table shows the major classes in the System.DirectoryServices namespace:

Class

Description

DirectoryEntry

This class is the main class of the System.DirectoryServices namespace. An object of this class represents an object in the Active Directory store. We use this class to bind to an object, and to view and update properties. The properties of the object are represented in a PropertyCollection . Every item in the PropertyCollection has a PropertyValueCollection .

DirectoryEntries

DirectoryEntries is a collection of DirectoryEntry objects. The Children property of a DirectoryEntry object returns a list of objects in a DirectoryEntries collection.

DirectorySearcher

This class is the main class used for searching for objects with specific attributes. To define the search the SortOption class and the enumerations SearchScope , SortDirection , and ReferalChasingOption can be used. The search results in a SearchResult or a SearchResultCollection . We also get ResultPropertyCollection and ResultPropertyValueCollection objects.

Binding

To get the values of an object in the Active Directory, we have to connect to the Active Directory. The connecting process is called binding . The binding path can look like this:

 LDAP://dc01.globalknowledge.net/OU=Marketing, DC=GlobalKnowledge, DC=Com 

With the binding process we can specify these items:

  • The Protocol specifies the provider to be used

  • The Server Name of the domain controller

  • The Port Number of the server process

  • The Distinguished Name of the object; this identifies the object we want to access

  • The Username and Password if a user that's different to the account running the current process is needed for accessing the Active Directory

  • An Authentication type can also be specified if encryption is needed

Let's have a more detailed look into these options:

Protocol

The first part of a binding path specifies the ADSI provider. The provider is implemented as a COM server; for identification a progID can be found in the Registry directly under HKEY_CLASSES_ROOT . The providers we get with Windows 2000 are listed in this table:

Provider

Description

LDAP

LDAP Server, such as the Exchange directory and the Windows 2000 Active Directory Server.

GC

GC is used to access the global catalog in the Active Directory. It can be used for fast queries.

IIS

With the ADSI provider for IIS it's possible to create new web sites and administer it in the IIS catalog.

WinNT

To access the user database of old Windows NT 4 domains we can use the ADSI provider for WinNT. The fact that NT 4 users only have a few attributes remains unchanged. It is also possible to use this protocol to bind to a Windows 2000 domain, but here you are also restricted to the attributes that are available with NT 4.

NDS

This progID is used to communicate with Novell Directory Services.

NWCOMPAT

With NWCOMPAT we can access old Novell directories, such as Novell Netware 3.x.

Server Name

The server name follows the protocol in the binding path. The server name is optional if you are logged on to an Active Directory domain. Without a server name serverless binding occurs; this means that Windows 2000 tries to get the "best" domain controller in the domain that's associated with the user doing the bind. If there's no server inside a site, the first domain controller that can be found will be used.

A serverless binding can look like LDAP://OU=Sales, DC=GlobalKnowledge, DC=Com .

Port Number

After the server name we can specify the port number of the server process, by using the syntax :xxx . The default port number for the LDAP server is port 389: LDAP://dc01.globalknowledge.net:389 . The Exchange server uses the same port number as the LDAP server. If the Exchange server is installed on the same system - for example, as a domain controller of the Active Directory - a different port can be configured.

Distinguished Name

The fourth part that we can specify in the path is the distinguished name (DN). The distinguished name is a guaranteed unique name that identifies the object we want to access. With the Active Directory we can use LDAP syntax that is based on X.500 to specify the name of the object.

For example, we could have this distinguished name:

 CN=Christian Nagel, OU=Trainer, DC=GlobalKnowledge, DC=com 

This distinguished name specifies a Common Name ( CN ) of Christian Nagel in the Organizational Unit ( OU ) called Trainer in the Domain Component ( DC ) called GlobalKnowledge of the domain GlobalKnowledge.com . The part that is specified rightmost is the root object of the domain. The name has to follow the hierarchy in the object tree.

The LDAP specification for the string representation of distinguished names can be found in RFC 2253: http://www.ietf.org/rfc/rfc2253.txt .

Relative Distinguished Name

A relative distinguished name ( RDN ) is used to reference objects within a container object. With an RDN the specification of OU and DC is not needed, as a common name is enough. CN=Christian Nagel is the relative distinguished name inside the organizational unit. A relative distinguished name can be used if we already have a reference to a container object and we want to access child objects.

Default Naming Context

If a distinguished name is not specified in the path, the binding process will be made to the default naming context. We can read the default naming context with the help of rootDSE . LDAP 3.0 defines rootDSE as the root of a directory tree on a directory server. For example:

   LDAP://rootDSE   

or:

   LDAP://servername/rootDSE   

By enumerating all properties of the rootDSE we can get the information about the defaultNamingContext that will be used when no name is specified. schemaNamingContext and configurationNamingContext specify the required names to be used to access the schema and the configuration in the Active Directory store.

The following code is used to get all properties of the rootDSE :

   using (DirectoryEntry de = new DirectoryEntry())     {     de.Path = "LDAP://celticrain/rootDSE";     de.Username = @"sentinel\chris";     de.Password = "someSecret";     PropertyCollection props = de.Properties;     foreach (string prop in props.PropertyNames)     {     PropertyValueCollection values = props[prop];     foreach (string val in values)     {     Console.Write(prop + ": ");     Console.WriteLine(val);     }     }     }   

Besides outputting other properties, this program shows the defaultNamingContext DC=eichkogelstrasse, DC=local , the context that can be used to access the schema: CN=Schema, CN=Configuration, DC=eichkogelstrasse, DC=local , and the naming context of the configuration: CN=Configuration, DC=eichkogelstrasse, DC=local :

click to expand
Object Identifier

Every object has a unique identifier, a GUID. A GUID is a unique 128-bit number as you may already know from COM development. We can bind to an object using the GUID. This way we always get to the same object no matter if the object was moved to a different container. The GUID is generated at object creation and always remains the same.

We can get to a GUID string representation with DirectoryEntry.NativeGuid . This string representation can then be used to bind to the object.

This example shows the path name for a serverless binding to bind to a specific object represented by a GUID:

   LDAP://<GUID=14abbd652aae1a47abc60782dcfc78ea>   
Object Names in Windows NT Domains

The WinNT provider doesn't allow LDAP syntax in the name part of the binding string. With this provider the object is specified using ObjectName, ClassName . Valid binding strings for a Windows NT domain are:

   WinNT:     WinNT://DomainName     WinNT://DomainName/UserName, user     WinNT://DomainName/ServerName/MyGroup, group   

The user and group postfixes specify that we access objects of type user or group .

Username

If a user other than the user of the current process must be used for accessing the directory (maybe this user doesn't have the required permissions to access the Active Directory), explicit user credentials must be specified for the binding process. With Active Directory we have a number of ways to set the username.

Downlevel Logon

With a downlevel logon the username can be specified with the pre-Windows 2000 domain name:

   domain\username   
Distinguished Name

The user can also be specified by a distinguished name of a user object, for example:

   CN=Administrator, CN=Users, DC=eichkogelstrasse, DC=local   
User Principal Name (UPN)

The user principal name ( UPN ) of an object is defined with the userPrincipalName attribute. The system administrator specifies this with the logon information in the Account tab of the User properties with the Active Directory Users and Computers tool. Note that this is not the e-mail address of the user.

This information also uniquely identifies a user, and can be used for a logon:

   Nagel@eichkogelstrasse.local   

Authentication

For secure encrypted authentication the authentication type can also be specified. The authentication can be set with the AuthenticationType property of the DirectoryEntry class. The value that can be assigned is one of the AuthenticationTypes enumeration values. Because the enumeration is marked with the [Flags] attribute, multiple values can be specified. Some of the possible values are where the data sent is encrypted, ReadonlyServer, where we specify that we need only read access, and Secure for secure authentication.

Binding with the DirectoryEntry Class

The System.DirectoryServices.DirectoryEntry class can be used to specify all the binding information. We can use the default constructor and define the binding information with the properties Path , Username , Password , and AuthenticationType , or pass all the information in the constructor:

   DirectoryEntry de = new DirectoryEntry();     de.Path = "LDAP://celticrain/DC=eichkogelstrasse, DC=local";     de.Username = "nagel@eichkogelstrasse.local";     de.Password = "someSecret";     // use the current user credentials     DirectoryEntry de2 = new DirectoryEntry(     "LDAP://DC=eichkogelstrasse, DC=local");   

Even if constructing the DirectoryEntry object is successful, this doesn't mean that the binding was a success. Binding will happen the first time a property is read to avoid unnecessary network traffic. At the first access of the object, it can be seen if the object exists, and if the specified user credentials are correct.

Getting Directory Entries

Now that we know how to specify the binding attributes to an object in the Active Directory, let's read the attributes of an object.

Properties of User Objects

The DirectoryEntry class has some properties to get information about the object: the Name , Guid , and SchemaClassName properties. The first time we access a property of the DirectoryEntry object, the binding occurs and the cache of the underlying ADSI object is filled. We will discuss this, more detail later. When we access the other properties, we're reading them just from the cache, and communication with the server isn't necessary for data from the same object.

In this example we are accessing a user object with the common name Christian Nagel in the organization unit Wrox Press :

   using (DirectoryEntry de = new DirectoryEntry())     {     de.Path = "LDAP://celticrain/CN=Christian Nagel, " +     "OU=Wrox Press, DC=eichkogelstrasse, DC=local";     Console.WriteLine("Name: " + de.Name);     Console.WriteLine("GUID: " + de.Guid);     Console.WriteLine("Type: " + de.SchemaClassName);     Console.WriteLine();     //...     }   

An Active Directory object holds much more information, with the information available depending on the type of the object; the Properties property returns a PropertyCollection . Each property is itself a collection, because a single property can have multiple values, for example, the user object can have multiple phone numbers . In our example, we go through the values with an inner foreach loop. The collection that is returned from properties[name] is an object array. The attribute values can be strings, numbers, or other types. We will just use the ToString() method to display the values.

   Console.WriteLine("Properties: ");     PropertyCollection properties = de.Properties;     foreach (string name in properties.PropertyNames)     {     foreach (object o in properties[name])     {     Console.WriteLine(name + ": " + o.ToString());     }     }   

In the resulting output we see all attributes of the user object Christian Nagel . We can see that otherTelephone is a multivalue property that has many phone numbers. Some of the property values just display the type of the object, System.__ComObject , for example lastLogoff , lastLogon , and nTSecurityDescriptor . To get the values of these attributes we have to use the ADSI COM interfaces directly from the classes in the System.DirectoryServices namespace.

In Chapter 17 you can read about how to work with COM objects and interfaces.

click to expand
Access a Property Directly by its Name

With DirectoryEntry.Properties we can access all properties. If a property name is known we can access the values directly:

   foreach (string homePage in de.Properties["wWWHomePage"])     Console.WriteLine("Home page: " + homePage);   

Object Collections

Objects are stored hierarchically in the Active Directory. Container objects contain children. We can enumerate these child objects with the Children property of the class DirectoryEntry . In the other direction, we can get the container of an object with the Parent property.

A user object doesn't have children, so now I'm using an organizational unit instead. Non-container objects return an empty collection with the Children property. Let's get all user objects from the organizational unit Wrox Press in the domain eichkogelstrasse.local . The Children property returns a DirectoryEntries collection that collects DirectoryEntry objects. We iterate through all DirectoryEntry objects to display the name of the child objects:

   using (DirectoryEntry de = new DirectoryEntry())     {     de.Path = "LDAP://celticrain/OU=Wrox Press, " +     "DC=eichkogelstrasse, DC=local";     Console.WriteLine("Children of " + de.Name);     foreach (DirectoryEntry obj in de.Children)     {     Console.WriteLine(obj.Name);     }     }   
click to expand

In this example we've seen all the objects in the organizational unit: users , contacts , printers , shares , and others. If we want to see only some object types we can use the SchemaFilter property of the DirectoryEntries class. The SchemaFilter property returns a SchemaNameCollection . With this SchemaNameCollection we can use the Add() method to define the object types we want to see. In our case we are just interested in seeing the user objects, so user is added to this collection:

 using (DirectoryEntry de = new DirectoryEntry())  {    de.Path = "LDAP://celticrain/OU=Wrox Press, " +               "DC=eichkogelstrasse, DC=local";    Console.WriteLine("Children of " + de.Name);   de.Children.SchemaFilter.Add("user");   foreach (DirectoryEntry obj in de.Children)    {       Console.WriteLine(obj.Name);    } } 

As a result we only see the user objects in the organizational unit:

click to expand

Cache

To reduce the network transfers, ADSI uses a cache for the object properties. As we mentioned earlier, the server isn't accessed when we create a DirectoryEntry object; instead when we first read a value from the directory store all the properties are written into the cache, so that a round trip to the server isn't necessary when we read the next property.

Writing any changes to objects will only change the cached object; setting properties doesn't generate network traffic. To transfer any changed data to the server, DirectoryEntry.CommitChanges() will flush the cache and take care of this. To get the newly written data from the directory store, we can use DirectoryEntry.RefreshCache() to read the properties. Of course if you change some properties without calling CommitChanges() and do a RefreshCache() , all your changes will be lost because we read the values from the directory service again using RefreshCache() .

It is possible to turn off this property cache by setting the DirectoryEntry.UsePropertyCache property to false . However, unless debugging, it's better not to turn off the cache because of the extra round trips to the server that will be generated.

Creating New Objects

When we want to create new Active Directory objects such as users, computers, printers, contacts, and so on, we can do this programmatically with the DirectoryEntries class.

To add new objects to the directory we first have to bind to a container object, such as an organizational unit, where new objects can be inserted - objects that can't contain other objects can't be used. Here I'm using the container object with the distinguished name CN=Users, DC=eichkogelstrasse, DC=local :

   DirectoryEntry de = new DirectoryEntry();     de.Path = "LDAP://celticrain/CN=Users, DC=eichkogelstrasse, DC=local";   

We can get to the DirectoryEntries object with the Children property of a DirectoryEntry :

   DirectoryEntries users = de.Children;   

With DirectoryEntries we have methods to add, remove, and find objects in the collection. Here I'm creating a new user object. With the Add() method, we need the name of the object and a type name. We can get to the type names directly using ADSI Edit.

   DirectoryEntry user = users.Add("CN=John Doe", "user");   

The object now has the default property values. To assign specific property values we can add properties with the Add() method of the Properties property. Of course, all of the properties must exist in the schema for the user object. If a specified property doesn't exist you'll get a COMException " The specified directory service attribute or value doesn't exist ":

   user.Properties["company"].Add("Some Company");     user.Properties["department"].Add("Sales");     user.Properties["employeeID"].Add("4711");     user.Properties["samAccountName"].Add("JDoe");     user.Properties["userPrincipalName"].Add("JDoe@eichkogelstrasse.local");     user.Properties["givenName"].Add("John");     user.Properties["sn"].Add("Doe");     user.Properties["userPassword"].Add("someSecret");   

Finally, to write the data to the Active Directory, we have to flush the cache:

   user.CommitChanges();   

Updating Directory Entries

Objects in the Active Directory can be updated as easily as they can be read. After reading the object, we are able to change the values. To remove all values of a single property the method PropertyValueCollection.Clear() can be called. With Add() new values can be added to a property. Remove() and RemoveAt() remove specific values from a property collection.

We can change a value simply by setting it to the specified value. With the following code example the mobile phone number is set to a new value by using an indexer for the PropertyValueCollection . With the indexer a value can only be changed if it exists. Therefore, we should always check with DirectoryEntry.Properties.Contains() if the attribute is available:

 using (DirectoryEntry de = new DirectoryEntry()) {    de.Path = "LDAP://celticrain/CN=Christian Nagel, " +              "OU=Wrox Press, DC=eichkogelstrasse, DC=local";   if (de.Properties.Contains("mobile"))     {     de.Properties["mobile"][0] = "+43(664)3434343434";     }     else     {     de.Properties["mobile"].Add("+43(664)3434343434");     }     de.CommitChanges();     }   

With the else part in our example we add a new property for the mobile phone number if it doesn't already exist with the method PropertyValueCollection.Add() . If we would use the Add() method with already existing properties the resulting effect depends on the type of the property - a single-value or a multi-value property. Using the Add() method with a single value property that already exists we get a COMException: A constraint violation occurred . Using Add() with a multi value property, however, succeeds, and an additional value is added to the property.

The property mobile for a user object is defined as a single-value property, so additional mobile phone numbers cannot be added. However a user can have more than one mobile phone number. For multiple mobile phone numbers we have the property otherMobile . otherMobile is a multi-value property that allows setting multiple phone numbers, and so calling Add() multiple times. There is one important check for multi-value properties: they are checked for uniqueness. If case the second phone number is added to the same user object again we get a COMException: The specified directory service attribute or value already exists .

Important 

Remember to call DirectoryEntry.CommitChanges() after creating or updating new directory objects. Otherwise only the cache gets updated, and the changes are not sent to the directory service.

Accessing Native ADSI Objects

Often it is a lot easier to call methods of predefined ADSI interfaces instead of searching for the names of object properties. Some ADSI objects also support methods that can't be directly used from the DirectoryEntry class. One example of a practical use is the IADsServiceOperations interface that has methods to start and stop Windows services. Windows Services will be discussed in Chapter 22.

The classes of the System.DirectoryServices namespace use the underlying ADSI COM objects as we discussed before in this chapter. The DirectoryEntry class supports calling methods of the underlying objects directly by using the Invoke() method.

The first parameter of Invoke() requires the method name that should be called in the ADSI object; the params keyword of the second parameter allows a flexible number of additional arguments that can be passed to the ADSI method:

   public object Invoke(string methodName, params object[] args);   

You can find the methods that can be called with the Invoke() method in the ADSI documentation. Every object in the domain supports the methods of the IADs interface. The user object that we created previously also supports the methods of the IADsUser interface.

In the following code example, we use the method IADsUser.SetPassword() to change the password of the previously created user object:

   using (DirectoryEntry de = new DirectoryEntry())     {     de.Path = "LDAP://celticrain/CN=John Doe, " +     "CN=Users, DC=eichkogelstrasse, DC=local";     de.Invoke("SetPassword", "anotherSecret");     de.CommitChanges();     }   

Instead of using Invoke() it is also possible to use the underlying ADSI object directly. To use these objects we have to add a reference to the Active DS Type Library using Project Add Reference . This creates a wrapper class where we can access these objects in the namespace ActiveDs .

click to expand

The native object can be accessed with the NativeObject property of the DirectoryEntry class. In our example, the object de is a user object, so we can cast it to ActiveDs.IADsUser . SetPassword() is a method that is documented in the IADsUser interface, so we can call it directly instead of using the Invoke() method. By setting the AccountDisabled property of IADsUser to false , we can enable the account. As in our previous examples the changes are written to the directory service by calling CommitChanges() with the DirectoryEntry object:

   ActiveDs.IADsUser user = (ActiveDs.IADsUser)de.NativeObject;     user.SetPassword("someSecret");     user.AccountDisabled = false;     de.CommitChanges();   

Searching in the Active Directory

Since the Active Directory is a data store that's optimized for read-mostly access, we will generally be searching it for values. To search in the Active Directory, the .NET Framework has the DirectorySearcher class.

We can only use DirectorySearcher with the LDAP provider; it doesn't work with the other providers such as NDS or IIS.

In the constructor of the DirectorySearcher class we can define four important parts for the search. We can also use a default constructor and define the search options with properties.

SearchRoot

The search root specifies where the search should start. The default of the SearchRoot is the root of the domain you're currently using. The SearchRoot is specified with the Path of a DirectoryEntry object.

Filter

The filter defines the values where we want to get hits. The filter is a string that must be enclosed in parentheses.

Relational operators such as < = , = , and > = are allowed in expressions. (objectClass=contact) will search all objects of type contact; (lastName > =Nagel) searches all objects where the lastName property is equal to or larger than Nagel , which means that it follows in the alphabet.

Expressions can be combined with the & and prefix operators. For instance, ( & (objectClass=user)(description=Auth*)) searches all objects of type user where the property description starts with the string Auth . Because the & and operators are at the beginning of the expressions it's possible to combine more than two expressions with a single prefix operator.

The default filter is (objectClass=*) so all objects are valid.

The filter syntax is defined in RFC 2254, " The String Representation of LDAP Search Filters ". This RFC can be found at http://www.ietf.org/rfc/rfc2254.txt .

PropertiesToLoad

With PropertiesToLoad we define a StringCollection of all the properties that we are interested in. Objects can have a lot of properties, most of which will not be important for our search request. We define the properties that should be loaded into the cache. The default properties we get if nothing is specified are the Path and the Name of the object.

SearchScope

SearchScope is an enumeration that defines how deep the search should extend:

  • SearchScope.Base only searches the attributes in the object where the search started, so we get at most one object.

  • With SearchScope.OneLevel the search continues in the child collection of the base object. The base object itself is not searched for a hit.

  • SearchScope.Subtree defines that the search should go down the complete tree.

The default SearchScope is Subtree .

Search Limits

A search for specific objects in a directory service can span multiple domains. To limit the search to the number of objects or the time taken we have some additional properties to define, as shown in the following table:

Property

Description

ClientTimeout

The maximum time the client waits for the server to return a result. If the server does not respond no records are returned.

PageSize

With a paged search the server returns a number of objects defined with the PageSize instead of the complete result. This reduces the time for the client to get a first answer and the memory needed. The server sends a cookie to the client, which is sent back to the server with the next search request, so that the search can continue at the point where it finished.

ServerPageTimeLimit

For paged searches this value defines the time a search should continue to return a number of objects that's defined with the PageSize value. If the time is reached before the PageSize value, the objects that were found up to that point are returned to the client. The default value is -1 , which means infinite.

ServerTimeLimit

Defines the maximum time the server will search for objects. When this time is reached all objects that are found up to this point are returned to the client. The default is 120 seconds, and you cannot set the search to a higher value.

ReferalChasing

A search can cross multiple domains. If the root that's specified with SearchRoot is a parent domain or no root was specified, the search can continue to child domains. With this property we can specify if the search should continue on different servers.

ReferalChasingOption.None means that the search does not continue onto other servers.

With the value ReferalChasingOption.Subordinate it's specified that the search should go on to child domains. When the search starts at DC=Wrox, DC=COM the server can return a result set and the referral to DC=France, DC=Wrox, DC=COM . The client can continue the search in the subdomain.

ReferalChasingOption.External means that the server can refer the client to an independent server that is not in the subdomain. This is the default option.

With ReferalChasingOption.All both external and subordinate referrals are returned.

In our search example we want to search for all user objects in the organizational unit Wrox Press , where the property description has a value of Author .

First, we bind to the organizational unit Wrox Press . This is where the search should start. We are creating a DirectorySearcher object where the SearchRoot is set. The filter is defined as ( & (objectClass=user)(description=Auth*)) , so that we find all objects of type user with a description of Auth following by something else. The scope of the search should be a sub-tree, so that child organizational units within Wrox Press are searched, too:

   using (DirectoryEntry de =     new DirectoryEntry("LDAP://OU=Wrox Press, DC=eichkogelstrasse, DC=local"))     using (DirectorySearcher searcher = new DirectorySearcher())     {     searcher.SearchRoot = de;     searcher.Filter = "(&(objectClass=user)(description=Auth*))";     searcher.SearchScope = SearchScope.Subtree;   

The properties we want to have in the result of the search are name , description , givenName , and wWWHomePage :

   searcher.PropertiesToLoad.Add("name");     searcher.PropertiesToLoad.Add("description");     searcher.PropertiesToLoad.Add("givenName");     searcher.PropertiesToLoad.Add("wWWHomePage");   

We are ready to do the search. However, the result should also be sorted. DirectorySearcher has a property Sort , where we can set a SortOption . The first argument in the constructor of the SortOption class defines the property that we use to sort by; the second argument defines the direction of the sort. The SortDirection enumeration has values Ascending and Descending .

To start the search we can use the method FindOne() to find the first object, or FindAll() . FindOne() returns a simple SearchResult , whereas FindAll() returns a SearchResultCollection . We want to get all the authors, so FindAll() is used here:

   searcher.Sort = new SortOption("givenName", SortDirection.Ascending);     SearchResultCollection results = searcher.FindAll();   

With a foreach loop we are accessing every SearchResult in the SearchResultCollection . A SearchResult represents a single object in the search cache. The Properties property returns a ResultPropertyCollection , where we access all properties and values with the property name and the indexer:

   SearchResultCollection results = searcher.FindAll();     foreach (SearchResult result in results)     {     ResultPropertyCollection props = result.Properties;     foreach (string propName in props.PropertyNames)     {     Console.Write(propName + ": ");     Console.WriteLine(props[propName][0]);     }     Console.WriteLine();     }     }   

If you would like to get to the complete object after a search that's also possible: SearchResult has a method GetDirectoryEntry() that returns the corresponding DirectoryEntry of the found object.

The resulting output shows the beginning of the list of all authors of Professional C# with the properties we've chosen :

click to expand
  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net