Common System.DirectoryServices Patterns
Common Patterns in System.DirectoryServices
Searching the Directory:
Searching 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();
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();