C# Asynchronous Multi-Threaded Active Directory User/Group Browser


Part I. Querying Active Directory and Building an Object Pool

Querying Active Directory can be time consuming and in order to avoid freezing the UI, we will run the AD queries on a worker thread. Below is the code to perform the query finding all the Active Directory groups.  The DirectorySearcher and SearchResultCollection are members of the System.DirectoryServices namespace in the System.DirectoryServices dll (which has to be explicitly added to a project). 

private static String[] GetAdGroupsFromAD()
{
    DirectorySearcher
        srch = new DirectorySearcher();

    const string
        query = "(&(objectCategory=group))";

    srch.Filter = query;

    SearchResultCollection
        sResult = srch.FindAll();

    List<String>
        results = new List<string>();

    if (null != sResult)
        for (int i = 0; i < sResult.Count; i++)
            if (null != sResult[i].Properties["name"] && sResult[i].Properties["name"].Count == 1)
                results.Add(sResult[i].Properties["name"][0] as String);

    return results.ToArray();
}

This query can take a long time to execute and we don't expect the results to change very frequently so we can use a caching mechanism to store the results. 

Because the object pool (the cache) will eventually be hit by an asynchronous call and may have more than one thread at a time accessing it, we'll have to take extra precautions to make sure the state of the pool is not corrupted.  We will protect the pools by creating synchronization objects for the express purpose of locking down the pool by ensuring only one thread at a time is allowed which guarantees the state is not corrupted while reading the data.

private static Object m_groupPoolSync = new Object();

Every time we read or write information to the group pool we will lock on the m_groupPoolSync object.  This is preferable to locking the whole class because we have more granular control of the critical section and we only lock what is absolutely necessary for the shortest amount of time. Also, we will have a flag that tells us whether the pool has been initialized so we can skip the AD query if we already have the data we need.

private static String[] GetAdGroups()
{

    List<String> results = new List<string>();

    lock (m_groupPoolSync)
    {
        if (!m_groupPoolInitialized)
        {
            foreach (String result in GetAdGroupsFromAD())
                m_groupPool.Add(result);

            m_groupPoolInitialized = true;
        }

        results.AddRange(m_groupPool);
    }

    return results.ToArray();
}

It may be necessasry to clear the pool at some point, so we should expose this functionality which is just a matter of setting the flag.  This is locked because there may be a thread in the GetAdGroups() method above that is busy querying AD.  If we did not lock the code below, the ClearPools() could execute during the AD query and the flag would first be set as m_groupPoolInitialized = false and then when the AD query completed in the above method, the flag would be reset to m_groupPoolInitialized = true which would cause our application to behave in unexpected ways.  I could see a valid argument being made for not locking the pool clearing as long as the behavior is obvious to other developers working with the code.

public static void ClearPools()
{
    lock (m_groupPoolSync)
        m_groupPoolInitialized = false;
}

Part II. Exposing the Long Running Process Asynchronously.

Now we want to provide access to our long running process asynchronously.  This will be accomplished through a delegate that will call our GetAdGroups() method.

private delegate String[] GetAdGroupsDelegate();

To make the async call using our delegate we create a new instance of the delegate by assigning it to the method we want to hit.  Next we use the BeginInvoke() method from the delegate to do the async call.  We'll pass the Delegate to the BeginInvoke() method in order to retrieve the results later:

public static IAsyncResult BeginGetAdGroups(AsyncCallback callback, Object state)
{
    GetAdGroupsDelegate mthd = GetAdGroups;
    return mthd.BeginInvoke(callback, new Object[] { mthd, state });
}

To get the results we'll first need to assert that the async call has completed.  Because we'll reuse this quite a bit we can encapsulate the functionality in a single utility method.

private static void AssertCompleted(IAsyncResult result)
{
    if (!result.IsCompleted)
        throw new InvalidOperationException("result has not completed");
}

We'll also have to  have a way to retrieve the delegate object that we passed in the BeginInvoke() method.  We can use a generic method to do create a second utility method that will be reusable.

private static T GetStateMethodItem<T>(Object value)
{
    Object[] state = value as Object[];

    if (null == state)
        throw new InvalidOperationException("Invalid AsyncState: Expecting Object[]");

    if (state.Length < 1)
        throw new InvalidOperationException("Invalid AsyncState: Object[] has no items");

    if (state[0].GetType() != typeof(T))
        throw new InvalidOperationException("Invalid AsyncState: Object[] is not of type " + typeof(T).ToString());

    return (T)state[0];
}

Finally, we'll use our two utility methods to retrieve the results of the async call.  Notice we are using the standard method signatures to wrap our original call asynchronously by prefacing the original method name [GetAdGroups()] with "Begin" [BeginGetAdGroups()] and "End" [EndGetAdGroups()].

public static String[] EndGetAdGroups(IAsyncResult result)
{
    AssertCompleted(result);
    GetAdGroupsDelegate mthd = GetStateMethodItem<GetAdGroupsDelegate>(result.AsyncState);
    return mthd.EndInvoke(result);
}

  Part III. Consuming the Async Helper Class with a Win Form

First, we'll need a form to show wile our async process is running.  The code for this form is attached to this article.

Next, we'll need to launch our form whenever there is a call to an async process.  We will also need to update the main form when the async call is complete.  With win forms, only the thread that created the window will be allowed to update it.  In order to accomplish this, we can use a delegate for a method to show the results of our async query and call it safely with the following utility method:

        private delegate void ShowResults();

        private void SafeInvoke(ShowResults rslts)
        {
            if (InvokeRequired)
                Invoke(rslts);
            else
                rslts.Invoke();
        }

Now to call the method we'll create an instance of the working form and use an anonymous method to handle the async result.  If you want more details on async programming you can read my articles on the subject Part I, Part II, Part III , Part IV, Part V, Part VI.

After the async call is launched, we'll show the working form as a dialog.  In the async response, we'll pass an anonymous method to the SafeInvoke() method to ensure the main window's thread updates the windows controls and closes the working form.

private void m_btnSearch_Click(object sender, EventArgs e)
{
    WorkingForm frm = new WorkingForm();
    String searchString = m_txtSearch.Text;

    ActiveDirectory.AdHelper.BeginGetAdGroups(delegate(IAsyncResult result)
    {
        SafeInvoke(delegate()
        {
            String[] values = Array.FindAll(AdHelper.EndGetAdGroups(result), delegate(String value)
            {
                return value.Contains(searchString);
            });

            Array.Sort(values);

            m_lstUsers.DataSource = new String[0];
            m_lstGroups.DataSource = values;
            frm.Hide();
        });
    }, null);

    frm.ShowDialog();
}

I hope you found this article useful. I'm by no means an expert with active directory and I imagine there are probably much more efficient queries that could be run against AD but the idea I wanted to get across here is the async approach and how to implement it in a windows project.  The accompanying code includes finding all the ad users and user and group membership in addition to finding the ad groups.


Until next time,
Happy coding