Our application has a process which fetches all users from Active Directory and updates the relevant SQL tables with their information. The process at nights and it was written a few years ago - so it's legacy code which works and "if it ain't broken, don't fix it". However we're introducing a new feature to our application which requires modifications to this code, and since it hasn't been touched for years I thought I might as well clean it up a little.
Said process runs ONLY during the night except for rare server faults, in which case we have to run it manually during the day. The process uses the good old System.DirectoryServices
library to do its job, and although it works, it runs quite slowly.
I thought about using the newer System.DirectoryServices.AccountManagement
library instead, so I started rewriting the whole process (a few hundreds lines of code) and I was amazed to see that PrincipalSearcher
dramatically outperforms DirectorySearcher
.
I've been trying to look for the reason and came upon the following SO answer which gives a comparison between the two, stating that DirectorySearcher
should be faster than PrincipalSearcher
.
I fired up a test project to make sure I was not hallucinating:
class Program
{
static void Main(string[] args)
{
// New stuff
var context = new PrincipalContext(ContextType.Domain, "mydomain.com");
var properties = new[] { "cn", "name", "distinguishedname", "surname", "title", "displayname" };
var i = 0;
var now = DateTime.Now;
new Thread(delegate()
{
while (true)
{
Console.Write("\r{0} ms, {1} results", (DateTime.Now - now).TotalMilliseconds, i);
Thread.Sleep(1000);
}
}).Start();
using (var searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
var underlying = searcher.GetUnderlyingSearcher() as DirectorySearcher;
underlying.PageSize = 1000;
underlying.PropertiesToLoad.Clear();
underlying.PropertiesToLoad.AddRange(properties);
underlying.CacheResults = false;
using (var results = searcher.FindAll())
{
foreach (var result in results)
{
i++;
}
}
}
Console.WriteLine("It took {0}", (DateTime.Now - now).TotalMilliseconds);
now = DateTime.Now;
i = 0;
// Old stuff
var root = new DirectoryEntry("LDAP://DC=mydomain,DC=com");
var filter = "(&(objectCategory=user)(objectClass=user))";
using (var searcher = new DirectorySearcher(root, filter, properties))
{
searcher.PageSize = 1000;
searcher.CacheResults = false;
using (var results = searcher.FindAll())
{
foreach (var result in results)
{
i++;
}
}
}
Console.WriteLine("It took {0}", (DateTime.Now - now).TotalMilliseconds);
}
}
Querying some thousand users, the results were around 0.9ms per user with PrincipalSearcher
(around 30 seconds for ~34k users) and around 5.2ms per user with DirectorySearcher
(around 2 minutes and 30 seconds for ~34k users) - PrincipalSearcher
being almost six times faster.
I tried debugging and comparing the PrincipalSearcher
's underlying DirectorySearcher
with the one I created and they seemed pretty much similar.
I tried examining further ahead and it seems that if I use the search root from the PrincipalSearcher
's underlying searcher, then the DirectorySearcher
I create actually outperforms the PrincipalSearcher
:
// ...
DirectoryEntry psRoot;
using (var searcher = new PrincipalSearcher(new UserPrincipal(context)))
{
var underlying = searcher.GetUnderlyingSearcher() as DirectorySearcher;
psRoot = underlying.SearchRoot;
// ...
}
// ...
using (var searcher = new DirectorySearcher(psRoot, filter, properties))
{
// ...
}
While debugging I found out that the search roots are largely the same - i.e., they represent the same domain.
What could cause the search speed to slow down like this?