Search for all matching users and security groups using System.DirectoryServices.AccountManagement I am attempting to return all users and security groups that begin with the provided substring. The following does work but I suspect there may be a better way to write this and am interested in both improving the performance and the style of the Search function.

ActiveDirectory.cs

public static class ActiveDirectory
    {

        public static IEnumerable&lt;Recipient&gt; Search(string search)
        {
            var recipients = new List&lt;Recipient&gt;();

            using (var ctx = new PrincipalContext(ContextType.Domain))
            {
                search = $"{search}*";

                var userPrincipals = new List&lt;UserPrincipal&gt;
                {
                    new UserPrincipal(ctx) {DisplayName = search},
                    new UserPrincipal(ctx) {Name = search},
                    new UserPrincipal(ctx) {SamAccountName = search}
                };

                foreach (var principal in userPrincipals)
                {
                    foreach (var usr in new PrincipalSearcher(principal).FindAll())
                    {
                        if (!(usr is UserPrincipal user) || recipients.Any(m =&gt; m.Name == (user.DisplayName ?? user.Name) &amp;&amp; m.Type == RecipientType.Group)) continue;
                        recipients.Add(new Recipient()
                        {
                            Name = user.SamAccountName,
                            DisplayName = user.DisplayName ?? user.Name,
                            Type = RecipientType.User
                        });
                    }
                }

                var groupPrincipals = new List&lt;GroupPrincipal&gt;
                {
                    new GroupPrincipal(ctx) {DisplayName = search},
                    new GroupPrincipal(ctx) {Name = search},
                    new GroupPrincipal(ctx) {SamAccountName = search}
                };

                foreach (var principal in groupPrincipals)
                {
                    foreach (var grp in new PrincipalSearcher(principal).FindAll())
                    {
                        if (!(grp is GroupPrincipal group) || recipients.Any(m =&gt; m.Name == (group.DisplayName ?? group.Name) &amp;&amp; m.Type == RecipientType.Group)) continue;
                        recipients.Add(new Recipient()
                        {
                            Name = group.SamAccountName,
                            DisplayName = group.DisplayName ?? group.Name,
                            Type = RecipientType.Group
                        });
                    }
                }
            }

            return recipients;
        }

    }


Recipient.cs

public class Recipient
    {
        public Guid? Id { get; set; }
        public string Name { get; set; }
        public string DisplayName { get; set; }
        public RecipientType Type { get; set; }

        public Guid? ChannelId { get; set; }
    }


RecipientType.cs

public enum RecipientType
    {
        User,
        Group
    }
I don't know better ways to query for principals, so below is some general comments.




        if (!(usr is UserPrincipal user) || recipients.Any(m =&gt; m.Name == (user.DisplayName ?? user.Name) &amp;&amp; m.Type == RecipientType.Group)) continue;



I think m.Type == RecipientType.Group should be m.Type == RecipientType.User



When I run your function I get an InvalidOpeartionException for this filter:


  new GroupPrincipal(ctx) {DisplayName = search}




You could consider to extend the function with an argument, that allows the client to specify the context type:

public static IEnumerable&lt;Recipient&gt; Search(string search, ContextType contextType)




The returned Principals from new PrincipalSearcher(principal).FindAll() is of the same type as the argument Principal so you can omit this check:


  if (!(grp is GroupPrincipal group)...






Both PrincipalSearcher and Principal implements IDisposable so all those objects should call Dispose() or be wrapped in a using() statement. 



You do essentially the same in the two loops so you should only have one that can handle different types of data. Below I've tried to do that, where all disposable objects are disposed.

public static IEnumerable&lt;Recipient&gt; Search(string search, ContextType contextType)
{
  search = $"{search}*";

  using (PrincipalContext context = new PrincipalContext(contextType))
  {
    var recipients = new List&lt;Recipient&gt;();

    var filterTypes = new[]
    {
      new {
        Filters = new Func&lt;Principal&gt;[]
        {
          () =&gt; new UserPrincipal(context) {DisplayName = search},
          () =&gt; new UserPrincipal(context) {Name = search},
          () =&gt; new UserPrincipal(context) {SamAccountName = search}
        },
        RepicientType = RecipientType.User },
      new {
        Filters = new Func&lt;Principal&gt;[]
        {
          //() =&gt; new GroupPrincipal(context) {DisplayName = search},
          () =&gt; new GroupPrincipal(context) {Name = search},
          () =&gt; new GroupPrincipal(context) {SamAccountName = search}
        },
        RepicientType = RecipientType.Group },
    };

    foreach (var principalFilter in filterTypes)
    {
      foreach (var filterFunc in principalFilter.Filters)
      {
        using (var filter = filterFunc())
        using (var searcher = new PrincipalSearcher(filter))
        {
          foreach (var foundPrincipal in searcher.FindAll())
          {
            using (foundPrincipal)
            {
              if (recipients.Any(m =&gt; m.Name == (foundPrincipal.DisplayName ?? foundPrincipal.Name) &amp;&amp; m.Type == principalFilter.RepicientType))
                continue;

              recipients.Add(new Recipient()
              {
                Name = foundPrincipal.SamAccountName,
                DisplayName = foundPrincipal.DisplayName ?? foundPrincipal.Name,
                Type = principalFilter.RepicientType
              });
            }
          }
        }
      }
    }

    return recipients;
  }
}


Here I use new Func&lt;Principal&gt;[] instead of just new Principal[] in order to signal, that the "client" (the loop) is responsible for cleaning up the created object.

I'm not sure I like this:

      foreach (Principal foundPrincipal in searcher.FindAll())
      {
        using (foundPrincipal)
        {
           ...


but it's doable.



Just for the exercise: a version that goes all in on LINQ with the same considerations as above:

  public static class ActiveDirectory
  {
    private static IEnumerable&lt;TPrincipal&gt; Search&lt;TPrincipal&gt;(
      this IEnumerable&lt;Func&lt;TPrincipal&gt;&gt; filters,
      IEqualityComparer&lt;TPrincipal&gt; equalityComparer,
      ContextType contextType) where TPrincipal : Principal
    {
      return filters
        .SelectMany(creator =&gt;
        {
          using (TPrincipal principal = creator())
          using (var searcher = new PrincipalSearcher(principal))
          {
            return searcher.FindAll();
          }
        })
        .Cast&lt;TPrincipal&gt;()
        .Distinct(equalityComparer)
        .ToArray();
    }

    private static IEnumerable&lt;Recipient&gt; Convert&lt;TPrincipal&gt;(this IEnumerable&lt;TPrincipal&gt; principals, RecipientType recipientType, bool disposePrincipals = true) where TPrincipal : Principal
    {
      foreach (TPrincipal principal in principals)
      {
        Recipient recipient = new Recipient
        {
          Name = principal.SamAccountName,
          DisplayName = principal.DisplayName ?? principal.Name,
          Type = recipientType
        };

        if (disposePrincipals)
          principal.Dispose();

        yield return recipient;
      }
    }

    public static IEnumerable&lt;Recipient&gt; Search(string search, ContextType contextType)
    {
      search = $"{search}*";

      using (var context = new PrincipalContext(contextType))
      {
        var userFilters = new Func&lt;UserPrincipal&gt;[]
        {
          () =&gt; new UserPrincipal(context) {DisplayName = search},
          () =&gt; new UserPrincipal(context) {Name = search},
          () =&gt; new UserPrincipal(context) {SamAccountName = search}
        };

        var groupFilters = new Func&lt;GroupPrincipal&gt;[]
        {
          //() =&gt; new GroupPrincipal(context) {DisplayName = search},
          () =&gt; new GroupPrincipal(context) {Name = search},
          () =&gt; new GroupPrincipal(context) {SamAccountName = search}
        };

        var users = userFilters.Search(new PrincipalEqualityComparer&lt;UserPrincipal&gt;(), contextType);
        var groups = groupFilters.Search(new PrincipalEqualityComparer&lt;GroupPrincipal&gt;(), contextType);

        return users.Convert(RecipientType.User).Concat(groups.Convert(RecipientType.Group)).ToArray();
      }
    }
  }


  class PrincipalEqualityComparer&lt;TPrincipal&gt; : IEqualityComparer&lt;TPrincipal&gt; where TPrincipal : Principal
  {
    public bool Equals(TPrincipal x, TPrincipal y)
    {
      if (x != null &amp;&amp; y == null || x == null &amp;&amp; y != null) return false;
      return ReferenceEquals(x, y) || string.Equals(x.Name, y.Name) || string.Equals(x.DisplayName, y.DisplayName);
    }

    public int GetHashCode(TPrincipal obj)
    {
      return 0; // obj.GetHashCode();
    }
  }