47

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());
}
Dave Markle
  • 95,573
  • 20
  • 147
  • 170
Drew Chapin
  • 7,779
  • 5
  • 58
  • 84
  • For me it would be the strong typing?, better abstraction. Doesn't the Principal Searcher live in the an Active Directory management extensions library or some such. The more the commmunity know of this the better. I used it to avoid the gnarly syntax usually encountered when messing with AD. – brumScouse Apr 19 '14 at 22:30
  • System.DirectoryServices.AccountManagement is the library fwiw. – brumScouse Apr 19 '14 at 22:49
  • +1 Nice code samples! See my answer maybe it'll help answer this question – Lzh May 17 '14 at 16:50

4 Answers4

114

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;
Drew Chapin
  • 7,779
  • 5
  • 58
  • 84
  • This is a great answer. Whilst the directorysearch was quicker than the principal search I found that subsequent searches were quicker with the principalsearcher. I'm guessing there is come sort of caching under the hood. – heymega Sep 24 '15 at 12:16
  • great amount of effort put in.. +1 – krilovich Jan 12 '16 at 21:21
  • Excellent... tutorial? ;) Great job! +1 – Lzh Aug 25 '16 at 08:28
  • What is UserPrincipalEx? – Jonas Nov 17 '16 at 12:30
  • It's extension of `UserPrinciple` that makes use of the `AdvancedFiltersEx` class so that it can filter out based `msExchResourceMetaData` and `objectCategory`. – Drew Chapin Nov 20 '16 at 19:48
  • If I remember I'll post the code for it later. But it's really simple and can be find in other posts if you want to see it before I post it. – Drew Chapin Nov 20 '16 at 19:53
  • @druciferre another difference is that you can specify the domain controller using PrincipalSearcher, but you can't if you use DirectorySearcher. – David Klempfner Dec 12 '16 at 23:52
  • @Backwards_Dave, this is not accurate. You can specify it at minimum using an LDAP search root (e.g. `new DirectorySearcher("LDAP://domaincontrollername/DC=contoso,DC=local")`. There is another way too given just the DC name or to list all of the DCs. I have a code sample I can post later when I get back to my PC. I had to write a block of code to get the most accurate login time from all DCs. – Drew Chapin Dec 13 '16 at 00:33
  • @Backwards_Dave, see my Gist for an [example of using a specific DC](https://gist.github.com/druciferre/8cdd93bbf45faa54c90cbc6cf57bbc07). – Drew Chapin Dec 13 '16 at 15:27
  • great answer :D – VnDevil Jul 03 '18 at 03:49
  • I changed my Gist/GitHub username. New link for [example of using a specific DC](https://gist.github.com/drewchapin/8cdd93bbf45faa54c90cbc6cf57bbc07) – Drew Chapin Jul 11 '18 at 17:53
  • What about if you use PrincipalSearcher but using GetUnderlyingSearcher add in the PropertiesToLoad? See here: https://stackoverflow.com/questions/45357892/why-is-directorysearcher-so-slow-when-compared-to-principalsearcher – William Jun 27 '19 at 15:28
  • I want to retrieve attribute msExchRecipientDisplayType and PrincipalSearcher gives System.__ComObject. as a result it gives proper value by DirectorySearcher. but DirectorySearcher has only 1000 limit ?? – MSTdev Jun 10 '20 at 14:31
  • check this https://stackoverflow.com/questions/62306314/why-principalsearcher-gives-system-comobject-for-attribut-msexchrecipientdispl – MSTdev Jun 10 '20 at 14:42
  • Why does your `UserPrincipleEx` set `objectCategory` to `User` with `ExtensionSet` but your `AdvancedFilterSet` tests for `objectCategory` equal to `person`? – NetMage Jul 28 '20 at 22:20
  • Major issue in your code is that you're not disposing of your objects but you didn't do it with either example so I'm not sure how that would influence your results, especially doing this in a loop. I do like your extension method. – Charles Owen Aug 22 '21 at 03:34
5

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.

Lzh
  • 3,585
  • 1
  • 22
  • 36
1

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++;
    }
}
awsnap
  • 13
  • 9
  • AuthenticationTypes.FastBind increases the performance quite a bit for DirectorySearcher. Also look at your ReferralChasingOptions. – Charles Owen Aug 22 '21 at 04:06
1

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)
Charles Owen
  • 2,403
  • 1
  • 14
  • 25