Search code examples
c#.netdirectoryservicesuserprincipalprincipalcontext

Memory Leak when using PrincipalSearcher.FindAll()


I too have a long running service using plugins and appdomains and am having a memory leak due to using directoryservices. Note that I am using system.directoryservices.accountmanagement but it is my understanding that it uses the same underlying ADSI API's and hence is prone to the same memory leaks.

I've looked at all the CLR memory counters and the memory isn't being leaked there, and is all returned either on a forced GC or when I unload the appdomain. The leak is in private bytes which continually grow. I searched on here and have seen some issues related to a memory leak when using the ADSI API's but they seem to indicate that simply iterating over the directorysearcher fixes the problem. But as you can see in the code below, I am doing that in a foreach block and still the memory is being leaked. Any suggestions? Here is my method:

public override void JustGronkIT()
{
    using (log4net.ThreadContext.Stacks["NDC"].Push(GetMyMethodName()))
    {
        Log.Info("Inside " + GetMyMethodName() + " Method.");
        System.Configuration.AppSettingsReader reader = new System.Configuration.AppSettingsReader();
        //PrincipalContext AD = null;
        using (PrincipalContext AD = new PrincipalContext(ContextType.Domain, (string)reader.GetValue("Domain", typeof(string))))
        {
            UserPrincipal u = new UserPrincipal(AD);
            u.Enabled = true;
            //u.Surname = "ju*";
            using (PrincipalSearcher ps = new PrincipalSearcher(u))
            {
                myADUsers = new ADDataSet();
                myADUsers.ADUsers.MinimumCapacity = 60000;
                myADUsers.ADUsers.CaseSensitive = false;
                foreach (UserPrincipal result in ps.FindAll())
                {
                     myADUsers.ADUsers.AddADUsersRow(result.SamAccountName, result.GivenName, result.MiddleName, result.Surname, result.EmailAddress, result.VoiceTelephoneNumber,
                            result.UserPrincipalName, result.DistinguishedName, result.Description);
                 }
                 ps.Dispose();
            }
            Log.Info("Number of users: " + myADUsers.ADUsers.Count);
            AD.Dispose();
            u.Dispose();
        }//using AD
    }//Using log4net
}//JustGronkIT

I made the following changes to the foreach loop and it's better but private bytes still grows and is never reclaimed.

 foreach (UserPrincipal result in ps.FindAll())
 {
     using (result)
     {
         try
         {
             myADUsers.ADUsers.AddADUsersRow(result.SamAccountName, result.GivenName,           result.MiddleName, result.Surname, result.EmailAddress, result.VoiceTelephoneNumber,                                        result.UserPrincipalName, result.DistinguishedName, result.Description);
             result.Dispose();
         }
         catch
         {
             result.Dispose();
         }
     }
 }//foreach

Solution

  • I spoke too soon, simply being aggressive with calling Dispose() did NOT solve the problem over the long run. The real solution? Stop using both directoryservices and directoryservices.accountmanagement and use System.DirectoryServices.Protocols instead and do a paged search of my domain because there's no leak on Microsoft's side for that assembly.

    As requested, here's some code to illustrate the solution I came up with. Note that I also use a plugin architecture and appDomain's and I unload the appdomain when I am done with it, though I think given that there's no leak in DirectoryServices.Protocols you don't have to do that. I only did it because I thought using appDomains would solve my problem, but since it wasn't a leak in managed code but in un-managed code, it didn't do any good.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.DirectoryServices.Protocols;
    using System.Data.SqlClient;
    using System.Data;
    using System.Data.Linq;
    using System.Data.Linq.Mapping;
    using System.Text.RegularExpressions;
    using log4net;
    using log4net.Config;
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    using System.Diagnostics;
    using System.IO;
    
    namespace ADImportPlugIn {
    
        public class ADImport : PlugIn
        {
    
            private ADDataSet myADUsers = null;
            LdapConnection _LDAP = null;
            MDBDataContext mdb = null;
            private Orgs myOrgs = null;
    
            public override void JustGronkIT()
            {
                string filter = "(&(objectCategory=person)(objectClass=user)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))";
                string tartgetOU = @"yourdomain.com";
                string[] attrs = {"sAMAccountName","givenName","sn","initials","description","userPrincipalName","distinguishedName",
                "extentionAttribute6","departmentNumber","wwwHomePage","manager","extensionName", "mail","telephoneNumber"};
                using (_LDAP = new LdapConnection(Properties.Settings.Default.Domain))
                {
                    myADUsers = new ADDataSet();
                    myADUsers.ADUsers.MinimumCapacity = 60000;
                    myADUsers.ADUsers.CaseSensitive = false;
    
                    try
                    {
                        SearchRequest request = new SearchRequest(tartgetOU, filter, System.DirectoryServices.Protocols.SearchScope.Subtree, attrs);
                        PageResultRequestControl pageRequest = new PageResultRequestControl(5000);
                        request.Controls.Add(pageRequest);
                        SearchOptionsControl searchOptions = new SearchOptionsControl(System.DirectoryServices.Protocols.SearchOption.DomainScope);
                        request.Controls.Add(searchOptions);
    
                        while (true)
                        {
                            SearchResponse searchResponse = (SearchResponse)_LDAP.SendRequest(request);
                            PageResultResponseControl pageResponse = (PageResultResponseControl)searchResponse.Controls[0];
                            foreach (SearchResultEntry entry in searchResponse.Entries)
                            {
                                string _myUserid="";
                                string _myUPN="";
                                SearchResultAttributeCollection attributes = entry.Attributes;
                                foreach (DirectoryAttribute attribute in attributes.Values)
                                {
                                    if (attribute.Name.Equals("sAMAccountName"))
                                    {
                                        _myUserid = (string)attribute[0] ?? "";
                                        _myUserid.Trim();
                                    }
                                    if (attribute.Name.Equals("userPrincipalName"))
                                    {
                                        _myUPN = (string)attribute[0] ?? "";
                                        _myUPN.Trim();
                                    }
                                    //etc with each datum you return from AD
                            }//foreach DirectoryAttribute
                            //do something with all the above info, I put it into a dataset
                            }//foreach SearchResultEntry
                            if (pageResponse.Cookie.Length == 0)//check and see if there are more pages
                                break; //There are no more pages
                            pageRequest.Cookie = pageResponse.Cookie;
                       }//while loop
                  }//try
                  catch{}
                }//using _LDAP
            }//JustGronkIT method
        }//ADImport class
    } //namespace