Reading a ranged property from Active Directory using C#

You can find a PDF with this article.

Whenever reading data from Active Directory, the data must be read in multiple pages if there is a lot of data to be sent. A DirectorySearcher automatically reads all data without any extra line of code (Ok, this is not fully correct. We have to set the PageSize property. If we do not set this property, the number of results is limited!).

But as soon as we want to get all data from a property, this is something we have to do on our own. This can be done with a DirectorySearcher instance. But for this we have to understand, how ranged properties work with a DirectorySearcher.

A ranged property can be requested through adding the range to the property name: So if we want to check some group members, we do not ask for “member” – instead we can ask for “member;range=1000-*”

But when we play around with these ranged properties, we will find behaviours that could be unexpected:

So one thing to take care of: The range that comes back can be different than the range we requested. The easiest situation for this could be to request 1000 elements but the server only wants to give back 500. So a request of “member;range=1000-2000” could give us a property “mamber;range=1000-1500” instead.

Another important thing to take care of: When we request more elements that available, we might get an error. So if we request “member;range=1000-2000” but there are only 1500 member available, we could receive an error. (In which case we need to query from 1000-* to receive the last elements!)

So with this knowledge it should be quite easy to write some code that reads a full property with taking full care of paging if required:

  • We request a paged property with ;range=0-* and then put the results of this query into a List.
  • We cannot know if we got all results or not so we simply start to read another page: So if we got 500 elements, we try to read from 500 – * so we get more elements if there are more.
  • We continue till we no longer get any data back.

So a solution that reads a full property could be (Copy & Paste the code so you can also read the lines that are to long):

using System;
using System.Collections.Generic;
using System.DirectoryServices;
using System.Globalization;
using System.Linq;

namespace RangedProperty
{
    public class RangedProperty {
        public const string RangeSeparator = "-";
        public const string RangeKey = ";range=";
        public const string RangedAttributeFormat = "{0}" + RangeKey + "{1}" + RangeSeparator + "{2}";

        public static string Escape(Guid? value)
        {
            if (!value.HasValue)
                return @"0";

            return value.Value.ToByteArray().Aggregate("", (current, idByte) => current + (@"\" + idByte.ToString("x2", CultureInfo.InvariantCulture)));
        }

        public static string GetResultName(SearchResult result, string propertyName)
        {
            string resultName = (from string name in result.Properties.PropertyNames
                                 where name.StartsWith(propertyName + RangeKey, StringComparison.OrdinalIgnoreCase)
                                 select name).FirstOrDefault();

            if (resultName != null)
                return resultName;

            resultName = (from string name in result.Properties.PropertyNames
                          where String.Compare(name, propertyName, StringComparison.OrdinalIgnoreCase) == 0
                          select name).FirstOrDefault();

            return resultName;
        }

        public static bool ParseRangedPropertyName(string propertyName, out int start, out int end)
        {
            start = 0;
            end = 0;
            int rangeLocation = propertyName.IndexOf(RangeKey, StringComparison.OrdinalIgnoreCase);
            int seperatorLocation = propertyName.IndexOf(RangeSeparator, StringComparison.OrdinalIgnoreCase);

            if (rangeLocation <= 0 || seperatorLocation <= 0
                || !Int32.TryParse(propertyName.Substring(rangeLocation + RangeKey.Length, seperatorLocation - rangeLocation - RangeKey.Length), out start))
                return false;

            if (propertyName.Substring(seperatorLocation + 1).Equals("*"))
                end = Int32.MaxValue;

            if (end != Int32.MaxValue && !Int32.TryParse(propertyName.Substring(seperatorLocation + 1), out end))
                    return false;

            return true;
        }

        public static T[] Read<T>(DirectorySearcher searcher, Guid objectId, string propertyName)
        {
            searcher.Filter = "(objectGUID=" + Escape(objectId) + ")";
            SearchResult result;
            try {
                searcher.PropertiesToLoad.Add(String.Format(RangedAttributeFormat, propertyName, 0, "*"));
                result = searcher.FindOne();
            }
            catch (DirectoryServicesCOMException adsError)
            {
                if (adsError.ErrorCode == -2147016672)
                {
                    searcher.PropertiesToLoad.Clear();
                    searcher.PropertiesToLoad.Add(propertyName);
                    result = searcher.FindOne();
                }
                else {
                    throw;
                }
            }

            if (result == null)
                return null;

            var results = new List<T>();
            var resultPropertyName = GetResultName(result, propertyName);
            if (!String.IsNullOrEmpty(resultPropertyName))
            {
                results.AddRange(result.Properties[resultPropertyName].Cast<T>().ToArray());

                int start, end;
                if (ParseRangedPropertyName(resultPropertyName, out start, out end))
                {
                    if (end != Int32.MaxValue)
                    {
                        var lastRange = false;
                        while (!lastRange)
                        {
                            searcher.PropertiesToLoad.Clear();
                            searcher.PropertiesToLoad.Add(String.Format(RangedAttributeFormat, propertyName, results.Count, "*"));
                            try {
                                result = searcher.FindOne();
                            }
                            catch (DirectoryServicesCOMException ex)
                            {
                                if (ex.ErrorCode == -2147016672)
                                {
                                    break;
                                }
                                throw; 
                            }

                            resultPropertyName = GetResultName(result, propertyName);
                            if (!String.IsNullOrEmpty(resultPropertyName))
                            {
                                T[] values = result.Properties[resultPropertyName].Cast<T>().ToArray();
                                results.AddRange(values);
                                if (values.Length == 0)
                                    lastRange = true;
                            }
                        }
                    }
                }
            }

            return results.ToArray();
        }
    }
}

Feel free to send me (konrad@neitzel.de) a small note if you found this usefull or you have any further questions.

Advertisements
This entry was posted in Software Development. Bookmark the permalink.

2 Responses to Reading a ranged property from Active Directory using C#

  1. Paolo says:

    Thanks a lot, you gave to me the starting point and some useful functions..
    But it seems to me that there be an error in the code:

    public static bool ParseRangedPropertyName(string propertyName, out int start, out int end){}

    if (ParseRangedPropertyName(resultPropertyName, out end, out start))

    shouldn’t the parameters end and start be inverted?

    • Konrad says:

      Hi Paolo,
      thank you for pointing out that stupid error. Of course the parameters must be switched!
      (Just wondering how I was able to introduce this error. Simply shows that it is never a good idea to have code that is not unit tested even if you just reproduce a simple example of some more complex production code.)

      With kind regards,

      Konrad

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s