I had a crack at this using a number of different approaches, as a learning experience.
What I found for myself was that all methods could list a set of adspath
values pretty quickly, but once introducing a Console.WriteLine in the iteration caused the performance to drastically vary.
My limited C# knowledge led me to experiment with various methods such as IEnumerator
straight over the DirectoryEntry, PrincipleSearcher
with context, but both of these methods are slow, and vary greatly depending on what is being done with the information
In the end, this is what I ended up with. It was far and away the fastest, and doesn't take any noticeable performance hit when increasing the options to parse.
Note: this is actually a complete copy/paste powershell wrapper for the class, as I am not currently near a VM with Visual Studio.
$Source = @"
// " " <-- this just makes the code highlighter work
// Syntax: [soexample.search]::Get("LDAP Path", "property1", "property2", "etc...")
// Example: [soexample.search]::Get("LDAP://CN=Users,DC=mydomain,DC=com","givenname","sn","samaccountname","distinguishedname")
namespace soexample
{
using System;
using System.DirectoryServices;
public static class search
{
public static string Get(string ldapPath, params string[] propertiesToLoad)
{
DirectoryEntry entry = new DirectoryEntry(ldapPath);
DirectorySearcher searcher = new DirectorySearcher(entry);
searcher.SearchScope = SearchScope.OneLevel;
foreach (string p in propertiesToLoad) { searcher.PropertiesToLoad.Add(p); }
searcher.PageSize = 100;
searcher.SearchRoot = entry;
searcher.CacheResults = true;
searcher.Filter = "(sAMAccountType=805306368)";
SearchResultCollection results = searcher.FindAll();
foreach (SearchResult result in results)
{
foreach (string propertyName in propertiesToLoad)
{
foreach (object propertyValue in result.Properties[propertyName])
{
Console.WriteLine(string.Format("{0} : {1}", propertyName, propertyValue));
}
}
Console.WriteLine("");
}
return "";
}
}
}
"@
$Asem = ('System.DirectoryServices','System')
Add-Type -TypeDefinition $Source -Language CSharp -ReferencedAssemblies $Asem
I ran this on a particular domain that had 160 users, and here is the outcome;
Using the example command in the code comments:
PS > Measure-Command { [soexample.search]::Get(args as above..) }
Output:
givenname : John
sn : Surname
samaccountname : john.surname
distinguishedname : CN=John Surname,CN=Users,DC=mydomain,DC=com
etc ... 159 more ...
Days : 0
Hours : 0
Minutes : 0
Seconds : 0
Milliseconds : 431
Ticks : 4317575
TotalDays : 4.99719328703704E-06
TotalHours : 0.000119932638888889
TotalMinutes : 0.00719595833333333
TotalSeconds : 0.4317575
TotalMilliseconds : 431.7575
Every additional string argument given, seems to increase the total processing time by about 100ms.
Running it with only samaccountname
takes only 0.1s to list 160 users, parsed into the console.
Using Microsoft's example here, and modifying it to just list one property, took over 3 seconds, and every additional property took about a second.
A couple notes:
(sAMAccountType=805306368)
turns out to be more efficient than (&(objectClass=user)(objectCategory=person))
(see https://stackoverflow.com/a/10053397/3544399) and many other examples
searcher.CacheResults = true;
didn't seem to make any difference (in my domain anyway) whether it was true or explicitly false.
searcher.PageSize = 100;
makes a measurable difference. I believe the default MaxPageSize
on a 2012R2 DC is 1000 (https://technet.microsoft.com/en-us/library/cc770976(v=ws.11).aspx)
The properties are not case sensitive (i.e. whatever is given to the searcher is returned in result.Properties.PropertyNames
, hence why the foreach
loop simply iterates those propertiesToLoad
)
The three foreach
loops at first glance seem un-necessary, but every successful removal of a loop ended up costing much more overhead in cast conversions and running through method extensions.
There may be better ways still, I've seen some elaborate examples with threading and result caching that I just wouldn't know what to do with, but the tuned DirectorySearcher
does seem to be the most flexible, and this code here only requires System
and System.DirectoryServices
namespaces.
Not sure exactly what you do with your "//do stuff"
as to whether this would help or not, but I did find this an interesting exercise as I didn't know there were so many ways to do something like this.