Common Patterns in System.DirectoryServicesSearching the Directory:
- Create a DirectoryEntry that represents your SearchRoot. Your
searches will be rooted to this location and will have the same
permissions as the bound SearchRoot. Failure to specify a
SearchRoot (bad practice) means you will attempt to search the entire
current domain (as specified by RootDSE defaultNamingContext) and can
be problematic for ASP.NET applications.
- Create your LDAP query.
- Optionally specify PropertiesToLoad collection - this will be
more efficient if specified. However, it will return all
available, non-constructed attributes if left null (Nothing in VB.NET).
- For v1.1 S.DS, use the DirectorySearcher.FindAll() for all
searches. The DirectorySearcher.FindOne() has a memory leak in
certain situations - .NET 2.0 is unaffected and safe to use.
Sample For Retrieving Only 1 Result:DirectoryEntry searchRoot = new DirectoryEntry(
"LDAP://server/OU=People,DC=domain,DC=com", //searches will be rooted under this OU
"domain\\user", //we will use these credentials
"password",
AuthenticationTypes.Secure
);
using (searchRoot) //we are responsible to Dispose this!
{
DirectorySearcher ds = new DirectorySearcher(
searchRoot,
"(sn=Smith)", //here is our query
new string[] {"sn"} //optionally specify attributes to load
);
ds.SizeLimit = 1;
SearchResult sr = null;
using (SearchResultCollection src = ds.FindAll())
{
if (src.Count > 0)
sr = src[0];
}
if (sr != null)
{
//now use your SearchResult
}
}Sample For Retrieving Multiple Results:DirectoryEntry searchRoot = new DirectoryEntry(
"LDAP://server/OU=People,DC=domain,DC=com", //searches will be rooted under this OU
"domain\\user", //we will use these credentials
"password",
AuthenticationTypes.Secure
);
using (searchRoot) //we are responsible to Dispose this!
{
DirectorySearcher ds = new DirectorySearcher(
searchRoot,
"(sn=Smith)", //here is our query
new string[] {"sn"} //optionally specify attributes to load
);
ds.PageSize = 1000; //enable paging for large queries
using (SearchResultCollection src = ds.FindAll())
{
foreach (SearchResult sr in src)
{
//use the SearchResult here
}
}
}Notice
the common pattern here and how all DirectoryEntry and
SearchResultCollection classes are Disposed. Failure to do so can
leak memory.
Reading Attributes:One
of the most common issues that people run into is getting an error
trying to read the attribute when it does not exist. It is
important to understand there is no concept of null attributes in
Active Directory - the attribute either exists on the object or it
doesn't - it is never null. This however can be confusing because
trying to read a non-existant attribute culminates in a null reference
exception in .NET.
To protect against this, use this pattern:
DirectoryEntry entry = new DirectoryEntry(...);
using (entry)
{
if (entry.Properties.Contains("property"))
{
//now safe to access "property"
//cast as appropriate to string, byte[], int, etc...
//object o = entry.Properties["property"].Value;
}
}This same pattern is appropriate for the SearchResult as well:
SearchResult result;
//... fill the result
if (result.Properties.Contains("property"))
{
//now safe to access "property"
//cast as appropriate to string, byte[], int, etc...
//object o = result.Properties["property"][0];
}
Depending
on the attribute, the PropertyValueCollection (or
ResultPropertyValueCollection) will return either a single value or
multiple values. To actually get a value from the DirectoryEntry,
we need to cast the 'object' to whatever type we are expecting.
Thus, we get something like:
DirectoryEntry entry = new DirectoryEntry(...);
using (entry)
{
if (entry.Properties.Contains("sn"))
{
string lastName = entry.Properties["sn"][0].ToString();
}
}
In this example, I cast the 'sn' attribute to the lastname. It
makes 2 assumptions: 1.) The value can be interpreted as a string, and
2.) I am only interested in a single valued attribute.
The pattern changes slightly if we are looking to read the values from a multi-valued attribute:
DirectoryEntry entry = new DirectoryEntry(...);
using (entry)
{
if (entry.Properties.Contains("mail"))
{
foreach (object o in entry.Properties["mail"])
{
//iterate through each value in the 'mail' attribute as a string
Response.Output.Write("mail: {0}", o.ToString());
}
}
}
Notice that I am iterating through all the objects and casting to
string in this case. If the attribute was something else, it
might be appropriate to cast to a byte[] array or an integer,
etc. It just depends on what it is, but the casting from object
to whatever type you need is up to you.
Binding to a Data Control:In order to bind to things like a dropdown list or a datagrid, we need to perform the search first and manually create a datasource to use. Since AD is hierarchical and datacontrols are not, we generally need to think about how we will model the data when rows/columns won't make any sense.
Here is a sample function that will search Active Directory given a filter for searching and the attributes to retrieve. I called it 'FindUsers' for lack of a better name about 5 years ago and decided not to change it to confuse people. In reality, it can be used to find anything, not just users. It returns a DataSet and optionally caches it for faster lookups next time.
public DataSet FindUsers(string sFilter, string[] columns, string path, bool useCached)
{
//try to retrieve from cache first
HttpContext context = HttpContext.Current;
DataSet userDS = (DataSet)context.Cache[sFilter];
if((userDS == null) || (!useCached))
{
//setup the searching entries
DirectoryEntry deParent = new DirectoryEntry(path);
//deParent.Username = Config.Settings.UserName;
//deParent.Password = Config.Settings.Password;
deParent.AuthenticationType = AuthenticationTypes.Secure;
DirectorySearcher ds = new DirectorySearcher(
deParent,
sFilter,
columns,
SearchScope.Subtree
);
ds.PageSize = 1000;
using(deParent)
{
//setup the dataset that will store the results
userDS = new DataSet("userDS");
DataTable dt = userDS.Tables.Add("users");
DataRow dr;
//add each parameter as a column
foreach(string prop in columns)
{
dt.Columns.Add(prop, typeof(string));
}
using (SearchResultCollection src = ds.FindAll())
{
foreach(SearchResult sr in src)
{
dr = dt.NewRow();
foreach(string prop in
columns)
{
if(sr.Properties.Contains(prop))
{
dr[prop] =
sr.Properties[prop][0];
}
}
dt.Rows.Add(dr);
}
}
}
//cache it for later, with sliding 3 minute window
context.Cache.Insert(sFilter, userDS, null, DateTime.MaxValue, TimeSpan.FromSeconds(180));
}
return userDS;
}
Now, just place a datagrid (or dropdown) on your page, and with a few lines of code, you have a searcher:
//sample use
string qry = String.Format("(&(objectCategory=person)(givenName={0}*))", txtFirstName.Text);
string[] columns = new string[]{"givenName", "sn", "cn", "sAMAccountName", "telephoneNumber", "l"}
string ldapPath = "LDAP://dc=mydomain";
DataSet ds = FindUsers(qry, columns, ldapPath, true);
DataGrid1.DataSource = ds;
DataGrid1.DataBind();
I will try to keep this thread updated with the most common questions.