ActiveDirectory DirectorySearcher: why is FindOne() slower than FindAll() and why are properties omitted?

10.8k views Asked by At

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.

3

There are 3 answers

4
Sean Hall On BEST ANSWER

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.

1
Sean Hall On

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.

0
Silicon Dragon On

You can easily emulate FindOne() by using FindAll() and setting DirectorySearch.PageSize to 1 and the search returns just as fast as using FindOne().

Also, you can use the DirectoryEntry constructor (as above) and use GC:// instead of LDAP:// to speed it up a bit more...not much, but it's noticeable if you're loading a zillion objects or you're in a tight loop.

Also also, if you use a searcher, by default it'll return all properties if you don't pass-in a propertiesToLoad array. If you have a complex AD, though, you might get more properties than you want which slows down data xfer over the wire.

Lastly, about using GC, not all attributes propagate to GC. IIRC, all built-in ones do, but custom attributes certainly don't unless you explicitly configure them to do so.

Lastly lastly , another trick I use occasionally is use GC for fast searching, retrieving only the GUID, storing the GUIDs in a List collection to finish the search loop ASAP, then go back and load the objects using LDAP and GUID syntax: "LDAP://<GUID=12345678-1234-...>" and you can pull the object from a specific DC with this syntax, too: "LDAP://dc-xyz.mycompany.com/<GUID=12345678-1234-...>" (and yes, it works with GC as well).