Gabe's Code

Stuff I've learned along the way


profile for Gabriel Luci at Stack Overflow, Q&A for professional and enthusiast programmers

Active Directory: Better Performance

One common complaint that pops up when programming against Active Directory is that it is slooowww.

When it comes to programming against AD, or any LDAP directory for that matter, two things will slow down performance more than anything else:

  1. The number of requests made to the server, and
  2. The amount of data returned.

Let’s talk about ways you a can minimize both.

While this article concentrates on .NET and specifically C#, the principals apply to any programming language that can make LDAP queries.

Don’t use System.DirectoryServices.AccountManagement

The AccountManagement namespace (classes like UserPrincipal and PrincipalSearcher) is a wrapper around the System.DirectoryServices namespace (DirectoryEntry and DirectorySearcher). It’s designed to make things easier on you. To some extent that is certainly true, but it comes at the cost of control. It is almost always slower than using the System.DirectoryServices classes directly.

Use System.DirectoryServices

You have much more control over how many network requests are made, and how much data gets retrieved by using DirectoryEntry and DirectorySearcher directly. However, you do need to be careful in how you use them, since it’s easy to write code that performs poorly. We’ll talk about some principals here that really apply to LDAP in general and not just .NET or Active Directory.

If you ask for nothing, you get everything

DirectorySearcher has a property called PropertiesToLoad. This is a list of the attributes that you want to retrieve in the search for each object found. But here’s the trick: if you don’t add anything to the list, it will return every attribute (except constructed attributes).

At best this is a slight waste of bandwidth. At worst, this can get very expensive. For example, if your organization uses the thumbnailPhoto attribute to store pictures of everyone (this is what will show up in Outlook, for example), then that can be up to 100kB of data that is returned for every result found, regardless of if you actually use that information.

You won’t really notice anything if you’re just searching for one user. But if you are, say, building a report of every user object on your domain, then this will add up!

Here is a simple example of returning the email address of every user on the domain:

public IEnumerable<string> EveryEmailAddress() {
    var search = new DirectorySearcher(new DirectoryEntry()) {
        PageSize = 1000,
        Filter = "(objectClass=user)"
    };
    search.PropertiesToLoad.Add("mail");
    
    using (var results = search.FindAll()) {
        foreach (SearchResult result in results) {
            if (result.Properties.Contains("mail")) {
                yield return (string) result.Properties["mail"][0];
            }
        }
    }
}

The lesson? Always add something to PropertiesToLoad.

If you ask for one thing, you get everything

This is the same principal as above, but with DirectoryEntry. Each instance of DirectoryEntry holds a cache of the attributes. When you access the Properties collection to read an attribute, it checks the cache first to see if it already has the value. If it doesn’t, it retrieves every attribute (except constructed attributes).

To avoid this, use RefreshCache() to load specific properties into the cache before you use them. This example shows how to retrieve only the mail attribute before using it:

ds.RefreshCache(new [] { "mail" });
var email = (string) ds.Properties["mail"]?.Value;

Don’t use GetDirectoryEntry()

Because of the two points above, don’t use SearchResult.GetDirectoryEntry() just to read attributes from an object. Take the following code for example:

//This is Bad™
public IEnumerable<string> EveryEmailAddress() {
    var search = new DirectorySearcher(new DirectoryEntry()) {
        PageSize = 1000,
        Filter = "(objectClass=user)"
    };
    
    using (var results = search.FindAll()) {
        foreach (SearchResult result in results) {
            var de = result.GetDirectoryEntry();
            
            yield return (string) de.Properties["mail"].Value;
        }
    }
}

We know that the search will return every attribute for every user found. Then we’re using GetDirectoryEntry() to create a new DirecotryEntry object with an empty cache. So once we do de.Properties["mail"], it goes back out to AD and gets every attribute again.

Even if we apply the two rules above, we will still be making two network requests when we only need to make one. The only reason to use GetDirectoryEntry() is if you’re going to modify the object.

Clean up after yourself

The documentation of SearchResultCollection says:

Due to implementation restrictions, the SearchResultCollection class cannot release all of its unmanaged resources when it is garbage collected. To prevent a memory leak, you must call the Dispose method when the SearchResultCollection object is no longer needed.

For this reason, you will see in all my examples that I put the call to DirectorySearcher.FindAll() in a using block.

Although DirectoryEntry also implements IDisposable, you don’t normally need to bother disposing it. Garbage collection does a good job of it. However, if you are looping over a large number of accounts and creating a new DirectoryEntry each time, garbage collection won’t have time to run until the loop finishes. That means that all of those DirectoryEntry objects will be adding up in memory and can slow down your application due to your application having to constantly as the OS for more memory.

An example of this is the code in my Find all the members of a group post where it loops through the memberOf attribute and creates a new DirectoryEntry object to check if it’s a group. In these cases, put the DirectoryEntry in a using block.

Ask for as much as you can at a time

Let’s say you have a list of usernames and you need to get the email address for all of them. You could make a separate search for each username, or you could combine all the usernames into one LDAP query and find them all at once:

(|(sAMAccountName=username1)(sAMAccountName=username2)(sAMAccountName=username3)(sAMAccountName=username4))

How long can you make your query? Well, there is no hard limit, but the entire LDAP request (not just the query string, but everything that goes along with it) must be less than 10MB. It is unlikely you’d ever hit that, but if you’re worried, break them into chunks (of maybe 50) and search for each chunk at a time.

As an example, here is a method that will take a collection of usernames and get all their email addresses in blocks of 50 at a time:

public IEnumerable<string> GetEmailAddressesE(IEnumerable<string> usernames) {
    var filter = new StringBuilder();
    var numUsernames = 0;
    
    var e = usernames.GetEnumerator();
    var hasMore = e.MoveNext();
    
    while (hasMore) {
        var username = e.Current;
        filter.Append($"(sAMAccountName={username})");
        numUsernames++;
        
        hasMore = e.MoveNext();
        if (numUsernames == 50 || !hasMore) {
            var search = new DirectorySearcher(new DirectoryEntry()) {
                PageSize = 1000,
                Filter = $"(&(objectClass=user)(|{filter}))"
            };
            search.PropertiesToLoad.Add("mail");
            
            using (var results = search.FindAll()) {
                foreach (SearchResult result in results) {
                    yield return (string) result.Properties["mail"][0];
                }
            }
            filter.Clear();
            numUsernames = 0;
        }
    }
}

You do have to be aware that because you’re doing a search doesn’t necessarily mean you will get a result. So you may decide you need to verify that you indeed got all the results you expected. You could just compare numbers (if I asked for 100 accounts, did I get 100?) or you could change this to return a Dictionary<string, string> with the sAMAccountName as the key and the mail attribute as the value so you can check which ones are missing. But all of that verification is done in memory and it still waaayy faster than doing a separate search for each account.

3 comments

oldovets on

Hey, Gabriel. Just read your articles about Active Directory performance and membership and would like to say, that you’re doing a great job. Keep going! One little thing I would like to share with you regarding performance is connection caching. See: https://docs.microsoft.com/en-us/windows/desktop/adsi/connection-caching. In terms of C# it means that we need to create and maintain a DirectoryEntry for every domain controller we connect to for all the time period we make LDAP queries to this DC. Singleton required here: every new created DirectoryEntry instance will reuse opened LDAP connection under the hood. This solves two problems. 1. All LDAP request will (almost always) share one single connection, and performance will go through the roof. 2. DoS. A domain controller applies a throttling policy to LDAP requests. Creating numerous LDAP connections from one computer (even if they are properly disposed) will trigger the policy and at some point we will be unable to connect to the DC

Gabe on

That’s really interesting! I didn’t know about that, although most of my code over the years was probably using it anyway :) Thanks for pointing that out and thank you for the compliments!

Leave a comment

Your email address is used to display your Gravatar, if applicable, and subscribe you to replies using the Mailgun web service, which you are free to unsubscribe from when you get any emails. Your email address will not be displayed publicly or shared with anyone else.