Практически каждому приложению ASP.NET необходимо отслеживать данные сеанса пользователя. ASP.NET предоставляет класс HttpSessionState для хранения значений состояния сеанса. Экземпляр класса HttpSessionState для каждого HTTP-запроса доступен во всем приложении с помощью статического свойства HttpContext.Current.Session. Доступ к одному и тому же экземпляру упрощается на каждой странице и пользовательском элементе управления с помощью свойства Session страницы или пользовательского элемента управления.
Класс HttpSessionState предоставляет коллекцию пар ключ-значение, где ключи имеют тип String, а значения — тип Object. Это означает, что сеанс чрезвычайно гибок, и вы можете хранить в сеансе практически любой тип данных.
Но (всегда есть «но») за эту гибкость приходится платить. Цена — это легкость, с которой ошибки могут быть внесены в ваше приложение. Многие из ошибок, которые могут быть обнаружены, не будут обнаружены при модульном тестировании и, вероятно, не при помощи какой-либо формы структурированного тестирования. Эти ошибки часто проявляются только тогда, когда приложение развернуто в производственной среде. Когда они появляются, зачастую очень сложно, если не невозможно, определить, как они возникли, и воспроизвести ошибку. Это означает, что их ремонт обходится очень дорого.
В этой статье представлена стратегия, помогающая предотвратить ошибки такого типа. Он использует шаблон проектирования, называемый фасадом, в котором он объединяет очень свободный интерфейс, предоставляемый классом HttpSessionState (который может удовлетворить требования любого приложения), с хорошо разработанным и контролируемым интерфейсом, специально созданным для конкретного приложения. Если вы не знакомы с шаблонами проектирования или шаблоном фасада, быстрый поиск в Интернете «шаблона проектирования фасада» предоставит вам много информации. Однако для понимания этой статьи не обязательно разбираться в шаблонах проектирования.
Пример кода, показанный в этой статье, написан на C#, но концепции применимы к любому языку .NET.
В чем проблема?
В этом разделе статьи я опишу проблемы с прямым доступом к классу HttpSessionState, без фасада. Я опишу виды ошибок, которые могут быть введены.
Ниже показан типичный код, написанный для доступа к переменным состояния сеанса.
// Сохраняем переменную сеанса
Session["некоторая строка"] = AnyOldObject;
// Читаем переменную сеанса
DateTime startDate = (DateTime)Session["StartDate"];
Проблемы возникают из-за гибкого интерфейса, предоставляемого HttpSessionState: ключи представляют собой просто строки, а значения не являются строго типизированными.
Использование строковых литералов в качестве ключей
Если в качестве ключей используются строковые литералы, строковое значение ключа не проверяется компилятором. Создать новые значения сеанса легко, просто ошибившись при вводе.
Сессия["получено"] = 27;...
Сеанс["получено"] = 32;
В приведенном выше коде сохранены два отдельных значения сеанса.
Большинство подобных ошибок выявляются при модульном тестировании, но не всегда. Не всегда может быть очевидно, что значение не изменилось, как ожидалось.
Мы можем избежать ошибок такого рода, используя константы:
Private const string получены = "получены";...
Сеанс[получено] = 27;...
Сеанс [получено] = 32;
Без проверки типа
Проверка типов значений, хранящихся в переменных сеанса, не осуществляется. Компилятор не может проверить правильность того, что сохраняется.
Рассмотрим следующий код:
Session["maxValue"] = 27;...
int maxValue = (int)Session["maxValue"];
В другом месте для обновления значения используется следующий код.
Сеанс["maxValue"] = 56,7;
Если код для чтения переменной сеанса «maxValue» в переменную int maxValue выполняется снова, будет выдано исключение InvalidCastException.
Большинство подобных ошибок выявляются при модульном тестировании, но не всегда.
Непреднамеренное повторное использование ключа
Даже когда мы определяем константы для сеансовых ключей на каждой странице, можно непреднамеренно использовать один и тот же ключ на разных страницах. Рассмотрим следующий пример:
Код на одной странице:
Private const string edit = "edit";...
Сессия[править] = правда;
Код на второй странице, отображаемый после первой:
Private const string edit = "edit";...
if ((bool)Сессия[edit])
{
...
}
Код на третьей, несвязанной странице:
Private const string edit = "edit";...
Сеанс[править] = ложь;
Если третья страница по какой-то причине отображается до отображения второй страницы, значение может отличаться от ожидаемого. Код, вероятно, все равно запустится, но результаты будут неправильными.
Обычно эта ошибка НЕ выявляется при тестировании. Ошибка проявляется только тогда, когда пользователь выполняет определенную комбинацию навигации по странице (или открывает новое окно браузера).
В худшем случае никто не узнает о проявлении ошибки, и мы можем просто изменить данные до непредусмотренного значения.
Непреднамеренное повторное использование ключа – снова
В приведенном выше примере тот же тип данных был сохранен в переменной сеанса. Поскольку проверка типов сохраняемых данных не осуществляется, также может возникнуть проблема несовместимости типов данных.
Код на одной странице:
Session["FollowUp"] = "true";
Код на второй странице:
Session["FollowUp"] = 1;
Код на третьей странице:
Session["FollowUp"] = true;
Когда ошибка проявится, будет выброшено исключение InvalidCastException.
Обычно эта ошибка НЕ выявляется при тестировании. Ошибка проявляется только тогда, когда пользователь выполняет определенную комбинацию навигации по странице (или открывает новое окно браузера).
Что мы можем сделать?
Первое быстрое решение
Первое и самое простое, что мы можем сделать, это убедиться, что мы никогда не используем строковые литералы для ключей сеанса. Всегда используйте константы, чтобы избежать простых ошибок при вводе.
частная константная строка limit = "limit";...
Сеанс[ограничение] = 27;...
Сеанс[ограничение] = 32;
Однако, когда константы определяются локально (например, на уровне страницы), мы все равно можем непреднамеренно повторно использовать один и тот же ключ.
Лучшее быстрое решение
Вместо того чтобы определять константы на каждой странице, сгруппируйте все константы ключей сеанса в одном месте и предоставьте документацию, которая появится в Intellisense. В документации должно быть четко указано, для чего используется переменная сеанса. Например, определите класс только для сеансовых ключей:
общедоступный статический класс SessionKeys.
{
/// <сводка>
/// Максимум...
/// </сводка>
общественная константная строка Limit = «предел»;
}
...
Session[SessionKeys.Limit] = 27;
Если вам нужна новая переменная сеанса, если вы выберете имя, которое уже использовалось, вы узнаете об этом, когда добавите константу в класс SessionKeys. Вы можете увидеть, как он используется в данный момент, и определить, следует ли вам использовать другой ключ.
Однако мы по-прежнему не обеспечиваем согласованность типов данных.
Гораздо лучший способ — использование фасада
Доступ к HttpSessionState осуществляется только из одного статического класса вашего приложения — фасада. Не должно быть прямого доступа к свойству Session из кода на страницах или элементах управления, а также прямого доступа к HttpContext.Current.Session, кроме как из фасада.
Все переменные сеанса будут отображаться как свойства класса фасада.
Это имеет те же преимущества, что и использование одного класса для всех сеансовых ключей, а также следующие преимущества:
Строгая типизация того, что помещается в переменные сеанса.
Нет необходимости приводить код, в котором используются переменные сеанса.
Все преимущества установщиков свойств для проверки того, что помещается в переменные сеанса (больше, чем просто тип).
Все преимущества методов получения свойств при доступе к переменным сеанса. Например, инициализация переменной при первом доступе к ней.
Пример класса фасада сеанса
Вот пример класса для реализации фасада сеанса для приложения MyApplication.
Крах
/// <сводка>
/// MyApplicationSession предоставляет фасад объекту сеанса ASP.NET.
/// Весь доступ к переменным сеанса должен осуществляться через этот класс.
/// </сводка>
общедоступный статический класс MyApplicationSession
{
# региона Частные константы
//------------------------------------------------ ---------------------
частная константная строка userAuthorisation = «UserAuthorisation»;
частная константная строка teamManagementState = "TeamManagementState";
частная константная строка startDate = «StartDate»;
частная константная строка endDate = "EndDate";
//------------------------------------------------ ---------------------
# конечный регион
# регион Публичные свойства
//------------------------------------------------ ---------------------
/// <сводка>
/// Имя пользователя — это имя домена и имя пользователя текущего пользователя.
/// </сводка>
общедоступная статическая строка Имя пользователя
{
get { return HttpContext.Current.User.Identity.Name; }
}
/// <сводка>
/// UserAuthorisation содержит информацию об авторизации для
/// текущий пользователь.
/// </сводка>
public static UserAuthorisation UserAuthorisation
{
получать
{
Авторизация пользователя
= (UserAuthorisation)HttpContext.Current.Session[userAuthorisation];
// Проверяем, истек ли срок действия авторизации пользователя
если (
userAuth == ноль ||
(userAuth.Created.AddMinutes(
MyApplication.Settings.Caching.AuthorisationCache.CacheExpiryMinutes))
<ДатаВремя.Сейчас
)
{
userAuth = UserAuthorisation.GetUserAuthorisation (имя пользователя);
UserAuthorisation = userAuth;
}
Вернуть userAuth;
}
частный набор
{
HttpContext.Current.Session[userAuthorisation] = значение;
}
}
/// <сводка>
/// TeamManagementState используется для хранения текущего состояния
/// Страница TeamManagement.aspx.
/// </сводка>
общедоступный статический TeamManagementState TeamManagementState
{
получать
{
return (TeamManagementState)HttpContext.Current.Session[teamManagementState];
}
набор
{
HttpContext.Current.Session[teamManagementState] = значение;
}
}
/// <сводка>
/// StartDate — самая ранняя дата, используемая для фильтрации записей.
/// </сводка>
публичный статический DateTime StartDate
{
получать
{
если (HttpContext.Current.Session[startDate] == null)
вернуть DateTime.MinValue;
еще
return (DateTime)HttpContext.Current.Session[startDate];
}
набор
{
HttpContext.Current.Session[startDate] = значение;
}
}
/// <сводка>
/// EndDate — самая поздняя дата, используемая для фильтрации записей.
/// </сводка>
публичный статический DateTime EndDate
{
получать
{
если (HttpContext.Current.Session[endDate] == null)
вернуть DateTime.MaxValue;
еще
return (DateTime)HttpContext.Current.Session[endDate];
}
набор
{
HttpContext.Current.Session[endDate] = значение;
}
}
//------------------------------------------------ ---------------------
# конечный регион
}
Класс демонстрирует использование методов получения свойств, которые могут предоставлять значения по умолчанию, если значение не было сохранено явно. Например, свойство StartDate предоставляет DateTime.MinValue по умолчанию.
Метод получения свойства UserAuthorisation обеспечивает простой кэш экземпляра класса UserAuthorisation, гарантируя актуальность экземпляра в переменных сеанса. Это свойство также показывает использование частного установщика, так что значение переменной сеанса может быть установлено только под контролем класса фасада.
Свойство Username демонстрирует значение, которое когда-то могло храниться как переменная сеанса, но больше не хранится таким образом.
Следующий код показывает, как можно получить доступ к переменной сеанса через фасад. Обратите внимание, что в этом коде нет необходимости выполнять какое-либо приведение.
// Сохраняем переменную сеанса
MyApplicationSession.StartDate = DateTime.Today.AddDays(-1);
// Читаем переменную сеанса
ДатаВремя startDate = MyApplicationSession.StartDate;
Дополнительные преимущества
Дополнительным преимуществом шаблона проектирования фасада является то, что он скрывает внутреннюю реализацию от остальной части приложения. Возможно, в будущем вы решите использовать другой механизм реализации состояния сеанса, отличный от встроенного класса ASP.NET HttpSessionState. Вам нужно изменить только внутренности фасада — в остальной части приложения больше ничего менять не нужно.
Краткое содержание
Использование фасада для HttpSessionState обеспечивает гораздо более надежный способ доступа к переменным сеанса. Это очень простой в реализации метод, но с огромной пользой.