1

I have a group, lets call it GotRocks. I am attempting to get all of its members, but I am getting wildly different results count-wise between DirectoryEntry and AccountManagement. Below are the counts by member retrieval method:

Method 1: DirectoryEntry.PropertyName.member = 350
Method 2: AccountManagement.GroupPrincipal.GetMembers(false) = 6500
Method 2: AccountManagement.GroupPrincipal.GetMembers(true) = 6500

As a sanity check, I went into ADUC and pulled the list of members from the group, which is limited to 2,000 by default. The important thing here is that ADUC seems to validate the AccountManagement result. I have checked the Children property as well, but it is blank. Also, none of the members listed in DirectoryEntry are of the SchemaName group - they are all users.

I do not think this is a code problem, but perhaps a lack of understanding of how DirectoryEntry and the GetMembers methods retrieve group members. Can anyone explain why the DirectoryEntry member list would yield a different result from the GetMembers recursive function? Is there a certain method or property I need to be aware of? Note: I have built a function that will query DirectoryEntry by "member;range={0}-{1}" where the loop gets members in chunks of 1,500. I am at a complete and utter loss here.

The fact that DirectoryEntry is returning so few results is problematic because I want to use DirectoryEntry for the simple fact that going this route is, at a minimum, two orders of magnitude faster than AccountManagement (i.e., stopwatch times of 1,100 milliseconds versus 250,000 milliseconds).

UPDATE 1: Methods:

Method 1: DirectoryEntry

private 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
    {
        directoryEntryGroup = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);

        directoryEntryGroup.RefreshCache();
    }
    catch (Exception)
    { }

    try
    {
        if (directoryEntryGroup.Properties["member"].Count > 0)
        {
            int intStart = 0;

            while (true)
            {
                int intEnd = intStart + intIncrement - 1;

                // Define the PropertiesToLoad attribute, which contains a range flag that LDAP uses to get a list of members in a pre-specified chunk/block of members that is defined by each loop iteration.
                strMemberPropertyRange = string.Format("member;range={0}-{1}", intStart, intEnd);

                directorySearcher = new DirectorySearcher(directoryEntryGroup)
                {
                    Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))", // User, Contact, Group, Computer objects

                    SearchScope = SearchScope.Base,

                    PageSize = intActiveDirectoryPageSize,

                    PropertiesToLoad = { strMemberPropertyRange }
                };

                try
                {
                    searchResultCollection = directorySearcher.FindAll();

                    foreach (SearchResult searchResult in searchResultCollection)
                    {
                        var membersProperties = searchResult.Properties;

                        // Find the property that starts with the PropertyName of "member;" and get all of its member values.
                        var membersPropertyNames = membersProperties.PropertyNames.OfType<string>().Where(n => n.StartsWith("member;"));

                        // For each record in the memberPropertyNames, get the PropertyName and add to the lest.
                        foreach (var propertyName in membersPropertyNames)
                        {
                            var members = membersProperties[propertyName];

                            foreach (string memberDn in members)
                            {                                   
                                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;
                }

                intStart += intIncrement;
            }
        }

        return listGroupMemberDn;
    }
    finally
    {
        listGroupMemberDn = null;
        strPath = null;
        strMemberPropertyRange = null;
        directoryEntryGroup.Close();
        if(directoryEntryGroup != null) directoryEntryGroup.Dispose();                
        if (directorySearcher != null) directorySearcher.Dispose();
        if(searchResultCollection != null) searchResultCollection.Dispose();
    }
}

Method 2: AccountManagement (toggle bolRecursive as either true or false).

private List<Guid> GetGroupMemberList(string strPropertyValue, string strDomainController, bool bolRecursive)
{
    // Variable declaration(s).
    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);

        // Setup the IdentityType. Use IdentityType.Guid because GUID is unique and never changes for a given object. Make sure that is what strPropertyValue is receiving.
        // This is required, otherwise you will get a MultipleMatchesException error that says "Multiple principals contain a matching Identity."
        // This happens when you have two objects that AD thinks match whatever you're passing to UserPrincipal.FindByIdentity(principalContextDomain, strPropertyValue)
        identityType = IdentityType.Guid;

        groupPrincipal = GroupPrincipal.FindByIdentity(principalContext, identityType, strPropertyValue);

        if (groupPrincipal != null)
        {
            // Get all members that the group contains and add it to the list.
            // Note: The true flag in GetMembers() specifies a recursive search, which enables the application to search a group recursively and return only principal objects that are leaf nodes.
            listPrincipalSearchResult = groupPrincipal.GetMembers(bolRecursive);

            // Remove the nulls from the list, otherwise the foreach loop breaks prematurly on the first null found and misses all other object members.
            listPrincipalNoNull = listPrincipalSearchResult.Where(item => item.Name != null).ToList();

            foreach (Principal principal in listPrincipalNoNull)
            {
                listGroupMemberGuid.Add((Guid)principal.Guid);
            }
        }

        return listGroupMemberGuid;
    }
    catch (MultipleMatchesException)
    {
        // Multiple principals contain a matching identity. 
        // In other words, the same property value was found on more than one record in either of the six attributes that are listed within the IdentityType enum.
        throw new MultipleMatchesException(strPropertyValue);
    }
    finally
    {
        // Cleanup objects.
        listGroupMemberGuid = null;
        if(listPrincipalSearchResult != null) listPrincipalSearchResult.Dispose();
        if(principalContext != null) principalContext.Dispose();
        if(groupPrincipal != null) groupPrincipal.Dispose();
    }
}

UPDATE 2:

public static void Main()
{
    Program objProgram = new Program();

    // Other stuff here.

    objProgram.GetAllUserSingleDc();

    // Other stuff here.
}

private void GetAllUserSingleDc()
{
    string strDomainController = "domain.com"; 
    string strActiveDirectoryHost = "LDAP://" + strDomainController;
    int intActiveDirectoryPageSize = 1000;
    string[] strAryRequiredProperties = null;
    DirectoryEntry directoryEntry = null;
    DirectorySearcher directorySearcher = null;
    SearchResultCollection searchResultCollection = null;
    DataTypeConverter objConverter = null;
    Type fieldsType = null;

    fieldsType = typeof(AdUserInfoClass);

    objConverter = new DataTypeConverter();

    directoryEntry = new DirectoryEntry(strActiveDirectoryHost, null, null, AuthenticationTypes.Secure);

    directorySearcher = new DirectorySearcher(directoryEntry)
    {
        //Filter = "(|(objectCategory=person)(objectCategory=computer)(objectCategory=group))", // User, Contact, Group, Computer objects
        Filter = "(sAMAccountName=GotRocks)", // Group

        SearchScope = SearchScope.Subtree,

        PageSize = intActiveDirectoryPageSize

        PropertiesToLoad = { "isDeleted","isCriticalSystemObject","objectGUID","objectSid","objectCategory","sAMAccountName","sAMAccountType","cn","employeeId",
                            "canonicalName","distinguishedName","userPrincipalName","displayName","givenName","sn","mail","telephoneNumber","title","department",
                            "description","physicalDeliveryOfficeName","manager","userAccountControl","accountExpires","lastLogon","logonCount","lockoutTime",
                            "primaryGroupID","pwdLastSet","uSNCreated","uSNChanged","whenCreated","whenChanged","badPasswordTime","badPwdCount","homeDirectory",
                            "dNSHostName" }
    };         

    searchResultCollection = directorySearcher.FindAll();            

    try
    {
        foreach (SearchResult searchResult in searchResultCollection)
        {
            clsAdUserInfo.GidObjectGuid = objConverter.ConvertByteAryToGuid(searchResult, "objectGUID");            
            clsAdUserInfo.StrDirectoryEntryPath = strActiveDirectoryHost + "/<GUID=" + clsAdUserInfo.GidObjectGuid + ">";
            clsAdUserInfo.StrSchemaClassName = new DirectoryEntry(clsAdUserInfo.StrDirectoryEntryPath, null, null, AuthenticationTypes.Secure).SchemaClassName;

            if (clsAdUserInfo.StrSchemaClassName == "group")
            {
                // Calling the functions here.
                List<string> listGroupMemberDnMethod1 = GetGroupMemberListStackOverflow(clsAdUserInfo.GidObjectGuid.ToString(), strActiveDirectoryHost, intActiveDirectoryPageSize);

                List<Guid> listGroupMemberGuidMethod2 = GetGroupMemberList(clsAdUserInfo.GidObjectGuid.ToString(), strDomainController, false)
            }   
            // More stuff here.
        }
    }
    finally
    {
        // Cleanup objects.
        // Class constructors.
        objProgram = null;
        clsAdUserInfo = null;
        // Variables.
        intActiveDirectoryPageSize = -1;
        strActiveDirectoryHost = null;
        strDomainController = null;
        strAryRequiredProperties = null;
        directoryEntry.Close();
        if(directoryEntry !=null) directoryEntry.Dispose();
        if(directorySearcher != null) directorySearcher.Dispose();
        if(searchResultCollection != null) searchResultCollection.Dispose();
        objConverter = null;
        fieldsType = null;
    }   
}

UPDATE 3:

Below is the list of namespaces that I am using.

using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Security.Principal;
using System.Text;
using System.Linq;
using System.Collections;

UPDATE 4: Program.cs

using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Security.Principal;
using System.Text;
using System.Linq;

namespace activeDirectoryLdapExamples
{
    public class Program
    {
         public static void Main()
        {
            Program objProgram = new Program();
            objProgram.GetAllUserSingleDc();
        }

        #region GetAllUserSingleDc
        private void GetAllUserSingleDc()
        {
            Program objProgram = new Program();
            string strDomainController = "EnterYourDomainhere";
            string strActiveDirectoryHost = "LDAP://" + strDomainController;
            int intActiveDirectoryPageSize = 1000;
            DirectoryEntry directoryEntry = null;
            DirectorySearcher directorySearcher = null;
            SearchResultCollection searchResultCollection = null;
            DataTypeConverter objConverter = null;

            objConverter = new DataTypeConverter();

            directoryEntry = new DirectoryEntry(strActiveDirectoryHost, null, null, AuthenticationTypes.Secure);

            directorySearcher = new DirectorySearcher(directoryEntry)
            {
                Filter = "(sAMAccountName=GotRocks)", // Group

                SearchScope = SearchScope.Subtree,

                PageSize = intActiveDirectoryPageSize,

                PropertiesToLoad = { "isDeleted","isCriticalSystemObject","objectGUID","objectSid","objectCategory","sAMAccountName","sAMAccountType","cn","employeeId",
                                        "canonicalName","distinguishedName","userPrincipalName","displayName","givenName","sn","mail","telephoneNumber","title","department",
                                        "description","physicalDeliveryOfficeName","manager","userAccountControl","accountExpires","lastLogon","logonCount","lockoutTime",
                                        "primaryGroupID","pwdLastSet","uSNCreated","uSNChanged","whenCreated","whenChanged","badPasswordTime","badPwdCount","homeDirectory",
                                        "dNSHostName" }
            };

            searchResultCollection = directorySearcher.FindAll();

            try
            {
                foreach (SearchResult searchResult in searchResultCollection)
                {                    
                    Guid? gidObjectGuid = objConverter.ConvertByteAryToGuid(searchResult, "objectGUID");
                    string StrSamAccountName = objConverter.ConvertToString(searchResult, "sAMAccountName");
                    // Get new DirectoryEntry and retrieve the SchemaClassName from it by binding the current objectGUID to it.
                    string StrDirectoryEntryPath = strActiveDirectoryHost + "/<GUID=" + gidObjectGuid + ">";
                    string StrSchemaClassName = new DirectoryEntry(StrDirectoryEntryPath, null, null, AuthenticationTypes.Secure).SchemaClassName;

                    #region GetGroupMembers
                    if (StrSchemaClassName == "group")
                    {
                        // FAST!
                        var watch = System.Diagnostics.Stopwatch.StartNew();
                        List<string> listGroupMemberDn = GetGroupMemberList(gidObjectGuid.ToString(), strActiveDirectoryHost, intActiveDirectoryPageSize);
                        watch.Stop();
                        var listGroupMemberDnElapsedMs = watch.ElapsedMilliseconds;

                        // SLOW!
                        watch = System.Diagnostics.Stopwatch.StartNew();
                        List<Guid> listGroupMemberGuidRecursiveTrue = GetGroupMemberList(gidObjectGuid.ToString(), strDomainController, true);
                        watch.Stop();
                        var listGroupMemberGuidRecursiveTrueElapsedMs = watch.ElapsedMilliseconds;

                        watch = System.Diagnostics.Stopwatch.StartNew();
                        List<Guid> listGroupMemberGuidRecursiveFalse = GetGroupMemberList(gidObjectGuid.ToString(), strDomainController, false);
                        watch.Stop();
                        var listGroupMemberGuidRecursiveFalseElapsedMs = watch.ElapsedMilliseconds;

                        ////// Display all members of the list.
                        //listGroupMemberDn.ForEach(item => Console.WriteLine("Member GUID: {0}", item));
                        //listGroupMemberGuidRecursiveTrue.ForEach(item => Console.WriteLine("Member GUID: {0}", item));
                        //listGroupMemberGuidRecursiveFalse.ForEach(item => Console.WriteLine("Member GUID: {0}", item));

                        Console.WriteLine("objectGUID: {0}", gidObjectGuid);
                        Console.WriteLine("sAMAccountName: {0}", strSamAccountName);

                        // Result: 350
                        Console.WriteLine("\nlistGroupMemberDn Count Members: {0}", listGroupMemberDn.Count);
                        Console.WriteLine("Total RunTime listGroupMemberDnElapsedMs (in milliseconds): {0}", listGroupMemberDnElapsedMs);

                        // Result: 6500
                        Console.WriteLine("\nlistGroupMemberGuidRecursiveTrue Count Members: {0}", listGroupMemberGuidRecursiveTrue.Count);
                        Console.WriteLine("Total RunTime listGroupMemberGuidRecursiveTrueElapsedMs (in milliseconds): {0}", listGroupMemberGuidRecursiveTrueElapsedMs);

                        // Result: 6500
                        Console.WriteLine("\nlistGroupMemberGuidRecursiveFalse Count Members: {0}", listGroupMemberGuidRecursiveFalse.Count);
                        Console.WriteLine("Total RunTime listGroupMemberGuidRecursiveFalseElapsedMs (in milliseconds): {0}", listGroupMemberGuidRecursiveFalseElapsedMs);
                        Console.WriteLine("\n");
                    }
                    #endregion

                    #region CurrentSearchResult
                    else
                    {
                        Console.WriteLine("ObjectGuid = {0}", gidObjectGuid);
                        Console.WriteLine("SamAccountName = {0}", strSamAccountName);

                    }
                    #endregion
                }

                Console.WriteLine("\nPress any key to continue.");
                Console.ReadKey();
            }
            finally
            {
                objProgram = null;
                intActiveDirectoryPageSize = -1;
                strActiveDirectoryHost = null;
                strDomainController = null;
                directoryEntry.Close();
                if (directoryEntry != null) directoryEntry.Dispose();
                if (directorySearcher != null) directorySearcher.Dispose();
                if (searchResultCollection != null) searchResultCollection.Dispose();
                objConverter = null;
            }
        }
        #endregion

        #region GetGroupMemberListGuid
        private List<Guid> GetGroupMemberList(string strPropertyValue, string strDomainController, bool bolRecursive)
        {
            List<Guid> listGroupMemberGuid = null;
            List<Principal> listPrincipalNoNull = null;
            GroupPrincipal groupPrincipal = null;
            PrincipalSearchResult<Principal> listPrincipalSearchResult = 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);
            }
            finally
            {
                // Cleanup objects.
                listGroupMemberGuid = null;
                listPrincipalNoNull = null;
                principalContext = null;
                if (groupPrincipal != null) groupPrincipal.Dispose();
                if (listPrincipalSearchResult != null) listPrincipalSearchResult.Dispose();
                if (principalContext != null) principalContext.Dispose();
            }
        }
        #endregion

        #region GetGroupMemberListDn
        private List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
        {
            List<string> listGroupMemberDn = new List<string>();
            string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
            const int intIncrement = 1500; // https://msdn.microsoft.com/en-us/library/windows/desktop/ms676302(v=vs.85).aspx

            var members = new List<string>();

            // The count result returns 350.
            var group = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);
            //var group = new DirectoryEntry($"LDAP://{"EnterYourDomainHere"}/<GUID={strPropertyValue}>", null, null, AuthenticationTypes.Secure);

            while (true)
            {
                var memberDns = group.Properties["member"];
                foreach (var member in memberDns)
                {
                    members.Add(member.ToString());
                }

                if (memberDns.Count < intIncrement) break;

                group.RefreshCache(new[] { $"member;range={members.Count}-*" });
            }
            return members;
        }
        #endregion

        #region DataTypeConvert
        private class DataTypeConverter
        {
            public DataTypeConverter() { }

            public String ConvertToString(SearchResult searchResult, string strPropertyName)
            {
                String bufferObjectString = null;

                try
                {
                    bufferObjectString = (String)this.GetPropertyValue(searchResult, strPropertyName);

                    if (string.IsNullOrEmpty(bufferObjectString))
                    {
                        return null;
                    }
                    else
                    {
                        return bufferObjectString;
                    }
                }
                finally
                {
                    bufferObjectString = null;
                }
            }

            public Guid? ConvertByteAryToGuid(SearchResult searchResult, string strPropertyName)
            {
                Guid? bufferObjectGuid = null;

                try
                {
                    bufferObjectGuid = new Guid((Byte[])(Array)this.GetPropertyValue(searchResult, strPropertyName));

                    if (bufferObjectGuid == null || bufferObjectGuid == Guid.Empty)
                    {
                        throw new NullReferenceException("The field " + strPropertyName + ", of type GUID, can neither be NULL nor empty.");
                    }
                    else
                    {
                        return bufferObjectGuid;
                    }
                }
                finally
                {
                    bufferObjectGuid = null;
                }
            }
        }
        #endregion
    }
}
J Weezy
  • 3,507
  • 3
  • 32
  • 88
  • The `GetMembers` method just reads from the `member` attribute of the AD object, so it doesn't make sense that it's getting different values. Can you show all of your code of how you are using both methods? – Gabriel Luci Mar 12 '18 at 15:34
  • @Gabriel. I have updated the question to include the code for the two methods. Thank you for taking a look at this. – J Weezy Mar 12 '18 at 15:50
  • I tested your code. I tried it with a group in my AD environment that has several nested groups inside. I got 7 members for both the DirectoryEntry method and GetMembers(false), as expected, and 304 with GetMembers(true) – Gabriel Luci Mar 12 '18 at 16:59
  • Try making Visual Studio break on all exceptions. You have a lot of try/catch blocks in there, so there could be some exceptions happening but it's just continuing silently. https://stackoverflow.com/questions/116896/visual-studio-how-to-break-on-handled-exceptions – Gabriel Luci Mar 12 '18 at 17:07
  • I followed the link you provided and enabled a break on all exceptions. I also deleted all hidden files; cleaned the solution; and rebuilt but without success. – J Weezy Mar 12 '18 at 17:44
  • Set a breakpoint in your code at `groupPrincipal.GetMembers(bolRecursive)` and make sure that `bolRecursive` is actually `false` when you want it to be. – Gabriel Luci Mar 12 '18 at 18:37
  • Confirmed, the recursive flag is false when I want it to. It still produces the ~6,500 results. – J Weezy Mar 12 '18 at 19:05
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/166716/discussion-between-gabriel-luci-and-j-weezy). – Gabriel Luci Mar 12 '18 at 23:23
  • @Gabriel, I responded to your chat but have not yet received a response. Did you receive my response? – J Weezy Mar 14 '18 at 14:31

1 Answers1

5

The last code block (Update 2) is the answer!

The code you have for reading the member attribute is more complicated than it needs to be. There may be a reason in there why it's returning skewed results, but I didn't look too hard because you don't need to be using DirectorySearcher at all. I just rewrote it.

Here is what it should look like, in it's simplest form:

private static List<string> GetGroupMemberList(string groupGuid, string domainDns) {
    var members = new List<string>();

    var group = new DirectoryEntry($"LDAP://{domainDns}/<GUID={groupGuid}>", null, null, AuthenticationTypes.Secure);

    while (true) {
        var memberDns = group.Properties["member"];
        foreach (var member in memberDns) {
            members.Add(member.ToString());
        }

        if (memberDns.Count == 0) break;

        try {
            group.RefreshCache(new[] {$"member;range={members.Count}-*", "member"});
        } catch (System.Runtime.InteropServices.COMException e) {
            if (e.ErrorCode == unchecked((int) 0x80072020)) { //no more results
                break;
            }
            throw;
        }
    }
    return members;
}

Call it like this:

var members = GetGroupMemberList("00000000-0000-0000-0000-000000000000", "domain.com");

This is not recursive. To make it recursive, you will have to create a new DirectoryEntry from each member and test if it is a group, then get the members of that group.

I have the code open, so here's the recursive version. It is slow because it has to bind to each member to see if it's a group.

This is not bullet-proof. There are still cases where you might get weird results (like if you have users on trusted external domains in a group).

private static List<string> GetGroupMemberList(string groupGuid, string domainDns, bool recurse = false) {
    var members = new List<string>();

    var group = new DirectoryEntry($"LDAP://{domainDns}/<GUID={groupGuid}>", null, null, AuthenticationTypes.Secure);

    while (true) {
        var memberDns = group.Properties["member"];
        foreach (var member in memberDns) {
            if (recurse) {
                var memberDe = new DirectoryEntry($"LDAP://{member}");
                if (memberDe.Properties["objectClass"].Contains("group")) {
                    members.AddRange(GetGroupMemberList(
                        new Guid((byte[]) memberDe.Properties["objectGuid"].Value).ToString(), domainDns,
                        true));
                } else {
                    members.Add(member.ToString());
                }
            } else {
                members.Add(member.ToString());
            }
        }

        if (memberDns.Count == 0) break;

        try {
            group.RefreshCache(new[] {$"member;range={members.Count}-*", "member"});
        } catch (System.Runtime.InteropServices.COMException e) {
            if (e.ErrorCode == unchecked((int) 0x80072020)) { //no more results
                break;
            }
            throw;
        }
    }
    return members;
}

Update: I did have to edit your GetMembers example, since it kept throwing exceptions on me. I commented out the .Where line and changed the foreach loop that adds the members to the list:

        //listPrincipalNoNull = listPrincipalSearchResult.Where(item => item.Name != null).ToList();
        if (groupPrincipal != null) {
            foreach (Principal principal in listPrincipalSearchResult) {
                listGroupMemberGuid.Add(((DirectoryEntry)principal.GetUnderlyingObject()).Guid);
            }
        }

This, of course, is compiling a list of Guids rather than DNs.

Update 2: Here is a version that also pulls users who have the group as the primary group (but not listed in the member attribute of the group). GetMembers seems to do this. It would be odd for a user-created group to be the primary group, but it is technically possible. Parts of this are copied from the answer here: How to retrieve Users in a Group, including primary group users

private List<string> GetGroupMemberList(string strPropertyValue, string strActiveDirectoryHost, int intActiveDirectoryPageSize)
{
    // Variable declaration(s).
    List<string> listGroupMemberDn = new List<string>();
    string strPath = strActiveDirectoryHost + "/<GUID=" + strPropertyValue + ">";
    const int intIncrement = 1500; // https://msdn.microsoft.com/en-us/library/windows/desktop/ms676302(v=vs.85).aspx

    var members = new List<string>();

    // The count result returns 350.
    var group = new DirectoryEntry(strPath, null, null, AuthenticationTypes.Secure);
    //var group = new DirectoryEntry($"LDAP://{"EnterYourDomainHere"}/<GUID={strPropertyValue}>", null, null, AuthenticationTypes.Secure);

    while (true)
    {
        var memberDns = group.Properties["member"];
        foreach (var member in memberDns)
        {
            members.Add(member.ToString());
        }

        if (memberDns.Count < intIncrement) break;

        group.RefreshCache(new[] { $"member;range={members.Count}-*" });
    }

    //Find users that have this group as a primary group
    var secId = new SecurityIdentifier(group.Properties["objectSid"][0] as byte[], 0);

    /* Find The RID (sure exists a best method)
     */
    var reg = new Regex(@"^S.*-(\d+)$");
    var match = reg.Match(secId.Value);
    var rid = match.Groups[1].Value;

    /* Directory Search for users that has a particular primary group
     */
    var dsLookForUsers =
        new DirectorySearcher {
            Filter = string.Format("(primaryGroupID={0})", rid),
            SearchScope = SearchScope.Subtree,
            PageSize = 1000,
            SearchRoot = new DirectoryEntry(strActiveDirectoryHost)
    };
    dsLookForUsers.PropertiesToLoad.Add("distinguishedName");

    var srcUsers = dsLookForUsers.FindAll();

    foreach (SearchResult user in srcUsers)
    {
        members.Add(user.Properties["distinguishedName"][0].ToString());
    }
    return members;
}
Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • Plus 1 as I can confirm that your simplified solution works, which is helpful - so thank you. But it still yields the same result of 350. – J Weezy Mar 12 '18 at 18:06
  • Does the recursive version give you 6500? – Gabriel Luci Mar 12 '18 at 18:35
  • I have tried your recursive function with both true and false passed to recurse and I received the same result: 350. When I click on group.Properties.ResultsView.[3] (note: the member attribute is item 3 in the list), there are only 350 items in the list. I think the problem is upstream from the code because of this. – J Weezy Mar 12 '18 at 19:18
  • That sounds like the group does indeed have 350 members. Are you sure you weren't running your tests against two different groups? – Gabriel Luci Mar 12 '18 at 19:20
  • I am passing the same GUID to either function. I will note that the DirectoryEntry member variables are somewhat different from the GroupPrincipal's member variables. For example, DE.Name = "CN=GotRocks", whereas GP.DistinguishedName = "CN=GotRocks,CN=Users,DC=,DC=com". I am unable to expand the members list of the GroupPrincipal during debug because it times out. – J Weezy Mar 12 '18 at 19:36
  • 1
    I updated my answer with a modification I had to make to get your example to run without exceptions (at least in my environment). See if that makes any difference. I don't see how it would, but you never know. – Gabriel Luci Mar 12 '18 at 19:48
  • The result for the update you provided is ~6,500. I am not surprised the result did not change because I removed properties with a NULL name. By definition, ObjectGuid is NOT NULL, so the results should have been the same - and they were. Does it matter whether or not the group is a securitygroup? Note: there are other securitygroup type groups in my list where the counts between DirectoryEntry and AccountManagement match. – J Weezy Mar 12 '18 at 20:10
  • 1
    No, it doesn't matter what type of group it is. Can you maybe show the code where you call your methods? – Gabriel Luci Mar 12 '18 at 21:21
  • I have posted the code. Note: I am getting all information from AD and then looping through each member provided. In the event that the current object is a group, I call the GetGroupMember function. Also, AdUserInfoClass is a simple class that stores all of the PropertiesToLoad variables. – J Weezy Mar 12 '18 at 22:18
  • I looked at Microsoft's implementation again. They aren't hard-coding 1500 in their code. It just keeps asking for more until it throws an exception. So I updated my code to reflect that. I couldn't find a way to change that 1500 limit, but maybe it can be changed and it got changed in your domain? – Gabriel Luci Mar 14 '18 at 15:29
  • I tried with your updated code and that still did not work. If the range is indeed getting in the way then I would expect to receive 1,500 members. In this case, I am only receiving 350 from the domain controller. I have also tried using different DCs. For more information on the range limit for members, see: https://msdn.microsoft.com/en-us/library/windows/desktop/ms676302(v=vs.85).aspx – J Weezy Mar 14 '18 at 16:04
  • 1
    I added a new version of the method to the end of my answer. Try that and see if it gives you the same results. – Gabriel Luci Mar 15 '18 at 20:43
  • I am reviewing the results and I have found that the code is not getting members of security groups. Any thoughts on how I can do this? Note: The GetMembers(false) does not get security group members but GetMembers(true) does. – J Weezy Mar 16 '18 at 15:18
  • I can't say, I didn't see that happening. There is no difference between how members are stored between security groups and distribution lists (the only difference between the two is that security groups can be used in permissions). Do those security groups only have nested groups inside? – Gabriel Luci Mar 16 '18 at 16:52
  • All security groups? Or only certain ones? – Gabriel Luci Mar 16 '18 at 16:54
  • All security and distribution group types for any combination of group scope of universal, global, and local domain. – J Weezy Mar 16 '18 at 17:37
  • What I am finding is that groups with nested groups are show variances. The GotRocks group went away. Though, I will note that GotRocks does not contain any nested groups. It appears that GotRocks was an odd scenario because of the PrimaryGroupId. – J Weezy Mar 16 '18 at 20:14
  • After working with this new solution I have realized that it works in a valuable way - I get all users and nested groups only, which is what I find valuable. However, I do not get an output of nested group members. But, after working with it in SQL, I have created a recursive CTE to get the nested group members. I am able to cross-reference the output with what is in ADUC. Now I can put this all together in SQL and build an SSRS report with parameters to control the recursive method. So, this works now. Thanks! – J Weezy Mar 20 '18 at 15:37
  • I am looking at modifying the GetMembers function to retrieve the objectGUID by making a call to DirectoryEntry for each distinguishedName. I'm noticing some slowness overall. Do you think the calls to DE would materially impact performance? – J Weezy May 03 '18 at 00:57
  • Yes, it would impact performance, since it needs to talk to AD again for every member. But to minimize that, call `.RefreshCache(new[] { "objectGUID"})` on the `DirectoryEntry` *before* accessing `.Properties["objectGUID"]` so that it retrieves *only* that one attribute. Otherwise, as soon as you access `.Properties`, it will retrieve *every* attribute, which is useless network traffic if you only need the one attribute. – Gabriel Luci May 03 '18 at 12:07
  • So, doing something like this won't work? listGroupMemberGuid.Add(new DirectoryEntry("LDAP://" + strMemberDn, null, null, AuthenticationTypes.Secure).Guid); I can post a new question if needed. – J Weezy May 03 '18 at 14:44
  • Sure, that might work too. It might still be getting all of the properties, but I'm not sure. – Gabriel Luci May 03 '18 at 14:55
  • FYI, I posted a question for performance tuning the function here: https://stackoverflow.com/questions/50158375/active-directory-tune-performance-of-function-to-retrieve-group-members II am testing the solution in your comment. If you can provide your comment as the answer then I can mark it answered if it works. :) – J Weezy May 03 '18 at 15:20
  • 1
    This is like when the really smart dev writes the code for you. Then, you go back to the code every so often to inspire better ways of writing your own code. +++++++++11111111 – snowYetis Aug 29 '18 at 20:38
  • 1
    @snowYetis Since this answer, I've reused this code in other answers. I ended up finally making a site where I can put some of this stuff I've learned so I don't have to retype it :) http://www.gabescode.com/active-directory/2018/06/08/finding-all-of-a-users-groups.html I have ideas for a couple more articles. I just have to get around to it. – Gabriel Luci Aug 30 '18 at 01:31