Проектирование пользовательского поставщика

120

Исходный код доступа к хранилищу данных - UserAndRole

Теперь рассмотрим, как реализовать собственного поставщика для служб членства и ролей. Создание пользовательского поставщика предусматривает выполнение следующих шагов:

  1. Проектирование и создание лежащего в основе хранилища данных. (Рассматривается в этой статье.)

  2. Создание служебных классов для доступа к хранилищу. (Рассматривается в этой статье.)

  3. Создание класса-наследника MembershipProvider. (Рассматривается в статье «Реализация поставщика Membership API».)

  4. Создание класса-наследника RoleProvider. (Рассматривается в статье «Реализация поставщика Roles API».)

  5. Создание тестового приложения для испытания поставщиков. (Рассматривается в статье «Использование классов пользовательских поставщиков».)

  6. Конфигурирование пользовательских поставщиков в тестовом приложении.

  7. Использование новых пользовательских поставщиков в рабочем приложении.

Реализация пользовательских поставщиков достаточно прямолинейна, но требует некоторого времени, т.к. требуется реализовать множество методов и свойств. В этой и последующих статьях будет создан пользовательский поставщик членства и ролей, работающий с XML-файлом и лежащим в основе источником данных. Файлы XML - не слишком хорошее решение для масштабируемых приложений, но может оказаться неплохой альтернативой, если разрабатывается простое приложение, которое должно размещаться на сайте поставщика, не имеющего доступа к базе данных SQL Server.

Обзор проектного решения пользовательского поставщика

Перед созданием пользовательского поставщика следует продумать общую структуру всего решения. Цель состоит в том, чтобы сохранить лежащую в основе функциональность насколько возможно простой. Это позволит сосредоточиться на действительной реализации поставщиков членства и ролей. В случае XML простейший способ загружать и сохранять данные в файлах XML - это XML-сериализация. Она дает возможность сохранять полный граф объекта в файле одним вызовом функции и читать его также одним вызовом другой функции.

Помните, что в момент создания экземпляра сериализатора XmlSerializer ему необходимо сообщить тип, который требуется сериализовать и десериализовать. Перед использованием классов XmlTextReader, XmlTextWriter и XmlSerializer также не забудьте импортировать в коде пространства имен System.Xml и System.Xml.Serialization.

Поскольку такие классы, как MembershipUser, не предоставляют доступа к некоторой информации - например, к паролю - применять их с XML-сериализацией непосредственно нельзя. XML-сериализация требует, чтобы все сохраняемые свойства и члены были общедоступными. Таким образом, потребуется создать собственные представления пользователей и ролей в виде служебных классов для хранилища заднего плана. Эти классы никогда не будут передаваться в приложение, которое просто полагается на существующие классы членства. (Будет предусмотрена некоторая простая логика отображения между внутренним представлением пользователя и классом MembershipUser.)

На рисунке ниже показано проектное решение для пользовательского поставщика:

Проектное решение для пользовательского поставщика

Как уже упоминалось, классы SimpleUser и SimpleRole обеспечивают возможность XML-сериализации. Хотя это требует определенной логики отображения для поддержки MembershipUser, вся реализация значительно облегчается. UserStore и RoleStore - служебные классы для инкапсуляции доступа к XML-файлу. Эти классы включают функции для загрузки и сохранения XML-файлов вместе с некоторыми базовыми служебными функциями поиска информации в хранилище.

И, наконец, модель включает классы XmlMembershipProvider и XmlRoleProvider. Класс XmlMembershipProvider наследует базовую функциональность от MembershipProvider, в то время как XmlRoleProvider - от RoleProvider. Оба базовых класса определены в пространстве имен System.Web.Security.

Проектирование и реализация пользовательского хранилища

После того как построена общая архитектура, можно приступать к лежащему в основе хранилищу данных. В данном примере хранилище данных будет состоять из XML-файла для хранения пользователей и XML-файла для хранения ролей. Чтобы максимально облегчить доступ к этим файлам, в качестве первичного механизма чтения и записи этих файлов применяется XML-сериализация. Таким образом, понадобятся некоторые классы для представления данных, сохраняемых в XML-файлах, либо в виде общедоступных полей, либо свойства, как показано ниже:

using System;
using System.Collections.Specialized;
   
namespace Professorweb.Providers.Store
{
    public class SimpleUser
    {
        public Guid UserKey = Guid.Empty;

        public string UserName = "";
        public string Password = "";
        public string PasswordSalt = "";

        public string Email = "";
        public DateTime CreationDate = DateTime.Now;
        public DateTime LastActivityDate = DateTime.MinValue;
        public DateTime LastLoginDate = DateTime.MinValue;
        public DateTime LastPasswordChangeDate = DateTime.MinValue;
        public string PasswordQuestion = "";
        public string PasswordAnswer = "";
        public string Comment;
    }

    public class SimpleRole
    {
        public string RoleName = "";
        public StringCollection AssignedUsers = new StringCollection();
    }
}

В данном примере в качестве ProviderUserKey используется GUID-идентификатор для уникальной идентификации пользователей в хранилище, подобно первичному ключу, применяемому для уникальной идентификации записей в таблице базы данных. Для каждого пользователя будет сохраняться имя, пароль (хешированный), адрес электронной почты, некоторая информация, связанная с датами, контрольный вопрос вместе с ответом, а также комментарии.

Что касается ролей, то будет храниться имя и ассоциации к пользователям. Для простоты каждая роль будет хранить массив имен пользователей (в виде строк), ассоциированных с ролью. Сериализированная версия массива пользователей будет хранилищем пользователей, в то время как сериализированная версия массива ролей будет хранилищем ролей, как показано на рисунке ниже:

Сериализированные версии массивов SimpleUser и SimpleRole

Обратите внимание, что на этом рисунке показаны сериализированные версии пользователей и ролей из окончательной версии разрабатываемого поставщика. Как вы узнаете далее, используются пароли со случайным хешем. Более того, может возникнуть вопрос о том, почему в файл XML не сериализируются комментарии? Дело в том, что XmlSerializer сериализирует поля только в том случае, если их значение отлично от null (за исключением случаев, когда указано обратное через атрибуты XmlSerializer, примененные к свойствам класса).

Другой аспект проектного решения, который должен быть обдуман - способ обращения к хранилищу. Для каждого хранилища необходим только один экземпляр в памяти (UserStore и RoleStore), чтобы сэкономить ресурсы и избежать слишком частой загрузки XML-файлов. Это можно реализовать с помощью шаблона Singleton (Одиночка) - решения, гарантирующего, что внутри процесса существует только один экземпляр класса. Это обеспечивает приватный конструктор и общедоступный статический метод для получения экземпляра.

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

Логика инкапсулирована в шаблоне Singleton, который реализован классами хранилища, что видно в следующем коде. Если посмотреть на следующий фрагмент кода, станет ясно, что для каждого хранилища XML-файла, например, множества пользователей, в памяти удерживается один экземпляр UserStore, который инкапсулирует доступ к одному хранилищу в изолированной манере (та же логика применена в RoleStore для хранения ролей). Хотя это средство в поставщике не используется, упомянутый шаблон может оказаться интересным для других сценариев. Рассмотрим все эти аспекты на основе класса UserStore:

using System;
using System.Collections.Specialized;
using System.Text;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using System.Web;
using System.Web.Security;
using System.Collections.Generic;

namespace Professorweb.Providers.Store
{
    public class UserStore
    {
        private string _FileName;
        private List<SimpleUser> _Users;
        private XmlSerializer _Serializer;

        private static Dictionary<string, UserStore> _RegisteredStores;

        private UserStore(string fileName)
        {
            _FileName = fileName;
            _Users = new List<SimpleUser>();
            _Serializer = new XmlSerializer(typeof(List<SimpleUser>));

            LoadStore(_FileName);
        }

        public static UserStore GetStore(string fileName)
        {
            // Создать зарегистрированное хранилище, если оно еще не существует
            if (_RegisteredStores == null)
                _RegisteredStores = new Dictionary<string, UserStore>();

            if (!_RegisteredStores.ContainsKey(fileName))
            {
                _RegisteredStores.Add(fileName, new UserStore(fileName));
            }

            // Вернуть соответствующее хранилище no переданному имени файла
            return _RegisteredStores[fileName];
        }
        
        // ...
    }
}

Класс содержит несколько приватных членов - для имени файла хранилища, списка пользователей и экземпляра XmlSerializer, используемого для чтения и записи файлов.

Поскольку конструктор приватный, экземпляры не могут создаваться за пределами класса. Внешние классы могут получать экземпляры только через вызов общедоступного статического метода GetStore().

Реализация шаблона Singieton в данном случае особенна. Она создает одиночные экземпляры на основе имен файлов. Для каждого файла, обрабатываемого поставщиком, создается по одному экземпляру класса UserStore. Если в одном и том же процессе более одного веб-приложения использует этого поставщика, необходимо обеспечить, чтобы разные экземпляры создавались для разных имен файлов. Таким образом, класс не управляет одной статической переменной для единственного экземпляра; вместо этого он включает словарь, содержащий все экземпляры класса, по одному для каждого имени файла.

Поскольку для записи и загрузки данных из хранилища применяется XML-сериализация, функции загрузки и сохранения достаточно просты:

public class UserStore
{
     // ...

     private void LoadStore(string fileName)
     {
            try
            {
                // Обратите внимание, что если файл не существует, в этот момент он игнорируется. 
                // При операции записи файл хранилища создается автоматически 
                // реализацией хранилища
                if (System.IO.File.Exists(fileName))
                {
                    using (XmlTextReader reader = new XmlTextReader(fileName))
                    {
                        _Users = (List<SimpleUser>)_Serializer.Deserialize(reader);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception(
                    string.Format("Не удалось загрузить файл {0}", fileName), ex);
            }
     }

     private void SaveStore(string fileName)
     {
            try
            {
                if (System.IO.File.Exists(fileName))
                    System.IO.File.Delete(fileName);

                using (XmlTextWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
                {
                    _Serializer.Serialize(writer, _Users);
                }
            }
            catch (Exception ex)
            {
                throw new Exception(
                    string.Format("Не удалось сохранить файл {0}", fileName), ex);
            }
     }
        
     // ...
}

Обе функции являются приватными, поскольку вызываются только внутри самого класса. Метод LoadStore() вызывается в конструкторе класса UserStore. В этом методе инициализируется приватная переменная Users. Каждый последующий запрос обращается к коллекции _Users класса хранилища. С другой стороны, метод SaveStore() просто сериализирует коллекцию Users в файл, указанный в приватной переменной-члене _FileName, которая передается через конструктор (и непрямо - через статический метод GetStore()).

И, наконец, класс поддерживает несколько методов для опроса информации из коллекции Users:

public class UserStore
{
     // ...

     public List<SimpleUser> Users
     {
            get { return _Users; }
     }

     public void Save()
     {
            SaveStore(_FileName);
     }

     public SimpleUser GetUserByName(string name)
     {
            return _Users.Find(
                user => string.Equals(name, user));
     }

     public SimpleUser GetUserByEmail(string email)
     {
            return _Users.Find(
                user => string.Equals(email, user.Email));
     }

     public SimpleUser GetUserByKey(Guid key)
     {
            return _Users.Find(
                user => user.UserKey.CompareTo(key) == 0);
     }
}

Свойство Users - это простое свойство, которое позволяет действительному поставщику (XmlMembershipProvider) иметь доступ к пользователям хранилища. После того как реализация поставщика изменяет что-то внутри хранилища (например, изменит свойства пользователя), она вызывает общедоступный метод Save(), который внутри вызывает SaveStore() для сериализации информации обратно в файл, указанный в приватной переменной FileName данного экземпляра.

Остальные методы предназначены для поиска пользователей на основе различных критериев. Для этой цели обобщенный List<> включает метод Find. Метод Find принимает ссылку на другой метод, который вызывается для сравнения каждого элемента в процессе итерации по списку. Если функция сравнения возвращает для элемента true, такой элемент включается в результаты.

В коде метода GetUserByKey() передается делегат (представляющий собой ссылку на функцию), который сравнивает внутренний ключ SimpleUser с переданным ему ключом. Если сравнение дает true, текущий пользователь, переданный как параметр из List<>, возвращается в результате; в противном случае продолжается итерация по элементам List<>. В данном случае мы используем лямбда-выражения C# для передачи делегата.

UserStore содержит реализацию для сохранения только информации пользователя. Роли не включаются. Для этой цели понадобится реализовать класс RoleStore (подобный классу UserStore), как показано ниже:

public class RoleStore
{
        XmlSerializer _Serializer;
        private string _FileName;
        List<SimpleRole> _Roles;

        private static Dictionary<string, RoleStore> _RegisteredStores;

        public static RoleStore GetStore(string fileName)
        {
            // Создать зарегистрированные хранилища
            if (_RegisteredStores == null)
                _RegisteredStores = new Dictionary<string, RoleStore>();

            // Вернуть соответствующее хранилище
            if (!_RegisteredStores.ContainsKey(fileName))
            {
                _RegisteredStores.Add(fileName, new RoleStore(fileName));
            }

            return _RegisteredStores[fileName];
        }

        private RoleStore(string fileName)
        {
            _Roles = new List<SimpleRole>();
            _FileName = fileName;
            _Serializer = new XmlSerializer(typeof(List<SimpleRole>));

            LoadStore(_FileName);
        }

        private void LoadStore(string fileName)
        {
            try
            {
                // Хранилище создается автоматически при его сохранении
                if (System.IO.File.Exists(fileName))
                {
                    using (XmlTextReader reader = new XmlTextReader(fileName))
                    {
                        _Roles = (List<SimpleRole>)_Serializer.Deserialize(reader);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format(
                    "Не удалось загрузить файл {0}", fileName), ex);
            }
        }

        private void SaveStore(string fileName)
        {
            try
            {
                if (System.IO.File.Exists(fileName))
                    System.IO.File.Delete(fileName);

                using (XmlTextWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
                {
                    _Serializer.Serialize(writer, _Roles);
                }
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format(
                    "Не удалось сохранить файл {0}", fileName), ex);
            }
        }

        public List<SimpleRole> Roles
        {
            get { return _Roles; }
        }

        public void Save()
        {
            SaveStore(_FileName);
        }

        public List<SimpleRole> GetRolesForUser(string userName)
        {
            List<SimpleRole> Results = new List<SimpleRole>();
            foreach (SimpleRole r in Roles)
            {
                if (r.AssignedUsers.Contains(userName))
                    Results.Add(r);
            }
            return Results;
        }

        public string[] GetUsersInRole(string roleName)
        {
            SimpleRole Role = GetRole(roleName);
            if (Role != null)
            {
                string[] Results = new string[Role.AssignedUsers.Count];
                Role.AssignedUsers.CopyTo(Results, 0);
                return Results;
            }
            else
            {
                throw new Exception(string.Format("Роль с именем \"{0}\" не существует!", roleName));
            }
        }

        public SimpleRole GetRole(string roleName)
        {
            return Roles.Find(
                role => role.RoleName.Equals(
                    roleName, StringComparison.OrdinalIgnoreCase));
        }
}

Эта реализация выглядит очень похоже на UserStore. Главное отличие состоит в том, что в ней используется класс SimpleRole вместо SimpleUser, а класс XmlSerializer инициализируется другим типом. Кроме того, отличается функция запроса к хранилищу. В то время как UserStore реализует функции нахождения пользователей по адресам электронной почты, уникальным идентификаторам либо именам, класс хранилища находит роли по именам, позволяя вернуть пользовательские роли, и опрашивает все роли для одного выделенного пользователя.

Обратите внимание, что в предыдущем методе GetRole() имена ролей сравнивались с помощью метода Equals экземпляра строки с передачей ему в качестве параметра StringComparison.OrdinalIgnoreCase. Это значит, что сравнение имен ролей производилось без учета регистра. Поэтому, если название роли будет передано с буквами разного регистра, она все равно будет найдена.

Теперь классы для доступа к хранилищам готовы, а это значит, что можно приступать к реализации классов пользовательских поставщиков.

Пройди тесты
Лучший чат для C# программистов