Share via


C# + AD: Dump COM and Use System.DirectoryServices.Protocols with LINQ to Query AD

A while back, I posted about using ADSI COM to get the data that you wanted. I want to show you how you can do this 10 times faster, using System.DirectoryServices.Protocols and LINQ.

Class for the actual AD query being performed:

 
    /// <summary>
    /// Initializes a new instance of the <see cref="LdapSearcher"/> class.
    /// </summary>
    internal class LdapSearcher
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="SearchTheForestTask"/> method.
        /// </summary>
        /// <param name="filter">The filter to use for the LDAP query.</param>
        /// <param name="sizeLimit">The limit of return objects for the query.</param>
        /// <param name="configContainer"></param>
        /// <returns></returns>
        public static async Task<SearchResponse> SearchTheForestTask(string filter, int? sizeLimit, bool configContainer)
        {
            // More information on the return statement can be found here: https://msdn.microsoft.com/en-us/library/1h3swy84.aspx
            // More information on the async/await keywords can be found here: https://msdn.microsoft.com/en-us/library/hh191443.aspx
            // More information on the Task<TResult>.Run() method can be found here: https://msdn.microsoft.com/en-us/library/hh160382(v=vs.110).aspx
            return await Task.Run(async /* Run on asynchronous thread so we don't block. */
                () =>
                {
                    // More information on the return statement can be found here: https://msdn.microsoft.com/en-us/library/1h3swy84.aspx
                    // More information on the async/await keywords can be found here: https://msdn.microsoft.com/en-us/library/hh191443.aspx
                    // More information on the Task<TResult>.Run() method can be found here: https://msdn.microsoft.com/en-us/library/hh160382(v=vs.110).aspx
                    return await Task.Run(() => SearchTheForest(filter, sizeLimit, configContainer));
                });
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="SearchTheForest"/> method.
        /// </summary>
        /// <param name="filter">The filter to use for the LDAP query.</param>
        /// <param name="sizeLimit">The limit of return objects for the query.</param>
        /// <param name="configContainer"></param>
        /// <returns></returns>
        private static SearchResponse SearchTheForest(string filter, int? sizeLimit, bool configContainer)
        {
            string targetServer = Domain.GetComputerDomain().FindDomainController().Name;

            // We can't await a Task to get RootDSE via async or it may execute AFTER we need it.
            DirectoryEntry rootDse = new DirectoryEntry("LDAP://RootDSE")
            {
                // More information on the DirectoryEntry.AuthenticationType property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.directoryentry.authenticationtype(v=vs.110).aspx
                // More information on the AuthenticationTypes enumeration can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.authenticationtypes(v=vs.110).aspx
                AuthenticationType = AuthenticationTypes.Encryption
            };

            string searchRoot = AdMethods.GetSearchRootTask(rootDse, configContainer).Result;

            // We instantiate the object to clear data.
            Ad.NewSearchResponse = null;

            // If the size limit isn't defined, we set it to 1k.
            int pageSize = sizeLimit ?? 1000;

            // We specify the page size so that we don't break all the things.
            // More information on the PageResultRequestControl class can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.pageresultrequestcontrol(v=vs.100).aspx
            PageResultRequestControl newPageResultRequestControl = new PageResultRequestControl(pageSize);

            // We include deleted objects in the search.
            // More information on the ShowDeletedControl class can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.showdeletedcontrol(v=vs.110).aspx
            ShowDeletedControl newShowDeletedControl = new ShowDeletedControl();
            
            // Establish connection to the Directory.
            LdapConnection newLdapConnection = new LdapConnection(targetServer);

            // Create the Search Request.
            // More information on the SearchRequest class be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchrequest(v=vs.110).aspx
            SearchRequest newSearchRequest = new SearchRequest
            {
                // More information on the SearchRequest.Filter property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchrequest.filter(v=vs.110).aspx
                Filter = filter,

                // We specify the search root for the query. Don't let the documentation lie to you,
                // this property does not specify the object to query FOR.
                // More information on the SearchRequest.DistinguishedName property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchrequest.distinguishedname(v=vs.110).aspx
                DistinguishedName = searchRoot,

                // More information on the SearchRequest.Scope property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchrequest.scope(v=vs.110).aspx
                // More information on the SearchScope enumeration can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.searchscope(v=vs.110).aspx
                Scope = System.DirectoryServices.Protocols.SearchScope.Subtree,

                // More information on the SearchRequest.TimeLimit property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchrequest.timelimit(v=vs.110).aspx
                TimeLimit = TimeSpanConstants.OneMinuteTimeSpan,

                // More information on the SearchRequest.RequestId property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.directoryrequest.requestid(v=vs.110).aspx
                RequestId = GuidConstants.RequestGuid.ToString(),

                // More information on the SearchRequest.Aliases property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchrequest.aliases(v=vs.110).aspx
                // More information on the DereferenceAlias enumeration can be found there: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.dereferencealias(v=vs.100).aspx
                Aliases = System.DirectoryServices.Protocols.DereferenceAlias.Never,

                // More information on the SearchRequest.TypesOnly property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchrequest.typesonly(v=vs.110).aspx
                TypesOnly = false,

                // More information on the SearchRequest.Controls property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.directoryrequest.controls(v=vs.110).aspx
                Controls = { newPageResultRequestControl, newShowDeletedControl }
            };
            if (sizeLimit.HasValue)
            {
                // More information on the SearchRequest.SizeLimit property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchrequest.sizelimit(v=vs.110).aspx
                newSearchRequest.SizeLimit = sizeLimit.Value;
            }

            // Since we're entering a critical section, let's lock so we block further calls.
            // More information on the lock keyword can be found here: https://msdn.microsoft.com/en-us/library/c5kehkcz.aspx
            lock (ObjectConstants.NewLock)
            {
                // Enter the critical section.
                // More information on the try keyword can be found here: https://msdn.microsoft.com/en-us/library/6dekhbbc.aspx
                try
                {
                    // More information on the SearchResponse class can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchresponse(v=vs.110).aspx
                    // More information on the LdapConnection.SendRequest(DirectoryRequest) method can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.ldapconnection.sendrequest(v=vs.110).aspx
                    Ad.NewSearchResponse = (SearchResponse) newLdapConnection.SendRequest(newSearchRequest, TimeSpanConstants.OneMinuteTimeSpan);
                }
                catch (LdapException exception)
                {
                    // We throw the base exception so it can be unwound by the calling thread.
                    // More information on the Exception.GetBaseException() method can be found here: https://msdn.microsoft.com/en-us/library/system.exception.getbaseexception(v=vs.110).aspx
                    throw exception.GetBaseException();
                }
            }

            // Since we need to await completion, we pass a value back.
            // More information on the return statement can be found here: https://msdn.microsoft.com/en-us/library/1h3swy84.aspx
            return Ad.NewSearchResponse;
        }
    }

The real magic - taking the return and using LINQ:

 
           // More information on the TaskFactory class can be found here: https://msdn.microsoft.com/en-us/library/system.threading.tasks.taskfactory(v=vs.110).aspx
            TaskFactory<SearchResponse> newTaskFactory = new TaskFactory<SearchResponse>();

            // More information on the using statement can be found here: https://msdn.microsoft.com/en-us/library/yh598w02.aspx
            // More information on the Task<TResult> class can be found here: https://msdn.microsoft.com/en-us/library/dd321424(v=vs.110).aspx
            // More information on the TaskFactory.StartNew(Action) method can be found here: https://msdn.microsoft.com/en-us/library/dd321439(v=vs.110).aspx
            using (Task<SearchResponse> newTask = newTaskFactory.StartNew(() => LdapSearcher.SearchTheForestTask(filter, null, false).Result))
            {
                // More information on the TaskAwaiter<TResult> class can be found here: https://msdn.microsoft.com/en-us/library/hh138386(v=vs.110).aspx
                // More information on the Task.GetAwaiter() method can be found here: https://msdn.microsoft.com/en-us/library/system.threading.tasks.task.getawaiter(v=vs.110).aspx
                TaskAwaiter<SearchResponse> newTaskAwaiter = newTask.GetAwaiter();

                // More information on the TaskAwaiter.IsCompleted property can be found here: https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.iscompleted(v=vs.110).aspx
                while (!newTaskAwaiter.IsCompleted)
                {
                    // More information on the string.Format() method can be found here: https://msdn.microsoft.com/en-us/library/system.string.format(v=vs.110).aspx
                    this.WriteDebug(string.Format("Awaiting task with id'{0}' to finish.", newTask.Id));

                    // Let's wait for task completion.
                    System.Threading.Thread.Sleep(500);
                }

                // More information on the TaskAwaiter.IsCompleted property can be found here: https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.iscompleted(v=vs.110).aspx
                if (newTaskAwaiter.IsCompleted)
                {
                    // More information on the string.Format() method can be found here: https://msdn.microsoft.com/en-us/library/system.string.format(v=vs.110).aspx
                    this.WriteDebug(string.Format("Task with Id '{0}' completed.", newTask.Id));

                    // More information on the Task.IsFaulted property can be found here: https://msdn.microsoft.com/en-us/library/system.threading.tasks.task.isfaulted(v=vs.110).aspx
                    if (!newTask.IsFaulted)
                    {
                        // More information on the TaskAwaiter.GetResult() method can be found here: https://msdn.microsoft.com/en-us/library/system.runtime.compilerservices.taskawaiter.getresult(v=vs.110).aspx
                        newTaskAwaiter.GetResult();

                        // More information on the SearchResultEntryCollection class can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchresultentrycollection(v=vs.110).aspx
                        SearchResultEntryCollection newSearchResultEntryCollection = Ad.NewSearchResponse.Entries;

                        // More information on the SearchResultEntry class can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchresultentry(v=vs.110).aspx
                        SearchResultEntry newSearchResultEntry = newSearchResultEntryCollection[0];

                        // More information on the SearchResultAttributeCollection class can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchresultattributecollection(v=vs.110).aspx
                        SearchResultAttributeCollection newSearchResultAttributeCollection = newSearchResultEntry.Attributes;

                        // Validate we don't hit the System.NullReferenceException
                        // More information on the SearchResultAttributeCollection.Values property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchresultattributecollection.values(v=vs.110).aspx
                        if (newSearchResultAttributeCollection.Values != null)
                        {
                            // To save overhead from iterating through each attribute on the object, we need to convert it into a queryable.
                            // This takes some magic but the overall result is the same data, just slightly faster.

                            // More information on the SearchResultAttributeCollection.Values property can be found here: https://msdn.microsoft.com/en-us/library/system.directoryservices.protocols.searchresultattributecollection.values(v=vs.100).aspx
                            ICollection newCollection = newSearchResultAttributeCollection.Values;

                            // More information on the Enumerable.Cast<TResult>() method can be found here: https://msdn.microsoft.com/en-us/library/bb341406(v=vs.110).aspx
                            IEnumerable newDirectoryAttributesCollection = newCollection.Cast<DirectoryAttribute>();

                            // More information on the Queryable.AsQueryable() method can be found here: https://msdn.microsoft.com/en-us/library/bb353734(v=vs.110).aspx
                            IQueryable<DirectoryAttribute> newQueryable = (IQueryable<DirectoryAttribute>) newDirectoryAttributesCollection.AsQueryable();

                            // More information on the Where() method can be found here: https://msdn.microsoft.com/en-us/library/bb546161.aspx
                            // More information on the Select() method can be found here: https://msdn.microsoft.com/en-us/library/bb546168.aspx
                            IQueryable<object[]> newQueryableObjectUno = newQueryable.Where(x => x.Name.Equals("msExchMailboxSecurityDescriptor")).Select(x => x.GetValues(typeof (byte[])));

                            // More information on the FirstOrDefault() method can be found here: https://msdn.microsoft.com/en-us/library/bb546140.aspx
                            object[] newObjectsUno = newQueryableObjectUno.FirstOrDefault();

                            // If the data is null, we went wrong somewhere but it's best to prevent System.NullReferenceException.
                            if (newObjectsUno != null)
                            {
                                byte[] newByteArrayBytes = (byte[]) newObjectsUno[0];

                                // We decode the string.
                                CommonSecurityDescriptor newCommonSecurityDescriptor = new CommonSecurityDescriptor(true, true, newByteArrayBytes, 0);
                                string attrValue = newCommonSecurityDescriptor.GetSddlForm(AccessControlSections.All);

                                this.WriteObject(attrValue);
                            }
                        }
                    }
                }
            }

While this isn't quite straight-forward, what's happening is that we're obtaining all of the properties for the object in AD, in byte format, and then performing a select against the specific property that we want to see.

Happy coding! :)