Reputation: 3935
Question: How do I retrieve all group members in a consistent manner?
Context: I am retrieving all objects that are either person, group, contact, or computer:
Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))"
I now need to retrieve all members of groups. I have developed three methods to do this; however, they are returning different results for the same group and I am not sure why. I suspect that it may be caused by nested groups (i.e., groups within groups). Debugging it is a challenge because some groups contain so many members that the debugger times out and shows no results.
Method 1 and 2 is slow. Method 3 is fast. So, I prefer to use method 3.
Group Name Retrieval Method Recursive Search Count Members Comment
Group1 AccountManagement TRUE 505
Group1 AccountManagement FALSE 505
Group1 DirectoryServices N/A 101
Group2 AccountManagement TRUE 440 Contains group name 'Group3'
Group2 AccountManagement FALSE 440 Contains group name 'Group3'
Group2 DirectoryServices N/A 100 Contains group name 'Group3'
Group3 AccountManagement TRUE 101 All Group3
Group3 AccountManagement FALSE 2 All Group3
Group3 DirectoryServices N/A 2 1 user 1 group (Group2)
Method 1 and 2: Get group members using the S.DS.AM
, where the GetMembers() is set to either true or false, respectively: https://msdn.microsoft.com/en-us/library/system.directoryservices.accountmanagement(v=vs.110).aspx
private static List<Guid> GetGroupMemberList(string strPropertyValue, string strDomainController, bool bolRecursive)
{
List<Guid> listGroupMemberGuid = null;
GroupPrincipal groupPrincipal = null;
PrincipalSearchResult<Principal> listPrincipalSearchResult = null;
List<Principal> listPrincipalNoNull = null;
PrincipalContext principalContext = null;
ContextType contextType;
IdentityType identityType;
try
{
listGroupMemberGuid = new List<Guid>();
contextType = ContextType.Domain;
principalContext = new PrincipalContext(contextType, strDomainController);
identityType = IdentityType.Guid;
groupPrincipal = GroupPrincipal.FindByIdentity(principalContext, identityType, strPropertyValue);
if (groupPrincipal != null)
{
listPrincipalSearchResult = groupPrincipal.GetMembers(bolRecursive);
listPrincipalNoNull = listPrincipalSearchResult.Where(item => item.Name != null).ToList();
foreach (Principal principal in listPrincipalNoNull)
{
listGroupMemberGuid.Add((Guid)principal.Guid);
}
}
return listGroupMemberGuid;
}
catch (MultipleMatchesException)
{
throw new MultipleMatchesException(strPropertyValue);
}
catch (Exception ex)
{
throw ex;
}
finally
{
listGroupMemberGuid = null;
listPrincipalSearchResult.Dispose();
principalContext.Dispose();
groupPrincipal.Dispose();
}
}
Method 3: Get group members using the S.DS.AD
: https://msdn.microsoft.com/en-us/library/system.directoryservices.activedirectory(v=vs.110).aspx
private static List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
{
List<string> listGroupMemberDn = new List<string>();
string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
DirectoryEntry directoryEntryGroup;
DirectoryEntry directoryEntryGroupMembers;
DirectorySearcher directorySearcher;
SearchResultCollection searchResultCollection;
DataTypeConverter objConverter = null;
objConverter = new DataTypeConverter();
try
{
directoryEntryGroup = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);
directoryEntryGroup.RefreshCache();
}
catch (Exception ex)
{
throw ex;
}
try
{
directorySearcher = new DirectorySearcher(directoryEntryGroup)
{
//Filter = "(objectCategory=group)", // Group
SearchScope = SearchScope.Subtree,
PageSize = intActiveDirectoryPageSize,
};
directorySearcher.PropertiesToLoad.Add("objectGUID");
searchResultCollection = directorySearcher.FindAll();
}
catch (Exception ex)
{
throw ex;
}
try
{
foreach (SearchResult searchResult in searchResultCollection)
{
directoryEntryGroupMembers = searchResult.GetDirectoryEntry();
foreach (object objGroupMember in directoryEntryGroupMembers.Properties["member"])
{
listGroupMemberDn.Add((string)objGroupMember);
}
}
return listGroupMemberDn;
}
catch (Exception ex)
{
throw ex;
}
finally
{
listGroupMemberDn = null;
strPath = null;
directoryEntryGroup.Dispose();
directoryEntryGroupMembers = null;
directorySearcher.Dispose();
searchResultCollection.Dispose();
objConverter = null;
}
}
Method 4: (Loop with GetNextChunk() method implementation)
private static List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
{
// Variable declaration(s).
List<string> listGroupMemberDn = new List<string>();
string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
string strMemberPropertyRange = null;
DirectoryEntry directoryEntryGroup = null;
DirectorySearcher directorySearcher = null;
SearchResultCollection searchResultCollection = null;
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms676302(v=vs.85).aspx
const int intIncrement = 1500;
// Load the DirectoryEntry.
try
{
// Setup a secure connection with Active Directory (AD) using Kerberos by setting the directoryEntry with AuthenticationTypes.Secure.
directoryEntryGroup = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);
// Load the property values for this DirectoryEntry object into the property cache.
directoryEntryGroup.RefreshCache();
}
catch (Exception)
{ }
#region Method1
// Enumerate group members.
try
{
// Check to see if the group has any members.
if (directoryEntryGroup.Properties["member"].Count > 0)
{
int intStart = 0;
while (true)
{
// End of the range.
int intEnd = intStart + intIncrement - 1;
strMemberPropertyRange = string.Format("member;range={0}-{1}", intStart, intEnd);
directorySearcher = new DirectorySearcher(directoryEntryGroup)
{
// Set the Filter criteria that is used to constrain the search within AD.
Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))", // User, Contact, Group, Computer objects
// Set the SearchScope for how the AD tree is searched (Default = Subtree).
SearchScope = SearchScope.Base,
// The PageSize value should be set equal to the PageSize that is set by the AD administrator (Default = 0).
PageSize = intActiveDirectoryPageSize,
PropertiesToLoad = { strMemberPropertyRange }
};
try
{
// Populate the searchResultCollection with all records within AD that match the Filter criteria.
searchResultCollection = directorySearcher.FindAll();
foreach (SearchResult searchResult in searchResultCollection)
{
var membersProperties = searchResult.Properties;
var membersPropertyNames = membersProperties.PropertyNames.OfType<string>().Where(n => n.StartsWith("member;"));
foreach (var propertyName in membersPropertyNames)
{
// Get all members from the ranged result.
var members = membersProperties[propertyName];
foreach (string memberDn in members)
{
// Add the member's "distinguishedName" attribute value to the list.
listGroupMemberDn.Add(memberDn);
}
}
}
}
catch (DirectoryServicesCOMException)
{
// When the start of the range exceeds the number of available results, an exception is thrown and we exit the loop.
break;
}
// Increment for the next range.
intStart += intIncrement;
}
}
// return the listGroupMemberDn;
return listGroupMemberDn;
}
#endregion
finally
{
// Cleanup objects.
listGroupMemberDn = null;
strPath = null;
strMemberPropertyRange = null;
directoryEntryGroup.Dispose();
directorySearcher.Dispose();
searchResultCollection.Dispose();
}
}
Upvotes: 1
Views: 8091
Reputation: 40858
System.DirectoryServices.AccountManagement
can be more convenient, since it hides much of the complexity of AD, but that is also what makes it slower. You have less control over what's going on.
DirectoryEntry
gives you more control, but you have to handle some of the complexity.
So that can explain the time difference.
But your method that uses DirectoryEntry
still seems overly complicated. Why use DirectorySearcher
? It doesn't seem to add anything. You already have the group when you set directoryEntryGroup
. After that, you can access the members:
foreach (var member in directoryEntryGroup.Properties["member"]) {
//member is a string of the distinguishedName
}
For very large groups, be aware that by default AD limits the records it returns to 1500. So once you hit that number, you will have to ask for more. You do that like this:
directoryEntryGroup.RefreshCache("member;range=1500-*")
Then loop through them again the same way. If you get exactly another 1500, then ask for more (replacing the 1500 with 3000), etc. until you have them all.
This is exactly what the .NET Core implementation of System.DirectoryServices.AccountManagement
does (and I presume the .NET 4.x does too - I just can't see that code). You can see the special class .NET Core code uses to do this here (see the GetNextChunk
method): https://github.com/dotnet/corefx/blob/0eb5e7451028e9374b8bb03972aa945c128193e1/src/System.DirectoryServices.AccountManagement/src/System/DirectoryServices/AccountManagement/AD/RangeRetriever.cs
As a side note:
catch (Exception ex)
{
// Something went wrong. Throw an error.
throw ex;
}
If you are going to re-throw an exception without doing anything else, just don't catch it. Re-throwing has the effect of hiding where the exception actually happened, because your stack trace will now say that the exception happened at throw ex;
, rather than tell you the actual line where the exception happened.
Even for your last block, you can use try
/finally
without catch
.
Upvotes: 2