Question

I have a loop that retrieves some info from ActiveDirectory. It turned out to be a big performance bottleneck.

This snippet (inside a loop that executed it 31 times) took 00:01:14.6562500 (1 minute and 14 seconds):

SearchResult data = searcher.FindOne();
System.Diagnostics.Trace.WriteLine(PropsDump(data));

Replacing it with this snippet brought it down to 00:00:03.1093750 (3 secconds):

searcher.SizeLimit = 1;
SearchResultCollection coll = searcher.FindAll();
foreach (SearchResult data in coll)
{
    System.Diagnostics.Trace.WriteLine(PropsDump(data));
}

The results are exactly identical, the same properties are returned in the same order. I found some info on memory leaks in another thread, but they did not mention performance (I'm on .Net 3.5).


The following is actually a different question but it gives some background on why I'm looping in the first place:

I wanted to get all the properties in one single query, but I cannot get the DirectorySearcher to return all the wanted properties in one go (it omits about 30% of the properties specified in PropertiesToLoad (also tried setting it in the constructor wich makes no difference), I found someone else had the same problem and this is his solution (to loop through them). When I loop through them like this, either using FindOne() or FindAll() I do get all the properties, But actually it all feels like a workaround.

Am I missing something?


Edit:

Seems like the problem was with the way I got the first DirectoryEntry on which I was using the DirectorySearcher.

This was the code that caused the DirectorySearcher only to return some of the properties:

private static DirectoryEntry GetEntry() {
    DirectoryContext dc = new DirectoryContext(DirectoryContextType.DirectoryServer, "SERVERNAME", "USERNAME", "PASSWORD");
    Forest forest = Forest.GetForest(dc);
    DirectorySearcher searcher = forest.GlobalCatalogs[0].GetDirectorySearcher();

    searcher.Filter = "OU=MyUnit";
    searcher.CacheResults = true;
    SearchResultCollection coll = searcher.FindAll();
    foreach (SearchResult m in coll)
    {
        return m.GetDirectoryEntry();
    }
    throw new Exception("DirectoryEntry not found");
}

After replacing that big mouthfull with just this line, the DirectorySearcher returned all the properties and looping was no longer needed:

private static DirectoryEntry GetEntry2()
{
    return new DirectoryEntry(@"LDAP://SERVERNAME/OU=MyUnit,DC=SERVERNAME,DC=local", "USERNAME", "PASSWORD");
}

Now it takes less than one 18th of a second to get all wanted properties of 31 entries. So, it seems that two different instances of the same DirectoryEntry can give different results depending on the way it was constructed... feels a bit creepy!


Edit

Used JetBrains DotPeek to look at the implementation. The FindOne function starts like this:

public SearchResult FindOne()
{
  SearchResult searchResult1 = (SearchResult) null;
  SearchResultCollection all = this.FindAll(false);
  ...

My first reaction was Argh! no wonder... but then I noticed the argument. FindAll has a private version that accepts a boolean, this is the start of FindAll:

[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public SearchResultCollection FindAll()
{
  return this.FindAll(true);
}

private SearchResultCollection FindAll(bool findMoreThanOne)
{
  ... // other code
  this.SetSearchPreferences(adsSearch, findMoreThanOne);

So this gives slightly more insight, but does not really explain much.

Was it helpful?

Solution

New answer for the new stuff. Your first method was using the Global Catalog, so it was like using

private static DirectoryEntry GetEntry3()
{
    return new DirectoryEntry(@"GC://SERVERNAME/OU=MyUnit,DC=SERVERNAME,DC=local", "USERNAME", "PASSWORD");
}

Also, Microsoft LDAP libraries usually have a way to tell it whether you're giving the server name, because it makes some optimizations that can be really slow if you don't say it was a server name. For DirectoryEntry, it's the constructor with the most arguments and AuthenticationTypes.ServerBind.

OTHER TIPS

Looping is not a good idea. I'm going to analyze that guy's code:

objGroupEntry = sr.GetDirectoryEntry();
dso = new DirectorySearcher(objGroupEntry);

dso.ClientTimeout = TimeSpan.FromSeconds(30);

dso.PropertiesToLoad.Add("physicalDeliveryOfficeName");
dso.PropertiesToLoad.Add("otherFacsimileTelephoneNumber");
dso.PropertiesToLoad.Add("otherTelephone");
dso.PropertiesToLoad.Add("postalCode");
dso.PropertiesToLoad.Add("postOfficeBox");
dso.PropertiesToLoad.Add("streetAddress");
dso.PropertiesToLoad.Add("distinguishedName");

dso.SearchScope = SearchScope.OneLevel;

dso.Filter = "(&(objectClass=top)(objectClass=person)(objectClass=organizationalPerson)(objectClass=user))";
dso.PropertyNamesOnly = false;

SearchResult pResult = dso.FindOne();

if (pResult != null)
{
    offEntry = pResult.GetDirectoryEntry();

    foreach (PropertyValueCollection o in offEntry.Properties)
    {
        this.Controls.Add(new LiteralControl(o.PropertyName + " = " + o.Value.ToString() + "<br/>"));
    }
}

I don't know why he's doing two searches, but let's assume there's a good reason. He should have gotten those properties from the SearchResult, not from the return value of pResult.GetDirectoryEntry, because its a completely new object.

string postalCode = pResult.Properties["postalCode"][0] as string;
List<string> otherTelephones = new List<string>();
foreach(string otherTelephone in pResult.Properties["otherTelephone"])
{
    otherTelephones.Add(otherTelephone);
}

If you insist on getting the DirectoryEntry, then ask for all of the properties at once with RefreshCache:

offEntry = pResult.GetDirectoryEntry();
offEntry.RefreshCache(propertyNameArray);

If none of that helps, look at your filters and see if you can use the BaseLevel scope.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top