Специальные поставщики профилей

187

Модель профилей гладко стыкуется с веб-страницами ASP.NET. Однако она не слишком гибкая в плане конфигурирования. Решение о создании собственного поставщика профилей может быть принято по нескольким причинам:

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

Пользовательские классы поставщиков профилей

Чтобы реализовать поставщик профилей, необходимо создать класс, унаследованный от абстрактного класса ProfileProvider из пространства имен System.Web.Profile. Сам абстрактный класс ProfileProvider наследуется от абстрактного класса SettingsProvider из пространства имен System.Configuration.Provider. В результате потребуется реализовать члены классов SettingsProvider и ProfileProvider.

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

В следующем примере рассматривается переопределенный метод простого поставщика профилей, который содержит основную логику, необходимую для подключения к странице, но не поддерживает большей части остальных составляющих Profiles API. Методы, которые не поддерживаются, просто генерируют исключение NotImplementedException, как показано ниже:

public override int DeleteProfiles(string[] usernames)
{
	throw new Exception("Метод или операция не реализованы");
}

Все прочие методы концептуально легко реализуемы (все, что для этого нужно - некоторый базовый код ADO.NET). Однако правильное кодирование каждого метода потребует ощутимого объема программирования.

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

Абстрактные методы для поставщика профилей
Класс Член Описание
ProviderBase Name *

Свойство только для чтения, возвращает имя (установленное в файле web.config) текущего поставщика

Initialize() *

Получает конфигурационный элемент из файла web.config, инициализирующий поставщика. Дает возможность прочесть специальные установки и сохранить информацию в переменных-членах

SettingsProvider ApplicationName

Имя (установленное в web.config), которое позволяет отделить пользователей разных приложений, сохраненных в одной и той же базе данных

GetPropertyValues() *

Извлекает информацию профиля отдельного пользователя. Этот метод вызывается автоматически, когда веб-страница обращается к свойству Page.Profile. Данный метод предоставляет список всех свойств профиля, определенных в приложении. Вы должны возвратить значение для каждого из этих свойств.

SetPropertyValues() *

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

ProfileProvider DeleteProfiles()

Удаляет из базы данных одну или более записей о профилях пользователей

DeleteInactiveProfiles()

Аналогично DeleteProfiles(), но ищет профили, к которым не было обращений с указанного момента времени. Чтобы поддержать этот метод, вы должны отслеживать время доступа или обновления профилей в базе данных

GetAllProfiles()

Возвращает информацию о группе записей профиля. Этот метод должен поддерживать разбиение на страницы, чтобы возвращалось только подмножество всех записей. Обратитесь к хранимой процедуре aspnet_Profile_GetProfiles, которую создает aspnet_regsql, для простой реализации разбиения страниц

GetAllInactiveProfiles()

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

FindProfilesByUserName()

Извлекает информацию о профиле на основе имен одного или более пользователей. Сама информация профилей не возвращается, а возвращается только некоторая стандартная информация - вроде даты последнего действия

FindInactiveProfilesByUserName()

Аналогичен FindProfilesByUserName(), но ищет профили, к которым не было обращений с определенного времени

GetNumberOfInactiveProfiles()

Подсчитывает количество профилей, к которым не было обращений с определенного времени

Проектирование FactoredProfileProvider

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

При реализации собственного поставщика профилей необходимо определить, насколько общее решение должно быть реализовано. Например, если планируется реализовать сжатие с использованием классов из пространства имен System.IO.Compression или шифрование с применением классов из пространства имен System.Security.Cryptography, то также понадобится выбрать, хотите ли вы создать решение на все случаи жизни, или же вам нужен лишь ограниченный поставщик, специально подогнанный под конкретный сценарий.

Соответственно, FactoredProfileProvider может иметь два возможных проектных решения:

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

Главная идея, лежащая в основе FactoredProfileProvider, заключается в том, что он должен решать две основные задачи (извлечение и обновление информации профиля) с помощью двух хранимых процедур. Это обеспечит мощный уровень гибкости, т.к. в любой момент хранимые процедуры можно модифицировать с целью использования других таблиц, имен столбцов, типов данных и даже режимов сериализации.

Важная особенность этого примера в том, что веб-приложение выбирает, какие хранимые процедуры использовать, за счет объявления их в файле web.config. Вот пример применения FactoredProfileProvider в приложении:

<profile defaultProvider="FactoredProfileProvider">
	<providers>
		<clear/>
		<add name="FactoredProfileProvider" type="FactoredProfileProvider" 
            connectionStringName="ProfileService" 
            updateUserProcedure="Users_Update" 
            getUserProcedure="Users_GetByUserName"/>
	</providers>
	<properties>
		...
	</properties>
</profile>

Наряду с обычными ожидаемыми атрибутами (name, type и connectionStringName), дескриптор <add> включает два новых атрибута: updateUserProcedure и getUserProcedure. Первый - updateUserProcedure - указывает имя хранимой процедуры, используемой для вставки и обновления информации профиля. Второй - getUserProcedure - задает имя хранимой процедуры, применяемой для извлечения информации профиля.

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

<properties>
	<add name="RealName"/>
	<add name="City"/>
	<add name="Street"/>
	<add name="ZipCode" type="Int32"/>
	<add name="Country"/>
</properties>

то FactorProfileProvider вызовет указанную хранимую процедуру обновления и передаст значения в параметрах с именами @RealName, @City и т.д. Запрашивая информацию профиля, FactorProfileProvider будет искать соответствующие имена полей.

Это подобно проектному решению для элементов управления SqlDataSource и ObjectDataSource. Хотя при разработке хранимых процедур необходимо следовать определенным соглашениям, больше никаких ограничений на остальную часть базы данных не накладывается. Например, хранимая процедура обновления может вставлять информацию в любую последовательность полей любой таблицы, а хранимая процедура, используемая для опроса информации профиля, может применять псевдонимы или объединения для конструирования ожидаемой таблицы.

Кодирование FactoredProfileProvider

Первый шаг в создании FactoredProfileProvider - наследование этого класса от ProfileProvider. Все методы, которые не реализованы в этом примере, просто содержат единственную строку кода, которая генерирует исключение:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.Profile;
using System.Data.SqlClient;
using System.Collections.Specialized;

public class FactoredProfileProvider : ProfileProvider
{
    public override int DeleteProfiles(string[] usernames)
    {
        throw new Exception("Метод или операция не реализованы");
    }

    public override int DeleteProfiles(ProfileInfoCollection profiles)
    {
        throw new Exception("Метод или операция не реализованы");
    }

    public override int DeleteInactiveProfiles(ProfileAuthenticationOption authenticationOption, 
    	DateTime userInactiveSinceDate)
    {
        throw new Exception("Метод или операция не реализованы");
    }

    public override ProfileInfoCollection FindInactiveProfilesByUserName(
        ProfileAuthenticationOption authenticationOption, string usernameToMatch, 
        DateTime userInactiveSinceDate, 
        int pageIndex, int pageSize, out int totalRecords)
    {
        throw new Exception("Метод или операция не реализованы");
    }

    public override ProfileInfoCollection FindProfilesByUserName(
    	ProfileAuthenticationOption authenticationOption, string usernameToMatch, 
        int pageIndex, int pageSize, out int totalRecords)
    {
        throw new Exception("Метод или операция не реализованы");
    }

    public override ProfileInfoCollection GetAllInactiveProfiles(
    	ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate, 
        int pageIndex, int pageSize, out int totalRecords)
    {
        throw new Exception("Метод или операция не реализованы");
    }

    public override ProfileInfoCollection GetAllProfiles(
    	ProfileAuthenticationOption authenticationOption, int pageIndex, 
        int pageSize, out int totalRecords)
    {
        throw new Exception("Метод или операция не реализованы");
    }

    public override int GetNumberOfInactiveProfiles(
    	ProfileAuthenticationOption authenticationOption, DateTime userInactiveSinceDate)
    {
        throw new Exception("Метод или операция не реализованы");
    }

    public override string ApplicationName
    {
        get
        {
            throw new Exception("Метод или операция не реализованы");
        }
        set
        {
            throw new Exception("Метод или операция не реализованы");
        }
    }
    
    // ...
}

Быстрый способ заполнить все методы логикой генерации исключений предусматривает щелчок правой кнопкой мыши на ProfileProvider в объявлении класса и выбор в контекстном меню пункта Refactor --> Implement Abstract Class (Рефакторинг --> Реализовать абстрактный класс).

Инициализация

Класс FactoredProfileProvider должен отслеживать несколько базовых деталей - имя поставщика, строку соединения и две хранимых процедуры. Эти детали обеспечиваются следующими доступными только для чтения свойствами:

public class FactoredProfileProvider : ProfileProvider
{
	// ...

    private string name;
    public override string Name
    {
        get { return name; }
    }

    private string connectionString;
    public string ConnectionString
    {
        get { return connectionString; }
    }

    private string updateProcedure;
    public string UpdateUserProcedure
    {
        get { return updateProcedure; }
    }

    private string getProcedure;
    public string GetUserProcedure
    {
        get { return getProcedure; }
    }
    
    // ...
}

Чтобы установить эти детали, понадобится переопределить метод Initialize(). В этой точке вы получаете коллекцию, содержащую все атрибуты элемента <add>, которые зарегистрировал данный поставщик. Если любые из необходимых деталей отсутствуют, должно быть сгенерировано исключение. Следующий код демонстрирует это:

public class FactoredProfileProvider : ProfileProvider
{
    // ...

    public override void Initialize(string name, NameValueCollection config)
    {
        this.name = name;

        // Инициализировать значения из web.config
        ConnectionStringSettings connectionStringSettings = ConfigurationManager.
            ConnectionStrings[config["connectionStringName"]];
        if (connectionStringSettings == null ||
            connectionStringSettings.ConnectionString.Trim() == "")
        {
            throw new HttpException("Должна быть указана строка соединения");
        }
        else
        {
            connectionString = connectionStringSettings.ConnectionString;
        }

        updateProcedure = config["updateUserProcedure"];
        if (updateProcedure.Trim() == "")
        {
            throw new HttpException("Должна быть указана хранимая процедура для обновлений");
        }

        getProcedure = config["getUserProcedure"];
        if (getProcedure.Trim() == "")
        {
            throw new HttpException(
                "Должна быть указана хранимая процедура для извлечения записей пользователя");
        }
    }
    
    // ...
}

Чтение информации профиля

Когда веб-страница обращается к любой информации профиля, среда ASP.NET вызывает метод GetPropertyValues(). Он принимает два параметра: SettingContext, который включает в себя такую информацию, как имя текущего пользователя, и объект SettingPropertyCollection, содержащий коллекцию всех свойств профиля, определенных приложением (и доступность которых ожидается). Возвращаться должен экземпляр SettingsPropertyValueCollection с соответствующими значениями.

Ниже представлена реализация этого метода с подробными комментариями:

public class FactoredProfileProvider : ProfileProvider
{
    // ...

    public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext context, SettingsPropertyCollection properties)
    {
        // Прежде чем что-либо делать, необходимо создать новый экземпляр 
        // SettingsPropertyValueCollection. В этой коллекции будут храниться извлеченные значения
        SettingsPropertyValueCollection values = new SettingsPropertyValueCollection();

        // Нужно создать объекты ADO.NET, необходимые для выполнения хранимой процедуры
        SqlConnection con = new SqlConnection(connectionString);
        SqlCommand cmd = new SqlCommand(getProcedure, con);
        cmd.CommandType = CommandType.StoredProcedure;

        // Этот код извлекает имя текущего пользователя из словаря SettingsContext
        // и передает его в качестве параметра в хранимую процедуру
        cmd.Parameters.Add(new SqlParameter("@UserName", (string)context["UserName"]));

        try
        {
            // Выполнить команду
            con.Open();
            SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow);
            reader.Read();

            // Проходим по коллекции свойств и заполняем коллекцию values
            foreach (SettingsProperty property in properties)
            {
                SettingsPropertyValue value = new SettingsPropertyValue(property);

                if (reader.HasRows)
                {
                    value.PropertyValue = reader[property.Name];
                }
                values.Add(value);
            }
            reader.Close();
        }
        finally
        {
            con.Close();
        }

        return values;
    }

    // ...
}

Обновление информации профиля

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

public class FactoredProfileProvider : ProfileProvider
{
    // ...

    public override void SetPropertyValues(SettingsContext context, SettingsPropertyValueCollection values)
    {
        // Подготовить команду
        SqlConnection con = new SqlConnection(connectionString);
        SqlCommand cmd = new SqlCommand(updateProcedure, con);
        cmd.CommandType = CommandType.StoredProcedure;

        // Добавить параметры.
        // Предполагается, что каждое свойство отображается непосредственно
        // на одно имя параметра хранимой процедуры
        foreach (SettingsPropertyValue value in values)
        {
            cmd.Parameters.Add(new SqlParameter(value.Name, value.PropertyValue));
        }

        // Поставщик предполагает, что процедура принимает 
        // параметр по имени @UserName
        cmd.Parameters.Add(new SqlParameter("@UserName", (string)context["UserName"]));


        // Выполнить команду
        try
        {
            con.Open();
            cmd.ExecuteNonQuery();
        }
        finally
        {
            con.Close();
        }
    }
}

На этом код, необходимый для простой реализации FactoredProfileProvider, завершен. Если нужно имитировать поведение SqlProfileProvider, понадобится также при каждом вызове метода SetPropertyValues() обновлять базу данных временем последней активности.

Тестирование FactoredProfileProvider

Для того чтобы протестировать этот пример, как минимум, нужно создать базу данных с таблицей CustomUsers и двумя хранимыми процедурами. Ниже показан пример с таблицей CustomUsers, в которой хранится информация об адресах:

CREATE TABLE CustomUsers
(
	[Id] INT IDENTITY(1,1) PRIMARY KEY,
	[UserName] NVARCHAR(128) NOT NUll,
	[RealName] NVARCHAR(128) NOT NULL,
	[City] NVARCHAR(64) NOT NULL,
	[Street] NVARCHAR(128) NOT NULL,
	[ZipCode] INT NOT NULL,
	[Country] NVARCHAR(64) NOT NULL
)
Пользовательская таблица CustomUsers

Простая процедура по имени Users_GetByUserName запрашивает информацию профиля из таблицы:

CREATE PROCEDURE Users_GetByUserName 
	@UserName varchar(50)
AS
	SELECT * FROM CustomUsers WHERE UserName = @UserName

Хранимая процедура Users_Update несколько более интересна. Она начинается с проверки существования указанного пользователя. Если он не существует, создается запись с информацией профиля. Если же пользователь существует, эта запись обновляется. Такое проектное решение перекликается с поведением SqlProfileProvider.

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

Ниже приведен полный код хранимой процедуры Users_Update:

CREATE PROCEDURE Users_Update (
	@UserName varchar(128), 
	@RealName varchar(128), 
	@City varchar(64),
	@Street varchar(128),
	@ZipCode int,
	@Country varchar(64)
) AS
DECLARE @Match int
SELECT @Match = COUNT(*) FROM CustomUsers
WHERE UserName = @UserName 

IF (@Match = 0) 
	INSERT INTO CustomUsers (UserName, RealName, City, Street, ZipCode, Country)
	VALUES (@UserName, @RealName, @City, @Street, @ZipCode, @Country)
IF (@Match = 1) 
	UPDATE CustomUsers SET
		UserName = @UserName,
		RealName = @RealName,
		City = @City,
		Street = @Street,
		ZipCode = @ZipCode,
		Country = @Country
	WHERE
		(UserName = @UserName)

Для использования этой таблицы понадобится просто сконфигурировать FactoredProfileProvider, идентифицировать используемую хранимую процедуру и определить все поля таблицы Users, к которым необходим доступ. Рассмотрим все подробности конфигурации в web.config:

<profile defaultProvider="FactoredProfileProvider">
      <providers>
        <clear />
        <add name="FactoredProfileProvider" type="FactoredProfileProvider"
             connectionStringName="MyMembershipConnString"
             updateUserProcedure="Users_Update"
             getUserProcedure="Users_GetByUserName"/>
      </providers>
      <properties>
        <add name="RealName"/>
        <add name="City"/>
        <add name="Street"/>
        <add name="ZipCode" type="Int32"/>
        <add name="Country"/>
      </properties>
</profile>

С этого момента можно обращаться к деталям профиля точно так же, как это делалось бы с поставщиком SqlProfileProvider. Например, ниже показан код, необходимый для копирования информации из последовательности текстовых полей в запись профиля:

protected void cmdSave_Click(object sender, EventArgs e)
{
        Profile.RealName = txtName.Text;
        Profile.City = txtCity.Text;
        Profile.Street = txtStreet.Text;
        Profile.ZipCode = Int32.Parse(txtZip.Text);
        Profile.Country = txtCountry.Text;
}

А вот код, который читает текущие значения в текстовые поля и применяет их к профилю, завершая тестовую страницу:

protected void cmdGet_Click(object sender, EventArgs e)
{
        txtName.Text = Profile.RealName;
        txtCity.Text = Profile.City;
        txtStreet.Text = Profile.Street;
        txtZip.Text = Profile.ZipCode.ToString();
        txtCountry.Text = Profile.Country;
}

Внешний вид тестовой страницы показан на рисунке ниже:

Тестирование специального поставщика профилей
Пройди тесты
Лучший чат для C# программистов