Difference between PrincipalSearcher and DirectorySearcher

I've spent a lot of time analyzing the differences between these two. Here's what I've learned.

  • DirectorySearcher comes from the System.DirectoryServices namespace.

  • PrincipalSearcher comes from the System.DirectoryServices.AccountManagement namespace, which is built on top of System.DirectoryServices. PrincipalSearcher internally uses DirectorySearcher.

  • The AccountManagement namespace (i.e. PrincipalSearcher) was designed to simplify management of User, Group, and Computer objects (i.e. Principals). In theory, it's usage should be easier to understand, and produce fewer lines of code. Though in my practice so far, it seems to heavily depend on what you're doing.

  • DirectorySearcher is more low-level and can deal with more than just User, Group and Computer objects.

  • For general usage, when you're working with basic attributes and only a few objects, PrincipalSearcher will result in fewer lines of code and faster run time.

  • The advantage seems to disappear the more advanced the tasks you're doing become. For instance if you're expecting more than few hundred results, you'll have to get the underlying DirectorySearcher and set the PageSize

    DirectorySearcher ds = search.GetUnderlyingSearcher() as DirectorySearcher;
    if( ds != null )
        ds.PageSize = 1000;
    
  • DirectorySearcher can be significantly faster than PrincipalSearcher if you make use of PropertiesToLoad.

  • DirectorySearcher and like classes can work with all objects in AD, whereas PrincipalSearcher is much more limited. For example, you can not modify an Organizational Unit using PrincipalSearcher and like classes.

Here is a chart I made to analyze using PrincipalSearcher, DirectorySearcher without using PropertiesToLoad, and DirectorySearcher with using PropertiesToLoad. All tests...

  • Use a PageSize of 1000
  • Query a total of 4,278 user objects
  • Specify the following criteria
    • objectClass=user
    • objectCategory=person
    • Not a scheduling resource (i.e. !msExchResourceMetaData=ResourceType:Room)
    • Enabled (i.e. !userAccountControl:1.2.840.113556.1.4.803:=2)

DirectorySearcher vs. PrincipalSearcher Performance Chart


Code For Each Test


Using PrincipalSearcher

[DirectoryRdnPrefix("CN")]
[DirectoryObjectClass("Person")]
public class UserPrincipalEx: UserPrincipal
{

    private AdvancedFiltersEx _advancedFilters;

    public UserPrincipalEx( PrincipalContext context ): base(context)
    {
        this.ExtensionSet("objectCategory","User");
    }

    public new AdvancedFiltersEx AdvancedSearchFilter
    {
        get {
            if( null == _advancedFilters )
                _advancedFilters = new AdvancedFiltersEx(this);
                return _advancedFilters;
        }
    }

}

public class AdvancedFiltersEx: AdvancedFilters 
{

    public AdvancedFiltersEx( Principal principal ): 
        base(principal) { }

    public void Person()
    {
        this.AdvancedFilterSet("objectCategory", "person", typeof(string), MatchType.Equals);
        this.AdvancedFilterSet("msExchResourceMetaData", "ResourceType:Room", typeof(string), MatchType.NotEquals);
    }
}

//...

for( int i = 0; i < 10; i++ )
{
    uint count = 0;
    Stopwatch timer = Stopwatch.StartNew();
    PrincipalContext context = new PrincipalContext(ContextType.Domain);
    UserPrincipalEx filter = new UserPrincipalEx(context);
    filter.Enabled = true;
    filter.AdvancedSearchFilter.Person();
    PrincipalSearcher search = new PrincipalSearcher(filter);
    DirectorySearcher ds = search.GetUnderlyingSearcher() as DirectorySearcher;
    if( ds != null )
        ds.PageSize = 1000;
    foreach( UserPrincipalEx result in search.FindAll() )
    {
        string canonicalName = result.CanonicalName;
        count++;
    }

    timer.Stop();
    Console.WriteLine("{0}, {1} ms", count, timer.ElapsedMilliseconds);
}


Using DirectorySearcher

for( int i = 0; i < 10; i++ )
{
    uint count = 0;
    string queryString = "(&(objectClass=user)(objectCategory=person)(!msExchResourceMetaData=ResourceType:Room)(!userAccountControl:1.2.840.113556.1.4.803:=2))";

    Stopwatch timer = Stopwatch.StartNew();

    DirectoryEntry entry = new DirectoryEntry();
    DirectorySearcher search = new DirectorySearcher(entry,queryString);
    search.PageSize = 1000;
    foreach( SearchResult result in search.FindAll() )
    {
        DirectoryEntry user = result.GetDirectoryEntry();
        if( user != null )
        {
            user.RefreshCache(new string[]{"canonicalName"});
            string canonicalName = user.Properties["canonicalName"].Value.ToString();
            count++;
        }
    }
    timer.Stop();
    Console.WriteLine("{0}, {1} ms", count, timer.ElapsedMilliseconds);
}


Using DirectorySearcher with PropertiesToLoad

Same as "Using DirectorySearcher but add this line

search.PropertiesToLoad.AddRange(new string[] { "canonicalName" });

After

search.PageSize = 1000;

PrincipalSearcher is used to query the Directory for Groups or Users. DirectorySearcher is used to query all kinds of objects.

I used DirectorySearcher to get groups before then I discovered PrincipalSearcher so when I replaced the former with the latter, the speed of my program improved (maybe it was just PrincipalSearcher that created a better query for me. For what I care, PrincipalSearcher was just easier to use and more suitable for the task of getting pricipals.

DirectorySearcher on the other hand is more general as it can get other kinds of objects. This is why it can't be strongly typed as mentioned in the comments. PrincipalSearcher is all about principals so it will have strongly typed objects that pertain to principals, and this is why also you don't need to tell it to get you an object of kind user or group, it will be implied by the Principal classes you use.