Difference between PrincipalSearcher and DirectorySearcher

Drew Chapin picture Drew Chapin · Apr 20, 2014 · Viewed 23.2k times · Source

I see Active Directory examples that use PrincipalSearcher and other examples that do the same thing but use DirectorySearcher. What is the difference between these two examples?

Example using PrincipalSearcher

PrincipalContext context = new PrincipalContext(ContextType.Domain);
PrincipalSearcher search = new PrincipalSearcher(new UserPrincipal(context));
foreach( UserPrincipal user in search.FindAll() )
{
    if( null != user )
        Console.WriteLine(user.DistinguishedName);
}

Example using DirectorySearcher

DirectorySearcher search = new DirectorySearcher("(&(objectClass=user)(objectCategory=person))");
search.PageSize = 1000;
foreach( SearchResult result in search.FindAll() )
{
    DirectoryEntry user = result.GetDirectoryEntry();
    if( null != user )
        Console.WriteLine(user.Properties["distinguishedName"].Value.ToString());
}

Answer

Drew Chapin picture Drew Chapin · Oct 12, 2014

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;