понедельник, 23 мая 2011 г.

DirectoryServices.Protocols – малоизвестная альтернатива

Все знают о существовании пространства имён System.DirectoryServices. Оно содержит набор классов, являющихся тонкой обёрткой над ADSIDCOM-ориентированном API для работы с Active Directory. Но ADSI не единственный интерфейс, который можно использовать для общения с контроллерами домена. Помимо него в состав WinSDK входит библиотека LDAP API, реализующая 3-ю версию LDAP API в соответствии с RFC 2251. С её помощью приложение на C++ может получить доступ к любому LDAP серверу под управлением любой операционной системы. Но что делать, если платформа приложения – .NET, а язык разработки – C#? Ответ на этот вопрос находится в пространстве имён System.DirectoryServices.Protocols.

Что внутри?
Пространство имён System.DirectoryServices.Protocols содержит множество классов. Та часть, которая интересует нас в данный момент, является тонкой обёрткой над вышеупомянутой LDAP API. Другая его часть предоставляет доступ к протоколу Directory Services Markup Language (DSML) V2.

В отличие от классов DirectoryServices, представляющих сущности в Active Directory и находящихся на вершине иерархии, классы DirectoryServices.Protocols максимально приближены к транспортному уровню и ориентированы на представление операций, а не объектов. Для того, чтобы выполнить некое действие, вы должны создать соответствующий объект-запрос, сконфигурировать его и передать на исполнение. Результат вы получите в виде другого объекта, представляющего ответ сервера. В таблице ниже приведены имена классов, представляющих собой запросы и ответы, для наиболее часто используемых операций:
Операция Запрос Ответ
Создание AddRequest AddResponse
Удаление DeleteRequest DeleteResponse
Редактирование ModifyRequest ModifyResponse
Поиск SearchRequest SearchResponse

Давайте посмотрим, как пользоваться этими классами, на примере наиболее типичной операции – поиска. Перейдём сразу к коду:
void PrintAllUsers(string ldapServer, string searchContext)
{
    LdapConnection connection = new LdapConnection(ldapServer);

    string[] properties = new string[] {
        "displayName", "userPrincipalName" };

    try
    {
        SearchRequest request = new SearchRequest(
            searchContext,
            "(objectClass=user)",
            SearchScope.Subtree,
            properties);

        // Enable a paging
        PageResultRequestControl paging
            = new PageResultRequestControl(1000);
        request.Controls.Add(paging);

        while (true)
        {
            SearchResponse response = (SearchResponse)
                connection.SendRequest(request);

            if (response.Controls.Length != 1
                || !(response.Controls[ 0]
                    is PageResultResponseControl))
            {
                // Server does not support a paging
                throw new Exception(
                    "Server does not support a paging");
            }

            foreach (SearchResultEntry entry in response.Entries)
            {
                string displayName = GetStringAttr(
                    entry, "displayName");
                string upn = GetStringAttr(
                    entry, "userPrincipalName");
                System.Console.WriteLine(
                    "User '{0}' has UPN '{1}'",
                    displayName,
                    upn);
            }

            PageResultResponseControl pageResponse =
                (PageResultResponseControl)response.Controls[ 0];

            if (pageResponse.Cookie.Length == 0)
            {
                // Search complete
                break;
            }

            // Request the next page
            paging.Cookie = pageResponse.Cookie;
        }
    }
    catch (Exception e)
    {
        Console.WriteLine(
            "Unexpected exception occurred:\n\t{0}: {1}",
            e.GetType().Name,
            e.Message);
    }
}

string GetStringAttr(SearchResultEntry entry, string name)
{
    return entry.Attributes.Contains(name)
        ? entry.Attributes[name][ 0].ToString()
        : "<unknown>";
}
Как ясно из названия, функция PrintAllUsers выводит на консоль всех пользователей, которых она находит по указанному пути на переданном LDAP сервере. Первым делом функция создаёт объект типа LdapConnection, для установления соединения с LDAP сервером (в данном примере предполагается, что текущий пользователь имеет доступ к указанному LDAP серверу, в противном случае в конструктор LdapConnection необходимо передать информацию для аутентификации). Затем создаётся объект типа SearchRequest, для которого настраивается постраничный способ возврата информации с помощью объекта типа PageResultRequestControl.
Объекты управления наряду с запросами и ответами также являются типичными представители пространства имён DirectoryServices.Protocols. Они регулируют различные аспекты поведения клиента и сервера, например, применение ShowDeletedControl включает поиск удалённых объектов (tombstone).
Далее запрос отправляется серверу с помощью метода SendRequest, реализованного в классе LdapConnection. Результатом исполнения этого метода будет объект типа SearchResponse, содержащий ответ сервера со списком пользователей. Дальнейшие операции связаны с выводом данных и инициированием запроса следующей страницы.

Но зачем?
Очевидно, что программировать с помощью высокоуровневых сущностей пространства имён DirectoryServices проще, чем опускаться до уровня ответов и запросов из DirectoryServices.Protocols. Тогда встаёт вопрос, чем же последние могут быть полезны? Во-первых, это уже упомянутая ранее возможность взаимодействия с LDAP серверами "другой породы".

Вторая причина не так очевидна. ADSI, лежащая в основе DirectoryServices, славится своей неочевидностью в управлении памятью, что может приводить к её явному перерасходу. За примером далеко ходить не нужно, коль уж мы начали с поиска, поиском и закончим.
Приблизительным аналогом класса SearchResponse в пространстве имён DirectoryServices является класс SearchResultCollection. Он точно также принимает и сохраняет результат операции поиска. Но в отличие от SearchResponse, который всегда хранит только одну возвращённую страницу фиксированного размера, SearchResultCollection удерживает ссылки на все найденные объекты со всеми полученными атрибутами. Таким образом, вы не можете освободить объекты, которые уже обработали, и вынуждены держать их в памяти до полного завершения. А что если количество объектов измеряется десятками тысяч и они содержат сотни массивных атрибутов? Сам по себе перерасход памяти хоть и неприятен, но не критичен. Критичной является ситуация, когда объём потребляемой памяти напрямую зависит от внешнего фактора, и ваше приложение не имеет возможности на это повлиять. В своё время это было решающим аргументом в пользу использования DirectoryService.Protocols в утилите Customer Directory Integration входящей в состав Parallels Operations Automation.

Ну и последнее. Откровенно говоря, то, что программировать с помощью ADSI и классов DirectoryServices проще, – это миф. Она содержит такое количество подводных камней, прикрытых высокими абстракциями, что порой проще опуститься на самый низкий уровень, дабы понимать, что на самом деле ты делаешь.

Комментариев нет:

Отправить комментарий