DominikAmon
DominikAmon

Reputation: 1106

Performance Issue - Adding / Removing users from large Active Directory groups in .net

Our Active Directory groups are containing 500k users, one even more than a million users.

We are adding and removing users from groups using the System.DirectoryServices.AccountManagement namespace, as described here: https://stackoverflow.com/a/2143742/1099519

The code itself works perfectly fine, besides the fact that is super slow, adding a user takes up to a minute, sometimes even more!

I could figure out the following line of code, seems to trigger a lazy load mechanism in .net:

adGroupPrincipal.Members.Add(userPrincipal);

I used Wireshark to see what's happening, when calling GroupPrincipal.Members.Add(UserPrincipal) and I saw a lot of network traffic. My assumption: Accessing the Members property triggers a lazy load method to get all members of a group.

In the official documentation of the Members-property (https://msdn.microsoft.com/en-us/library/system.directoryservices.accountmanagement.groupprincipal.members(v=vs.110).aspx) is no information of its behavior.

Comparing adding a user the "old school" way with DirectoryEntry of the System.DirectoryServices namespace as such:

DirectoryEntry groupEntry = new DirectoryEntry("LDAP://server/CN=GROUPNAME,OU=Groups,OU=_CUSTOMERS,DC=srv,DC=tld", "USERNAME", "PASSWORD");
string userDn = String.Concat("LDAP://server/CN=", samAccountName, ",OU=Groups,OU=_CUSTOMERS,DC=srv,DC=tld"));    
groupEntry.Invoke("Add", new object[] { userDn });
groupEntry.CommitChanges();

That takes roughly 50ms.

Note that the Invoke("Add", new object[] { userDn }) method I used, was recommend in this Stackoverflow article Server is unwilling to process the request - Active Directory - Add User via C# in order to avoid the "Server is unwilling to process the request" exception

So basically my workaround does the job, but somehow I am not 100% happy, as I actually prefer to use the System.DirectoryServices.AccountManagement namespace, any ideas how to avoid the performance issue using that namespace?

Upvotes: 4

Views: 1758

Answers (1)

DominikAmon
DominikAmon

Reputation: 1106

I opened an "Advisory Call" at Microsoft for this issue, here is their answer (in German, English below):

S.DS.AM (System.DirectoryServices.Accountmanagement) ist nun nicht der Renner unter den Programmierschnittstellen, Bequemlichkeit ist Trumpf, perf-issues mit großen Gruppen sind also by Design. Wenn er auf Performance aus ist, sollte er S.DS.P (System.DirectoryServices.Protocols) oder plain LDAP verwenden.“

The meaningful translation in English would be:

Comparing the APIs, S.DS.AM (System.DirectoryServices.Accountmanagement) is not a "racer", but comfort is trump. Performance issues for larger groups is by design. When performance matters, use S.DS.P (System.DirectoryServices.Protocols) or plain LDAP.

I created a Console application in order to measure the differences of adding and removing a user from a group in milliseconds.

AccountManagement

public static void InsertGroupAccountManagement(UserPrincipal userPrincipal)
{
    using (GroupPrincipal adGroup = GroupPrincipal.FindByIdentity(_principalGroupContext, IdentityType.Guid, PRODUCT_USER_GROUP_ID))
    {
        adGroup.Members.Add(userPrincipal);
        adGroup.Save();
        adGroup.Members.Remove(userPrincipal);
        adGroup.Save();
    }
}

DirectoryServices

public static void InsertGroupDirectoryServices(string samAccountName)
{
    DirectoryEntry groupEntry = new DirectoryEntry("LDAP://server.address/CN=PSO_PRODUCT_USER,OU=PSO_,OU=Groups,OU=_PRODUCT,DC=address,DC=server", "USERNAME", "PASSWORD");
    string userDn = String.Concat("LDAP://server.address/CN=", samAccountName, ",OU=Users,OU=_PRODUCT,DC=address,DC=server");
    DirectoryEntry userEntry = new DirectoryEntry(userDn, "USERNAME", "PASSWORD");
    groupEntry.Invoke("Add", new object[] { userDn });
    groupEntry.CommitChanges();            
    groupEntry.Invoke("Remove", new object[] { userDn });
    groupEntry.CommitChanges();            
    groupEntry.Close();
}

Protocols

public static void InsertGroupProtocols(string samAccountName)
{
    LdapDirectoryIdentifier ldapDirectoryIdentifier = new LdapDirectoryIdentifier("server.address");
    NetworkCredential credentials = new NetworkCredential("USERNAME", "PASSWORD");
    LdapConnection ldapConnection = new LdapConnection(ldapDirectoryIdentifier, credentials);
    ldapConnection.SessionOptions.ProtocolVersion = 3;
    ldapConnection.SessionOptions.Signing = true;
    ldapConnection.SessionOptions.Sealing = true;
    ldapConnection.AuthType = AuthType.Negotiate;
    ldapConnection.Bind();

    // Add
    DirectoryAttributeModification addDirectoryModification = new DirectoryAttributeModification();
    addDirectoryModification.Name = "member";
    addDirectoryModification.Add(String.Concat("CN=", samAccountName, ",OU=Users,OU=_PRODUCT,DC=address,DC=server"));
    addDirectoryModification.Operation = DirectoryAttributeOperation.Add;

    ModifyRequest addRequest = new ModifyRequest("CN=PSO_PRODUCT_USER,OU=PSO_,OU=Groups,OU=_PRODUCT,DC=address,DC=server", addDirectoryModification);
    ModifyResponse addResponse = ldapConnection.SendRequest(addRequest) as ModifyResponse;

    // Remove
    DirectoryAttributeModification deleteDirectoryModification = new DirectoryAttributeModification();
    deleteDirectoryModification.Name = "member";
    deleteDirectoryModification.Add(String.Concat("CN=", samAccountName, ",OU=Users,OU=_PRODUCT,DC=address,DC=server"));
    deleteDirectoryModification.Operation = DirectoryAttributeOperation.Delete;
    
    ModifyRequest deleteRequest = new ModifyRequest("CN=PSO_PRODUCT_USER,OU=PSO_,OU=Groups,OU=_PRODUCT,DC=address,DC=server", deleteDirectoryModification);
    ModifyResponse deleteResponse = ldapConnection.SendRequest(deleteRequest) as ModifyResponse;
}

Result table in milliseconds

Running 10 tests in a row

Result table of time taken

So in my particular case the solution via DirectoryServices / DirectoryEntry is the fastest.

Upvotes: 5

Related Questions