23
votes

What would be an effective way to do pagination with Active Directory searches in .NET? There are many ways to search in AD but so far I couldn't find how to do it effectively. I want to be able to indicate Skip and Take parameters and be able to retrieve the total number of records matching my search criteria in the result.

I have tried searching with the PrincipalSearcher class:

using (var ctx = new PrincipalContext(ContextType.Domain, "FABRIKAM", "DC=fabrikam,DC=com"))
using (var criteria = new UserPrincipal(ctx))
{
    criteria.SamAccountName = "*foo*";

    using (var searcher = new PrincipalSearcher(criteria))
    {
        ((DirectorySearcher)searcher.GetUnderlyingSearcher()).SizeLimit = 3;
        var results = searcher.FindAll();
        foreach (var found in results)
        {
            Console.WriteLine(found.Name);
        }
    }
}

Here I was able to limit the search results to 3 but I wasn't able to get the total number of records corresponding to my search criteria (SamAccountName contains foo) neither I was able to indicate to the searcher to skip the first 50 records for example.

I also tried using the System.DirectoryServices.DirectoryEntry and System.DirectoryServices.Protocols.SearchRequest but the only thing I can do is specify the page size.

So is the only way to fetch all the results on the client and do the Skip and Count there? I really hope that there are more effective ways to achieve this directly on the domain controller.

4
if you sepcified the size limit active directory will return the first entries up to your size limit which matches your criteria, so the only way i see it is to return everything and then start filtering your own by configuring the size limit and number of pagesSaddam Abu Ghaida
you can specify the zie limit to high number and it will be distributed on number of pages that you sepcifiedSaddam Abu Ghaida
Maybe this would help.von v.

4 Answers

9
votes

You may try the virtual list view search. The following sort the users by cn, and then get 51 users starting from the 100th one.

    DirectoryEntry rootEntry = new DirectoryEntry("LDAP://domain.com/dc=domain,dc=com", "user", "pwd");

    DirectorySearcher searcher = new DirectorySearcher(rootEntry);
    searcher.SearchScope = SearchScope.Subtree;
    searcher.Filter = "(&(objectCategory=person)(objectClass=user))";
    searcher.Sort = new SortOption("cn", SortDirection.Ascending);
    searcher.VirtualListView = new DirectoryVirtualListView(0, 50, 100);

    foreach (SearchResult result in searcher.FindAll())
    {
        Console.WriteLine(result.Path);
    }

For your use case you only need the BeforeCount, AfterCount and the Offset properties of DirectoryVirtualListView (the 3 in DirectoryVirtualListView ctor). The doc for DirectoryVirtualListView is very limited. You may need to do some experiments on how it behave.

4
votes

If SizeLimit is set to zero and PageSize is set to 500, the search will return all 12,000 results in pages of 500 items, with the last page containing only 200 items. The paging occurs transparently to the application and the application does not have to perform any special processing other than setting the PageSize property to the proper value.

SizeLimit limits the amount of results that you can retrieve at once - so your PageSize needs to be less than or equal to 1000 (Active Directory limits the maximum number of search results to 1000. In this case, setting the SizeLimit property to a value greater than 1000 has no effect.). The paging is done automagically behind the scenes when you call FindAll() etc.

For more details please refer MSDN

https://msdn.microsoft.com/en-us/library/ms180880.aspx

https://msdn.microsoft.com/en-us/library/system.directoryservices.directorysearcher.pagesize.aspx

https://msdn.microsoft.com/en-us/library/system.directoryservices.directorysearcher.sizelimit.aspx

0
votes

Waaaay late to the party, but this is what I'm doing:

I'm using FindOne() instead of FindAll() and member;range=<start>-<end> on PropertiesToLoad.

There's a catch on member;range: when it's the last page, even if you pass member;range=1000-1999 (for instance), it returns member;range=1000-*, so you have to check for the * at the end to know if there is more data.

public void List<string> PagedSearch()
{ 
    var list = new List<string>();
    bool lastPage = false;
    int start = 0, end = 0, step = 1000;

    var rootEntry = new DirectoryEntry("LDAP://domain.com/dc=domain,dc=com", "user", "pwd");

    var filter = "(&(objectCategory=person)(objectClass=user)(samAccountName=*foo*))";

    using (var memberSearcher = new DirectorySearcher(rootEntry, filter, null, SearchScope.Base))
    {
        while (!lastPage)
        {
            start = end;
            end = start + step - 1;

            memberSearcher.PropertiesToLoad.Clear();
            memberSearcher.PropertiesToLoad.Add(string.Format("member;range={0}-{1}", start, end));

            var memberResult = memberSearcher.FindOne();

            var membersProperty = memberResult.Properties.PropertyNames.Cast<string>().FirstOrDefault(p => p.StartsWith("member;range="));

            if (membersProperty != null)
            {
                lastPage = membersProperty.EndsWith("-*");
                list.AddRange(memberResult.Properties[membersProperty].Cast<string>());
                end = list.Count;
            }
            else
            {
                lastPage = true;
            }
        }
    }
    return list;
}
-1
votes
    private static DirectoryEntry forestlocal = new DirectoryEntry(LocalGCUri, LocalGCUsername, LocalGCPassword);
    private DirectorySearcher localSearcher = new DirectorySearcher(forestlocal);

     public List<string> GetAllUsers() 
    {
        List<string> users = new List<string>();

        localSearcher.SizeLimit = 10000;
        localSearcher.PageSize = 250;

        string localFilter = string.Format(@"(&(objectClass=user)(objectCategory=person)(!(objectClass=contact))(msRTCSIP-PrimaryUserAddress=*))");

        localSearcher.Filter = localFilter;

        SearchResultCollection localForestResult;

        try
        {
            localForestResult = localSearcher.FindAll();

            if (resourceForestResult != null) 
            {

                foreach (SearchResult result in localForestResult) 
                {
                    if (result.Properties.Contains("mail"))
                        users.Add((string)result.Properties["mail"][0]);
                }

            }

        }
        catch (Exception ex) 
        {

        }

        return users;
    }