Question

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());
}
Was it helpful?

Solution

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;

OTHER TIPS

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.

DirectorySearcher is by far faster. The example from @DrewChapin can be taken even further. By my tests, about 10 times further/faster. I was able to pull 721 computer cn's in 3.8 seconds with his code. Pretty fast. With my changes I did it in 0.38 seconds. Depending on what you're doing, this could be huge. I used this in a predictive account search (Start typing the name and a combobox populates. A very short System.Timer starts after each keypress and is cancelled by a keypress. If the timer elapses, the list updates. Efficiency is huge for this.)

DirectoryEntry entry = new DirectoryEntry();
DirectorySearcher search = new DirectorySearcher(entry,queryString);
search.PageSize = 1000;
// *** Added following line
search.PropertiesToLoad.AddRange(new string[] { "canonicalName" });
foreach( SearchResult result in search.FindAll() )
{
    //DirectoryEntry user = result.GetDirectoryEntry();
    // *** Work directly with result instead of user
    if( result != null )
    {
        //user.RefreshCache(new string[]{"canonicalName"});
        // *** Following line modified
        string canonicalName = result.Properties["canonicalName"][0].ToString();
        count++;
    }
}

One thing that has made my PrincipalSearcher queries much faster than before is to use this syntax. I convert it to a Queryable after returning FindAll(). I'm returning 3000 records in about 2 seconds. I also run this in an async method.

using (var ctx = new PrincipalContext(ContextType.Domain, "domainname", 
  "DOMAIN_DESCRIPTOR"))
{
    using (var u = new UserPrincipal(ctx))
    {
          var result = new PrincipalSearcher(u)
                        .FindAll()
                        .AsQueryable()
                        .Take(3000)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top