
The .NET Framework includes Active Directory (AD) interaction classes out-of-the-box, and this article provides some guidance on using those classes to interrogate your Domain Controller.
The first thing to mention before we go any further is that the .NET classes don’t provide native connectivity to AD – they largely wrap and extend existing COM libraries that have been around for years. That being the case, you need to be very careful about cleaning up and releasing your resources or you may introduce memory leaks into your programs.
AD queries use a specific syntax (Lightweight Directory Access Protocol (LDAP) syntax), which uses a combination of keywords, operators and literal values to establish a query. The following tables provide some information on different aspects of the syntax but they are by no means exhaustive lists. Microsoft provide comprehensive information on their website (e.g. LDAP Query Basics).
LDAP Operators
Operator | Name | Description |
= | Equal To | Performs an equality check (normally between an LDAP property value and a literal value.) |
! | Not | Negation operator that can be used to check for non-matching results. |
& | And | Logically groups individual terms, requiring that all of them are satisfied. |
| | Or | Logically groups individual terms, requiring only one of them to be satisfied. |
* | Wildcard | Allows for wildcard matching, such as matching all records that start with finance (finance*). |
Commonly Used Properties
Microsoft provides various lists of LDAP properties, for example User Object Mappings and All Attributes. The table below summarises the AD properties that are most likely to be required in common programming scenarios.
Property Name | Description |
anr | Ambiguous Name Resolution. Allows searching within a number of LDAP properties. These include givenName, surname, cn, physicalDeliveryOfficeName, and sAMAccountName. Results are based on sub-set matching – so searching for ‘smith’ would match ‘smith’, ‘smithers’, ‘smith-jones’, etc. |
cn | Common Name. The name of the AD object. For user objects, this is normally of the form first-name last-name but it will depend what was entered when the object was created or amended. |
department | Department. The name of the department the user works in. |
description | Description. A single line description of the entry. |
directReports | Direct Reports. A list of the users who are managed by (direct reports of) the current user object. |
displayName | Display Name. A display name for the entry. For user objects, this is normally of the form: first-name last-name, or first-name initials last-name. |
distinguishedName | Distinguished Name. The fully resolved unique name of the entry, including organisational unit(s) information. Distinguished names are made up of Domain Components (DC – the aspects of a domain name), Organisational Units (OU – logical containers that can be used to organise AD objects in a tree structure), and a Common Name (CN – the short-name of the object.) |
givenName | Given Name. The first name of the user. |
grouptype | Group Type. An ID that represents the group type. Common group type IDs are: 2=Global-Distribution-Group, 4=Domain-Local-Distribution-Group, 8=Universal-Distribution-Group, -2147483646=Global-Security-Group, -2147483644=Domain-Local-Security-Group, -2147483640=Universal-Security-Group. |
initials | User’s Middle Initials. The initials for the user’s middle name(s). Often omitted from the AD schema though. |
lockoutTime | Lockout Time. A timestamp representing when the user’s account was locked out. NOTE: This isn’t a standard .NET tick count value – it is the number of 100 nano-second intervals that have elapsed since 1st Jaunuary 1601 (UTC). |
Email Address(es). The email address(es) for the user. NOTE: multiple email addresses are supported. |
|
manager | Manager. The distinguished name of the user’s manager. |
member | Members. Lists the members of the current group object. |
memberOf | Groups Membership. Lists the groups (security and distribution groups) the AD object is a member of. |
name | Name. The same as Common Name (‘cn’, see above.) |
objectClass | Object Class. The list of classes from which the AD object is derived. (e.g. for user accounts, objectClass=user, or possibly objectClass=userProxy for ADAM or AD-LDS.) |
objectSid | System Identifier. The unique system identifier (SID) for the object in the AD schema. Each object that is created in AD will have a unique SID associated it – if an object is deleted from AD and then a new one with the same salient information is later created, the SIDs will be different. |
physicalDeliveryOfficeName | Office Name/Location. The user’s office location in their place of business. (e.g. Building D, 4th Floor.) |
pwdLastSet | Password Last Set. Provides a timestamp representing the last time the user’s password was changed. NOTE: This isn’t a standard .NET tick count value – it is the number of 100 nano-second intervals that have elapsed since 1st January 1601 (UTC). |
sAMAccountName | Account Name The account username used when logging into legacy systems (Windows NT, Windows 9X, LAN Manager, etc.) |
sn | Surname. The user’s last name. |
telephoneNumber | Telephone Number. The user’s contact telephone number. |
title | User’s Job title. The user’s job title or role. |
userPrincipalName | User Principal Name. The user’s ‘user principal name’ (UPN). This is an Internet-style login name based on RFC 822. |
LDAP queries are structured in a specific way. Each term (or collection of terms) is normally encapsulated in brackets. Where terms are grouped together, the logical operator that should be associated appears before the terms. Terms are normally defined as name-value pairs (i.e. (name=value)).
Some examples of LDAP queries… The first example will match any users based on an ambiguous name of ‘smith’ in various property fields in the AD objects. (The additional ‘userProxy’ option caters for ADAM/AD-LDS where objects might be of class ‘userProxy’ instead of the usual ‘user’ class in the main Active Directory schema.) Something to be aware of with ambiguous name matching is that it will return results based on sub-set matching (so smith, smith-jones, smithly, etc. would all match!) The second example will match any distribution groups with a name of ‘Approvers’.
(&(anr=smith)(|(objectClass=user)(objectClass=userProxy)))
(&(cn=Approvers)(objectClass=group)(|(groupType=2)(groupType=4)(groupType=8)))
Code language: plaintext (plaintext)
Querying Active Directory
Now we’ve covered off the main operators and identifiers that are likely to be used when querying an AD schema, let’s look at how we can query the schema in .NET. The worked example below demonstrates a generic search capability and also how to customise the query results for your own purposes.
using System;
using System.DirectoryServices;
// Only need the System.Configuration directive (and reference)
// if you want to load settings from app.config.
using System.Configuration;
// NOTES:
// (1) Groups are distribution groups by default.
// (2) Group types can be OR'd with 2147483648 (0x80000000)
// to convert to a security group ID. For 32-bit values this
// produces negative numbers, like below!
// (3) Two additional group types exist that aren't being
// considered here. (16=APP_BASIC group, 32=APP_QUERY group - both
// for Windows Server Authorization Manager.)
public enum DirectoryGroupType
{
Unknown = 0,
DistributionGroupGlobal = 2,
DistributionGroupDomainLocal = 4,
DistributionGroupUniversal = 8,
SecurityGroupGlobal = -2147483646,
SecurityGroupDomainLocal = -2147483644,
SecurityGroupUniversal = -2147483640,
}
public class User
{
public string AccountName { get; set; }
public string Department { get; set; }
public string Email { get; set; }
public string DisplayName { get; set; }
public string SystemId { get; set; }
public string UniqueName { get; set; }
}
public class Group
{
public DirectoryGroupType GroupType { get; set; }
public string Name { get; set; }
public List<string> Members { get; set; }
public string UniqueName { get; set; }
}
public class DomainSearch
{
private DirectoryEntry schema = null;
/// <summary>
/// Creates a new instance of the <see cref="DomainSearch"/> class,
/// pulling connection details from the application configuration file.
/// </summary>
public DomainSearch()
{
this.schema = new DirectoryEntry(
ConfigurationManager.AppSettings["DirectorySchemaPath"],
ConfigurationManager.AppSettings["DirectoryServiceAccount"],
ConfigurationManager.AppSettings["DirectoryServiceAccountPassword"]);
}
/// <summary>
/// Creates a new instance of the <see cref="DomainSearch"/> class,
/// explicitly configuring the schema connection.
/// </summary>
/// <param name="path">
/// The LDAP path to the Active Directory schema
/// (e.g. LDAP://domain-name or LDAP://machine-name/DC=apps,DC=domain,DC=local.)
/// </param>
/// <param name="username">
/// The service account username to log into the Active Directory schema with.
/// </param>
/// <param name="password">
/// The service account password to log into the Active Directory schema with.
/// </param>
public DomainSearch(string path, string username, string password)
{
this.schema = new DirectoryEntry(path, username, password);
}
/// <summary>
/// Searches the Active Directory schema for results that match the
/// supplied LDAP query.
/// </summary>
/// <param name="query">
/// The query to run (in LDAP syntax.)
/// </param>
/// <returns>A collection of results.</returns>
public List<SortedList<string, object>> Find(string query)
{
if (this.schema == null) return new List<SortedList<string, object>>();
DirectorySearcher searcher = null;
SearchResultCollection results = null;
List<SortedList<string, object>> data = new List<SortedList<string, object>>();
try
{
searcher = new DirectorySearcher(this.schema);
searcher.Filter = query;
searcher.SearchScope = SearchScope.Subtree;
results = searcher.FindAll();
SortedList<string, object> values = null;
List<object> multiValues = null;
foreach (SearchResult result in results)
{
values = new SortedList<string, object>();
foreach (string name in result.Properties.PropertyNames)
{
multiValues = new List<object>();
foreach (object value in result.Properties[name])
{
multiValues.Add(value);
}
values.Add(
name,
multiValues.Count == 1
? multiValues[0]
: multiValues.ToArray());
}
data.Add(values);
}
}
finally
{
// Make sure the unmanaged resources are formally released
// to avoid memory leaks!
if (searcher != null)
{
searcher.Dispose();
searcher = null;
}
if (results != null)
{
results.Dispose();
results = null;
}
// Although this isn't strictly necessary, it doesn't
// hurt to reclaim the resources and make sure those
// pesky COM references are formally released!
GC.Collect();
}
return data;
}
/// <summary>
/// Searches the Active Directory schema for users matching the supplied name.
/// </summary>
/// <param name="name">
/// The name to use as a basis for the search.
/// </param>
/// <returns>A collection of results.</returns>
public List<User> FindUsers(string name)
{
if (this.schema == null ||
string.IsNullOrEmpty(name) ||
name.Trim() == "")
{
return new List<User>();
}
if (name.Contains("*"))
{
throw new ArgumentException(
"Wildcards aren't supported for user searching - the " +
"query already uses an ambiguous name lookup.");
}
string query = string.Format(
"(&(anr={0})(|(objectClass=user)(objectClass=userProxy)))", name);
DirectorySearcher searcher = null;
SearchResultCollection results = null;
List<User> users = new List<User>();
try
{
searcher = new DirectorySearcher(this.schema);
searcher.Filter = query;
searcher.SearchScope = SearchScope.Subtree;
results = searcher.FindAll();
foreach (SearchResult result in results)
{
users.Add(new User {
AccountName = result.Properties["sAMAccountName"].Count > 0
? result.Properties["sAMAccountName"][0].ToString()
: "",
Department = result.Properties["department"].Count > 0
? result.Properties["department"][0].ToString()
: "",
Email = result.Properties["mail"].Count > 0
? result.Properties["mail"][0].ToString()
: "",
DisplayName = result.Properties["displayName"].Count > 0
? result.Properties["displayName"][0].ToString()
: "",
SystemId = result.Properties["objectSid"].Count > 0
? result.Properties["objectSid"][0].ToString()
: "",
UniqueName = result.Properties["distinguishedName"].Count > 0
? result.Properties["distinguishedName"][0].ToString()
: ""
});
}
}
finally
{
// Make sure the unmanaged resources are formally released to
// avoid memory leaks!
if (searcher != null)
{
searcher.Dispose();
searcher = null;
}
if (results != null)
{
results.Dispose();
results = null;
}
// Although this isn't strictly necessary, it doesn't
// hurt to reclaim the resources and make sure those
// pesky COM references are formally released!
GC.Collect();
}
return users;
}
/// <summary>
/// Searches the Active Directory schema for groups matching the supplied name.
/// </summary>
/// <param name="name">
/// The name to use as a basis for the search.
/// </param>
/// <returns>A collection of results.</returns>
public List<Group> FindGroups(string name)
{
if (this.schema == null ||
string.IsNullOrEmpty(name) ||
name.Trim() == "")
{
return new List<Group>();
}
if (name.Contains("*"))
{
throw new ArgumentException(
"Wildcards aren't supported for user searching - the " +
"query already uses an ambiguous name lookup.");
}
const string GroupTypes =
"(groupType=2)" +
"(groupType=4)" +
"(groupType=8)" +
"(groupType=-2147483646)" +
"(groupType=-2147483644)" +
"(groupType=-2147483640)";
string query = string.Format(
"(&(cn={0})(objectClass=group)(|{1}))", name, GroupTypes);
DirectorySearcher searcher = null;
SearchResultCollection results = null;
List<Group> groups = new List<Group>();
Group group = null;
try
{
searcher = new DirectorySearcher(this.schema);
searcher.Filter = query;
searcher.SearchScope = SearchScope.Subtree;
results = searcher.FindAll();
foreach (SearchResult result in results)
{
group = new Group {
Name = result.Properties["cn"].Count > 0
? result.Properties["cn"][0].ToString()
: "",
UniqueName = result.Properties["distinguishedName"].Count > 0
? result.Properties["distinguishedName"][0].ToString()
: "",
GroupType = result.Properties["groupType"].Count > 0
? (DirectoryGroupType)int.Parse(result.Properties["groupType"][0].ToString())
: DirectoryGroupType.Unknown,
Members = new List<string>()
};
foreach (object member in result.Properties["member"])
{
group.Members.Add(member.ToString());
}
}
}
finally // Make sure the unmanaged resources are formally released to avoid memory leaks!
{
// Make sure the unmanaged resources are formally released to
// avoid memory leaks!
if (searcher != null)
{
searcher.Dispose();
searcher = null;
}
if (results != null)
{
results.Dispose();
results = null;
}
// Although this isn't strictly necessary, it doesn't
// hurt to reclaim the resources and make sure those
// pesky COM references are formally released!
GC.Collect();
}
return groups;
}
}
Code language: C# (cs)
That’s all there is to it! We can now query an Active Directory schema for whatever information we want.