Introduction
This article describes how to use .NET framework to manage resources under Windows Active Directory Services. Microsoft provides ADSI (Active Directory Services Interface) which can interact with many providers including IIS (Internet Information Services), LDAP (Lightweight Directory Access Protocol), WinNT and NDS (Novell Netware Directory Service).
Since my aim is to demonstrate the usage of .NET DirectoryService classes I am restricting the demo project to querying some commonly used resources (computers, users and printers) in the domain where the user's machine is, at the same time demonstrating the power of Active Directory services and the ease with which Active Directory objects can be retrieved.
I have also used LDAP in this demo since I thought that it will be useful to know this protocol as it is a platform independent protocol.
There are different ways to query the Active Directory Services in a C# program
- using the ADSI through COM Interop. For this, in the Visual Studio C# project go to "Add References..." and select the COM tab and select Active DS Type Library from the list. Add
using ActiveDs;
statement to the top of your file or use fully qualified class names to access ADSI functions. - Using the Active Directory Services OleDB Provider (
ADsDSOObject
). This approach is mostly useful to add Active Directory as a linked server in SQL Server. This subject is out of scope for the present article, I will discuss how to query Active Directory using the ADS OleDB provider in a different article. - Using the classes provided under .NET
System.DirectoryServices
namespace. To access these classes add System.DirectoryServices.dll to the references. This article demonstrates this approach.
.NET System.DirectoryServices namespace
The System.DirectoryServices
namespaces provides two important classes DirectoryEntry
and DirectorySearcher
to work with the Active Directory. The DirectoryEntry class represents a resource in the Active Directory and the DirectorySearcher class is used to query the Active Directory. The other classes under this namespace are for .NET security and collection classes to support the above said main classes.
You have to add System.DirectoryServices
to your references by selecting System.DirectoryServices.dll from .NET tab in the Add Reference dialog box.
LDAP format filter strings
The DirectorySearcher
class uses a search root which is a server where the search begins and a LDAP filter string (which is analogous to where clause in SQL) to query the Active Directory resources. LDAP format filter strings are similar to LISP. A condition is enclosed by parenthesis and an operator precedes 2 conditions. Eg. (& (A)(B))
The statement is equivalent to saying A and B. Remember the parenthesis. Another example (| (& (A)(B) ) (C) )
should be interpreted as (A and B) or (C).
The OLAP conditional statements are formed by using Active Directory attributes like name, objectCategory, objectClass, printerName, ListedName
etc. For eg. to query for a list of printers the condition would be (objectCategory=printQueue
) where objectCategory
is an attribute and printQueue
is the value expected to be assigned to a printer resource in Active Directory. Similary to query for all printers which start with a character 'G' the LDAP query would be (& (objectCategory=printQueue) (name=G*) )
In the above filter observe that the values do not have quotes (' or ") around them.
Using the code
The demo project demonstrates how to query the Active Directory Services and fetch different objects. The LDAP queries and the usage of .NET classes used are confined to QueryObjectsByNETClasses()
and GetFilterString()
methods.
The following code in QueryObjectsByNETClasses()
method creates a DirectorySearcher
object and sets properties on the searcher depending on the user preferences given in the main form. The description of different properties is in the comments in the code. Only "name" is added to the PropertiesToLoad
property of DirectorySearcher
class to save time as we are only interested in retrieving name and objectClass for the list of objects returned.
DirectorySearcher ds = new DirectorySearcher();
ds.SearchRoot = new DirectoryEntry("");
// start searching from local domain
ds.Filter = GetFilterString();
// get the LDAP filter string based on selections on the form
ds.PropertyNamesOnly = true;
// this will get names of only those
// properties to which a value is set
ds.PropertiesToLoad.Add("name");
// (PageSize) Maximum number of objects
// the server will return per page
// in a paged search. Default is 0, i.e. no paged search
if (ObjsPerPage.Text.Length > 0)
ds.PageSize = Int32.Parse(ObjsPerPage.Text);
// (ServerPageTimeLimit) the amount of time the server
// should observe to search a page of results
// default is -1, i.e. search indefinitely
if (PageTimeLimit.Text.Length > 0)
ds.ServerPageTimeLimit = new TimeSpan((long)(Decimal.Parse(
PageTimeLimit.Text) * TimeSpan.TicksPerSecond));
// (SizeLimit) maximum number of objects the server
// returns in a search
// default is 0 - interpreted as server
// set default limit of 1000 entries
if (ObjsToFetch.Text.Length > 0)
ds.SizeLimit = Int32.Parse(ObjsToFetch.Text);
// (ServerTimeLimit) amount of time that the server
// should observe in a search
// default is -1 interpreted as server default limit of 120 seconds
if (TotalTimeLimit.Text.Length > 0)
ds.ServerTimeLimit = new TimeSpan((long)(Decimal.Parse(
TotalTimeLimit.Text) * TimeSpan.TicksPerSecond));
// (SearchScope) option to search one level or complete subtree
// default is Subtree, so set this option only if oneLevel is selected
if (searchOptionCB.SelectedIndex == 1)
ds.SearchScope = SearchScope.OneLevel;
// (CacheResults) property by default is true
ds.CacheResults = CacheResultsCB.Checked;
ds.ReferralChasing = ReferralChasingOption.None;
if (SortResultsCB.Checked)
ds.Sort = new SortOption("name", SortDirection.Ascending);
The FormFilter()
and GetFilterString()
functions are used to form the LDAP query strings (see the explaination for the format of these strings in the LDAP format filter strings section above). Mainly observe the placement of & and | operators before the lists. For a complete set of attributes on which the queries can be formed for different objects refer to the Active Directory Schema under MSDN here.
// form a filter string for the search in LDAP format
private string FormFilter(string objectCategory, string filter)
{
String result;
result = String.Format("(&(objectCategory={0})(name={1}))",
objectCategory, filter);
return result;
}
// this function forms the filter string based on the selected
// objects on the form
private string GetFilterString()
{
// form the filter string for directory search
string filter = "";
if (UsersCB.Checked)
filter += FormFilter("user", UsersFilter.Text);
if (ComputersCB.Checked)
filter += FormFilter("computer", ComputersFilter.Text);
if (PrintersCB.Checked)
filter += FormFilter("printQueue", PrintersFilter.Text);
// add all the above filter strings
return "(|" + filter + ")";
}
Important Points and Notes
-
Use
objectCategory
attribute instead ofobjectClass
where ever possible. Active Directory documentation mentions two things related to this issue :
objectClass
attribute can have multiple values. This can be a problem especially if you are retrivingobjectClass
. You can end up with multiple values !objectCategory
is an indexed attribute in Active Directory. So usingobjectCategory
speeds up queries.
The second point is more important because all the examples given in MSDN use
objectClass
, whereas usingobjectCategory
will speed up queries !
-
If the list queried is too large then there is a possibility of a timeout. So don't be surprised if your query does not return the complete list. A point to be noted is that you cannot set a value to
ServerTimeLimit
larger than the default value of 120 seconds ! So if you are looking for all objects and your directory is too large it is better to query number of times by changing your LDAP filter string incrementally (for eg. a*, b* ..) and combining the results. -
Try to use
PropertiesToLoad
andPropertyNamesOnly
properties ofDirectorySearcher
if you know what properties you are trying to retrieve. IfPropertyNamesOnly
is set to true, the query will fetch the names of only those properties for which a value is set. Giving names of properties to be loaded toPropertiesToLoad
will reduce the fetch time. By defaultPropertiesToLoad
is set to an emptyStringCollection
to fetch all the properties, andPropertyNamesOnly
is set to false to retrieve all properties' names even if a value is not set. For eg. in my demo I have given "name" property to be loaded and setPropertyNamesOnly
to true. Please note that even if not specifiedobjectClass
andobjectCategory
propertied are automatically loaded whenever an object is fetched. -
By default all the results of a Active Directory fetch are cached. Set the
CacheResults
property ofDirectorySearcher
to false to refresh the object cache on the local computer.