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! :)