Friday, 10 August 2007

Range Retrieval using System.DirectoryServices.Protocols

Link pair attributes in Active Directory and ADAM can be quite big.  I don't know the official limit, but needless to say, for practical purposes you can assume they are quite large indeed.  By default, AD and ADAM will not return the entire attribute if it contains more than a certain number of values (1000 for Windows 2000 and 1500 for Windows 2003+ by default).  As such, if you truly want robust code, you need to always use what is called range retrieval for link value paired attributes.

Range retrieval is a process similar to paging in directory services, whereby you ask the directory for a certain range of particular attribute.  You know that you are using range retrieval when you see the attribute being requested in the following format:

"[attribute];range=[start]-[end]"

As an example, in the case of the 'member' attribute, you might ask for the first 1500 values like so:

"member;range=0-1499"

Notice, it is zero based so you need to take this into account.  The general algorithm as such is:

  1. Ask for as big as you can get.  This means use the "*" for the ending range to ask for it all.
  2. The directory will respond with either the actual max value (some integer), or with a "*" indicating you got everything.  If you got everything, you are done.
  3. If not, using the max value now as your step, repeatedly ask for larger and larger values inside a loop until the directory responds with a "*" as the end range.

We covered how to use range retrieval in SDS in our book, and you can download sample code that shows how from the book's website.  What we didn't cover was how to do it in SDS.P.

SDS.P is a layer closer the the metal than our ADSI based System.DirectoryServices (SDS).  As such, if you are expected to do range retrieval for SDS, you can be assured that you need to do it for SDS.P as well.  Adopting the code SDS, you get something like this (but modified to some extent):

static List<string> RangeRetrieve(

    LdapConnection connect, string dn, string attribute)

{

    int idx = 0;

    int step = 0;

    List<string> list = new List<string>();

 

    string range = String.Format(

       "{0};range={{0}}-{{1}}",

       attribute

       );

 

    string currentRange = String.Format(range, idx, "*");

 

    SearchRequest request = new SearchRequest(

        dn,

        String.Format("({0}=*)", attribute),

        SearchScope.Base,

        new string[] { currentRange }

        );

 

    SearchResultEntry entry = null;

    bool lastSearch = false;

 

    while (true)

    {

        SearchResponse response =

            (SearchResponse)connect.SendRequest(request);

 

        if (response.Entries.Count == 1) //should only be one

        {

            entry = response.Entries[0];

 

            //this might be optimized to find full step or just use 1000 for

            //compromise

            foreach (string attrib in entry.Attributes.AttributeNames)

            {

                currentRange = attrib;

                lastSearch = currentRange.IndexOf("*", 0) > 0;

                step = entry.Attributes[currentRange].Count;

            }

 

            foreach (string member in

                entry.Attributes[currentRange].GetValues(typeof(string)))

            {

                list.Add(member);

                idx++;

            }

 

            if (lastSearch)

                break;

 

            currentRange = String.Format(range, idx, (idx + step));

 

            request.Attributes.Clear();

            request.Attributes.Add(currentRange);

 

        }

        else

            break;

 

    }

    return list;

}

Happy coding... of course, if you are clever you will realize you can avoid all this range retrieval mess by using an attribute scope query (ASQ). :)

*edit: tried to fix the style for code to render in Google Reader correctly