When retrieving the members of a group, Active Directory will never return more than 1000 (Win2000) or 1500 (Win2003) entries. This is true for all multi-valued properties. So suppose you have a group with more than 1500 members, it’s difficult to get all of them.
DirectoryEntry groupEntry = ...; var members = groupEntry.Properties["member"]; var nrMembers = members.Count;
In the example, nrMembers
will never be larger than 1500, even when the group has more than 1500 members.
To overcome this problem, there are two possible solutions. First, instead of taking the group and reading the member
property, look at all the potential members and check their memberOf
property.
// Construct a memberOf LDAP filter. var groupWithMembersDn = "CN=TestGroup,OU=Groups,DC=itq,DC=local"; var filter = String.Format("memberOf={0}", groupDn); // Load only the objectSid of every member. var properties = new[] { "objectSid" }; // Get a root entry to start the search from. DirectoryEntry rootEntry = ...; // Find sid's for all group members. var searcher = new DirectorySearcher( rootEntry, filter, properties, SearchScope.Subtree); searcher.PageSize = 100; using (searcher) { var memberResults = searcher.FindAll(); foreach (SearchResult memberResult in memberResults) { var memberSidBytes = (byte[]) memberResult.Properties["objectSid"][0]; var memberSid = new SecurityIdentifier(sidBytes, 0); } }
In the example we get the SID of every member of the group with distinguished name CN=TestGroup,OU=Groups,DC=itq,DC=local
.
This approach has two disadvantages. One issue is that you always have to scope the search from a specific root entry. If you want to make sure you get every group member, your scope should always be the root of the Active Directory domain. This probably makes your search space too wide and therefore has a negative impact on performance.
A second and larger disadvantage is that this approach fails in a multi-domain or multi-forest environment. Since you scope the search to a specific root entry, you will find nothing outside this scope. So if the group has a member outside the current domain or forest, this member will not be found.
So, it would be nice if we could look directly at the group's members without being bound by the 1500 entries limit. This can be accomplished via range retrieval of attribute values.
To use this, you first need to get the DirectoryEntry
for the group you want to find the members of. This entry will be the search root of a DirectorySearcher
. Next, you append a range option to the multi-valued property you want to load (in this case, the member
property). Finally, you loop through each range and collect the member
property values.
1: var memberDns = new List<string>(); 2: const int increment = 999; 3: var groupEntry = ...; 4: int from = 0; 5: while (true) 6: { 7: // End of the range. 8: int to = from + increment - 1; 9: 10: // Attach a range option to the properties to load, 11: // for example: range=0-999. 12: var properties = new[] 13: { string.Format("member;range={0}-{1}", @from, to) }; 14: 15: // Perform a search using the group entry as the base. 16: var filter = "(objectClass=*)"; 17: var memberSearcher = new DirectorySearcher( 18: groupEntry, filter, properties, SearchScope.Base); 19: using (memberSearcher) 20: { 21: try 22: { 23: var memberResults = memberSearcher.FindAll(); 24: foreach (SearchResult memberResult in memberResults) 25: { 26: var membersProperties = memberResult.Properties; 27: var membersPropertyNames = 28: membersProperties.PropertyNames 29: .OfType<string>() 30: .Where(n => n.StartsWith("member;")); 31: foreach (var propertyName in membersPropertyNames) 32: { 33: // Get all members from the ranged result. 34: var members = membersProperties[propertyName]; 35: foreach (string memberDn in members) 36: { 37: memberDns.Add(memberDn); 38: } 39: } 40: } 41: } 42: catch (DirectoryServicesCOMException) 43: { 44: // When the start of the range exceeds the number 45: // of available results, an exception is thrown 46: // and we exit the loop. 47: break; 48: } 49: } 50: // Increment for the next range. 51: from += increment; 52: }
On lines 12 and 13 you can see the range option being added to the member
property we wish to load. The from
and to
values of a range are inclusive so a range=0-4
contains the elements 0
, 1
, 2
, 3
, 4
.
Line 24, where we iterate the found results, throws a DirectoryServicesCOMException
when the from part of the range extends the number of elements to be found. This is one way to check whether we have found all results. Another way to check that we have arrived at the last result is described below.
The results contain member
properties that are indexed as follows: for each result that is not the last result, the key to the actual members is member;range=0-999
, member;range=1000-1999
, etc. For the last result it is member;range=2000-*
(even if we specify member;range=2000-2999
in the search). By checking that the returned property name ends with *, we can determine that it is the last result.
UPDATE!: After some testing it appeared that my implementation had a bug. I wrote a while (true) {...}
loop that was supposed to exit with an exception. Well, not always.... When the group has no members, the loop keeps running without ever exiting. So between lines 3 and 4 in the last example, you should check whether the group has any members and make sure never to enter the loop: if (groupEntry.Properties["member"].Count == 0) {...}
. Alternatively, inside the loop, you could rewrite your end condition not to depend on an exception, but on the method described in the previous paragraph.