Main Page
 The gatekeeper of reality is
 quantified imagination.

Stay notified when site changes by adding your email address:

Your Email:

Bookmark and Share
Email Notification
Project Directory Virtual List View
Purpose
The purpose of this tutorial is to show how to use a C# .Net 4.6 and a little used capability in DirectoryServices to conduct fast Active Directory (AD) searches and user retrieval with DirectoryVirtualListView, complete with pagination. Most domains I've encountered to date, especially with the hardware and virtualization power available today, have indexing enabled (which is what virtual list view relies on).

TIP: How do you tell if indexing is enabled if you are not a Active Directory domain administrator? If you've got an Exchange Server that serves the domain (Outlook), newer versions usually require indexing be enabled. To check, what you can do is: (1) hover over a linked name in an email message - for example, you can open a new message and in the "TO" field enter your Outlook email address and then hover over it, (2) click on the down arrow for the contact card, (3) click on membership, (4) click on an AD group, (5) a new pane should appear showing the members of the AD group you clicked on. Pretty nifty.

In many applications today (2019) the preferred choice for interacting with AD is to use AccountManagement. It is a great solution for minimizing the volume of code that you need to write (and maintain or build upon). Unfortunately, until Microsoft decides to overhaul or add new capability to it with speed in mind, it can get very SLOW when dealing with large volumes of AD data...volumes of data being commonplace in today's world. What I found equally disturbing was the common view that, in many cases today, one had to copy volumes of AD information into a SQL database server, constantly update it, to leverage the speed of a database server even when that was not really needed...or use the poorly documented and very old "ActiveDS" library - which may not be available anymore. Why spend lots of extra money on additional infrastructure, maintenance, dated libraries, "stale" data being used, and possibly increased complexity or SQL security concerns/liability with applicataions - if you don't need to?

This tutorial shows a little known and used feature of DirectoryServices (the more modern but slower competitor for many AD activities is AccountManagement) called DirectoryVirtualListView that is optimized for dealing with large volumes of AD data WITH the ability to paginate results WITHOUT needing a separate database server, etc. In that regard, incomplete usage documentation by Microsoft, apparently has not lent itself to higher levels of usage and I could see why.

If you are ready to see one of my approaches with using DirectoryVirtualListView (with AD pagination), please continue reading. Don't worry, I still prefer AccountManagement, but when technological roadblocks hinder speed sometimes you have to switch the way you think about using it.

Paginate Members of a Massive AD Group

In this scenario, let's say that you need to get the members of an AD group in paginated format. What I'll show below is (1) the calling functionality that you need to use, and, (2) the logic which can get a count of total members in an AD group (dependent upon the max size allowed - by default I believe it is 1000), and paginating through the members in the AD group. It has been tested on groups with thousands of members. Here we go!

Calling Functionality
The following are what your application code would integrate so that you can keep track of the current page number and AD group:

/// <summary>
/// Track the page of results requested
/// </summary>
private static int _gPage = 1;
public static int gPage
{
	get { return _gPage; }
	set { _gPage = value; }
}
/// <summary>
/// Track the group requested; if this changes then the gPage should be reset
/// </summary>
private static string _gGroup = String.Empty;
public static string gGroup
{
	get { return _gGroup; }
	set { _gGroup = value; }
}


Now, your calling functionality (such as a button click) should contain the following:

var rResultsLimit = 20; /* Only return 20 members at a time */
var gDomain = "LDAP://your.ad.domain"; /* Domain to query */
var gSize = 1000; /* The maximum number of members in the AD group; this may be superceded by a size setting in DirectoryServices or the Domain */
var currentGroup = "ADGroupName"; /* Name of the AD group to get members from */
/* If the current group requested is not what may have already been requested, set to the current group and update page number */
if (gGroup.ToLower().Trim() != currentGroup.ToLower().Trim()) {
	gPage = 1;
	gGroup = currentGroup;
}
var result = customADInterface.retrieveADGroupMembershipAndUserDetail(rResultsLimit, gDomain, gGroup, gSize, gPage);
if (result.ToLower() == "completed") {
	/* Increment page number in preparation for getting the next page of results */
	gPage++;
}
else if (result.ToLower() == "lastpage") {
	/* The last page of results has been reached, so there is no need in getting another page of results */
}
else if (result.ToLower() == "nodata") {
	/* Nothing was found; perhaps the name of the AD group was not correct... */
}


DirectoryVirtualListView Functionality
This is the logic used that interacts with DirectoryVirtualListView itself. The resultant paginated data may go a variety of places such as a DataTable shown here that you would need to call separately in your calling code. Only one AD attribute is being requested for a member in the AD group, so if you need to get more, you'll need to specify them:
public class customADInterface
{
	/// <summary>
	/// Instantiate the DataSet; will contain the DataTable of data
	/// </summary>
	public static System.Data.DataSet dataSetContainer = new DataSet();
	/// <summary>
	/// Store the actual number of members in the Group up to the maximum allowed
	/// </summary>
	private static int _countDomainGroupMembers = 0;
	public static int countDomainGroupMembers
	{
		get { return _countDomainGroupMembers; }
		set { _countDomainGroupMembers = value; }
	}
	/// <summary>
	/// Store the AD path of the AD Group
	/// </summary>
	private static string _fullADGroupPath = String.Empty;
	public static string fullADGroupPath
	{
		get { return _fullADGroupPath; }
		set { _fullADGroupPath = value; }
	}
	/// <summary>
	/// This performs the high level orchestration to retrieve the members of an AD group and various attributes, along with pagination.
	/// </summary>
	/// <param name="rLimit">20 - maximum number of members to retrieve</param>
	/// <param name="tDomain">LDAP://your.ad.domain</param>
	/// <param name="tGroup">DomainAdmins - name of AD group to get the members of</param>
	/// <param name="tGroupSizeLimit">1000 - maximum size of the AD group; the actual number of members may be less than this number so needing a specific number is not required as it will be determined</param>
	/// <param name="rCurrentPage">1 - the current page of members to retrieve</param>
	/// <returns>string - completed = data was found, nodata = no data was found, lastpage = last page of data found</returns>
	public static string retrieveADGroupMembershipAndUserDetails(int rLimit, string tDomain, string tGroup, int tGroupSizeLimit, int rCurrentPage)
	{
		/* Get arguments */
		var rResultsLimit = rLimit;
		var gDomain = tDomain;
		var gGroup = tGroup;
		var gGroupSizeLimit = tGroupSizeLimit;
		var rCurrentPageResults = rCurrentPage;
		var results = "nodata";
		/* Create DataTable and Column Names */
		System.Data.DataTable table = new DataTable();
		DataColumn column;
		DataRow row;

		column = new DataColumn();
		column.DataType = System.Type.GetType("System.String");
		column.ColumnName = "CN";
		column.ReadOnly = true;
		column.Unique = false;
		table.Columns.Add(column);

		// Clear any pre-existing content for the current iteration
		dataSetContainer.Tables.Clear();

		// Add the new DataTable to the DataSetContainer
		dataSetContainer.Tables.Add(table);

		// STEP 1: Get group path information and count of members in the group only if we do not already have that information; the initial query will be slower than follow-up queries.
		if (rCurrentPageResults == 1) {
			// Get the full AD path of the group in the domain
			DirectoryEntry rootEntry = new DirectoryEntry(gDomain);
			DirectorySearcher searcher = new DirectorySearcher(rootEntry);
			searcher.Filter = "(&(ObjectClass=Group)(CN=" + gGroup + "))";
			SearchResult rawADGroupPath = searcher.FindOne();
			fullADGroupPath = rawADGroupPath.Path.ToString().Replace(gDomain + "/", "");

			// Get count of members in the group
			searcher.Filter = "(&(ObjectClass=person)(memberOf=" + fullADGroupPath + "))";
			/* Attempting searcher.VirtualListView.ApproximateTotal did not return what was expected so loop instead */
			searcher.PropertiesToLoad.Add("cn");
			/* Sort is required or DirectoryVirtualListView will not work */
			searcher.Sort = new SortOption("cn", SortDirection.Ascending);
			searcher.VirtualListView = new DirectoryVirtualListView(0, gGroupSizeLimit, 1);
			SearchResultCollection tmpData = searcher.FindAll();
			foreach (SearchResult step in tmpData) { countDomainGroupMembers++; }
		}
		// STEP 2: Get the members in the AD group in paginated form
		var beforeCountPosition = 0;
		var afterCountPosition = countDomainGroupMembers;
		var startOffsetPosition = 0;
		// Determine the block of members to return from the AD group
		if (rCurrentPageResults == 1) { startOffsetPosition = 1; }
		else {
			startOffsetPosition = (rResultsLimit * (rCurrentPageResults - 1)) + 1;
		}
		afterCountPosition = rResultsLimit - 1;
		// Determine whether to get results or not
		var resultsLastPageReached = 0;
		if (startOffsetPosition < countDomainGroupMembers) {
			// Determine if this is the last set of results to retrieve
			if ((countDomainGroupMembers - (startOffsetPosition + afterCountPosition)) <= 0) { resultsLastPageReached = 1; }
			// Get results for this iteration
			var tmpFoundData = 0;
			DirectoryEntry rootEntryMembers = new DirectoryEntry(gDomain);
			DirectorySearcher memberRetrieve = new DirectorySearcher(rootEntryMembers);
			memberRetrieve.Filter = "(&(ObjectClass=person)(memberOf=" + fullADGroupPath + "))";
			memberRetrieve.PropertiesToLoad.Add("cn");
			memberRetrieve.Sort = new SortOption("cn", SortDirection.Ascending);
			memberRetrieve.VirtualListView = new DirectoryVirtualListView(beforeCountPosition, afterCountPosition, startOffsetPosition);
			SearchResultCollection memberData = memberRetrieve.FindAll();
			foreach (SearchResult entry in memberData)
			{
				var e_cn = "";
				try { e_cn = entry.Properties["cn"][0].ToString(); }
				catch (Exception e) { /* No data present */ }
				// Save result to DataRow object
				row = table.NewRow();
				row["CN"] = e_cn;
				table.Rows.Add(row);
				tmpFoundData = 1;
			}
			if (tmpFoundData == 1) {
				if (resultsLastPageReached == 1) { results = "lastpage"; }
				else { results = "completed"; }
			}
			else {
				if (resultsLastPageReached == 1) { results = "lastpage"; }
				else { results = "nodata"; }
			}
		}
		else {
			// Limit reached, no results left to retrieve
			results = "nodata";
		}
		return results;
	}
}


About Joe