Управление состоянием в ASP.NET
ОГЛАВЛЕНИЕ
С момента появления Web одной из главных проблем разработчиков стала задача сохранения данных веб-приложения между запросами пользователей, поскольку протокол HTTP по своей природе является протоколом без состояния (stateless). Существуют различные подходы к решению этой задачи. В данной статье рассмотрены механизмы, реализованные в ASP.NET.
В ASP.NET существует несколько способов сохранить информацию между запросами, и критерием выбора, когда и какой из них использовать, являются ответы на следующие четыре вопроса:
- Какому количеству пользователей должна быть доступна информация?
- Как долго информация должна храниться?
- Какие объемы информации необходимо сохранять?
- Какие требования к секретности информации?
Отвечая на эти вопросы, можно определить какой из следующих подходов использовать.
Все методы можно разделить на две категории: клиентские, т.е. информация будет храниться на стороне клиента и серверные, т.е. информация будет храниться на стороне сервера. Достоинством клиентских методов является отсутствие необходимости использовать серверные ресурсы для хранения информации, а недостатком - требования секретности, т.к. любая отосланная на клиентскую машину информация может быть искажена потенциальным злоумышленником. Для серверных способов ситуация диаметрально противоположная. Рассмотрим каждый из них более детально.
Клиентские методы
ViewState
Контейнер ViewState представляет собой коллекцию типа ключ-значение, которая позволяет сохранять информацию, относящуюся к определенной странице. Перед отправкой клиенту эта коллекция сериализуются и записываются в hidden поле под названием __VIEWSTATE. В этом можно убедиться, посмотрев сгенерированный html код любой ASP.NET страницы. В результате вы увидите подобный код:
<form name="Form1" method="post" action="WebForm1.aspx" id="Form1">
...
<input type="hidden" name="__VIEWSTATE" value="dDwtMTI3OTMzNDM4NDs7PqSz2R6MBTQBd6YfrNqpkCkIceLd" />
...
</form>
После того, как пользователь выполнит какие-то действия на странице и инициирует постбек, сервер получит очередной запрос. Данное hidden поле распарситься и выполниться десериализация его значения. Этот процесс происходит сразу после обработки события OnInit страницы. В случае, если при десериализации возникает ошибка, тем самым указывая, что либо информация в поле была повреждена в процессе передачи, либо намеренно изменена на клиенте, система сгенерирует исключение
System.Web.HttpException: The viewstate is invalid for this page and might be corrupted
Убедиться в этом можно написав в браузере http://localhost/WebForm1.aspx?__VIEWSTATE=abc
Данный контейнер позволяет пользователю сохранять значения следующим образом:
// запись
ViewState["color"] = "red";
// чтение
string strColor =(string)ViewState["color"];
Этот способ применим на веб-страницах и пользовательских контролах (наследниках классов Page и UserControl соответственно). В коллекцию ViewState можно занести любой сериализуемый объект, либо имеющий TypeConverter для преобразования его в строку. Контейнер оптимизирован для хранения примитивных типов, а также String, ArrayList, HashTable. В случае сохранения других типов рекомендуется перегрузить методы LoadViewState и SaveViewState, в которых реализовать свою более эффективную методику.
Кроме того, эти методы необходимо перегружать в custom контролах (наследниках класса WebControl). Предположим, что custom контрол сохраняет свое состояние во ViewState под ключом X. Этот контрол размещается на некоторой странице, которая также сохраняет свое состояние во ViewState под таким же ключом X. В результате одно из значений будет потеряно, что, вероятно, приведет к неправильной работе. Поэтому в custom контролах настоятельно рекомендуются пользоваться следующим способом:
// запись
protected override object SaveViewState()
{
// сохранение состояния как массива объектов
object[] allStates = new object[3];
// сохранение состояния родителя
allStates[0] = base.SaveViewState();
// сохранение значения контрола
allStates[1] = "Control Value 1";
allStates[2] = "Control Value 2";
return allStates;
}
// чтение
protected override void LoadViewState(object savedState)
{
if (savedState != null)
{
// восстановление состояния из массива, сформированного в методе SavedViewState.
object[] myState = (object[])savedState;
// восстановление состояния родителя
if (myState[0] != null)
base.LoadViewState(myState[0]);
// восстановления значений, сохраненных контролом
String ControlValue1, ControlValue2;
if (myState[1] != null)
ControlValue1 = (string)myState[1];
if (myState[2] != null)
ControlValue2 = (string)myState[2];
}
}
На использование ViewState также влияют два атрибута директивы @Page, которая определяет любую aspx страницу. Первый из них EnableViewStateMac, установка которого в true сообщает системе о необходимости шифрования hidden поля _VIEWSTATE. Второй - EnableViewState, позволяющий отключить использование коллекции. Т.е. при EnableViewState=false выполнение вышеуказанного кода даст strColor=null. Однако, посмотрев html код страницы, для которой отключен ViewState, мы все равно увидим hidden поле __VIEWSTATE. Это связано с тем, что ViewState также хранит некоторую системную информацию, например id активной формы страницы.
Использование атрибута EnableViewState=false может повысить скорость обработки страницы, однако также может вызвать проблемы, в случае если используются контролы, сохраняющие свои внутренние данные с помощью ViewState (например, номер столбца, по которому сортируется DataGrid). Для преодоления этой проблемы в ASP.NET 2.0 был введен контейнер ControlState. Этот контейнер записывается в hidden поле вне зависимости от значения атрибута EnableViewState. Для доступа к ControlState необходимо использовать методы LoadControlState и SaveControlState.
Поскольку все данные будут храниться в одном hidden поле, то не рекомендуется использовать данный контейнер для больших объектов, поскольку это может существенно увеличить размер страницы и скорость ее загрузки. В определенной степени размер ViewState можно уменьшить за счет упаковки поля.
Cookie
Контейнер Cookie представляет собой объектно-ориентированный подход для работы с cookie. На самом деле существует две коллекции Cookie. Первая предоставляется свойством Request веб-страницы и являет собой коллекцию cookies, которые были считаны с клиента. Вторая предоставляется свойством Response и является коллекцией, которая будет записана клиенту.
Следующий пример демонстрирует их использование:
// запись даты последнего посещения сайта
HttpCookie myCookie = new HttpCookie("LastVisit");
DateTime time = DateTime.Now;
myCookie.Value = time.ToString();
myCookie.Expires = time.AddMonths(6);
myCookie.Secure = false;
Response.Cookies.Add(myCookie);
// вывод всех cookies клиента
foreach(string cookieKey in Request.Cookies)
{
HttpCookie cookie = Request.Cookies[cookieKey];
Response.Write(string.Format("Cookie: {0}
Expires: {1}
Secure: {2}
Value: {3}
", cookie.Name, cookie.Expires.ToShortDateString(), cookie.Secure, cookie.Value));
}
// удаление cookie
Response.Cookies["LastVisit"].Value = null;
Response.Cookies["LastVisit"].Expires = DateTime.Now.AddMonths(-1);
При использование этого контейнера следует учитывать такие факты: во-первых клиентский браузер может быть настроен на игнорирование cookies (проверяется с помощью свойства Request.Browser.Cookies); во-вторых большинство современных браузеров имеют ограничение в 4096 байт на размер cookie (хотя некоторые новые браузеры повышают это ограничение до 8192 байт). Неоспоримым достоинством этого подхода является возможность явно установить срок жизни объекта (по умолчанию он равен сроку жизни сессии, т.е. 20 минут).
Кроме вышеуказанных способов, можно также использовать обычные hidden-поля на форме, либо добавлять дополнительные параметры в строке запроса. Однако применение этих методов желательно минимизировать по причинам безопасности. hidden-поля можно использовать для обмена несекретной информацией между кодом, выполняемым на сервере, и скриптом, выполняемым на клиенте. Параметры в строке запроса можно применять для передачи информации между различными страницами. Также неоспоримым достоинством является то, что пользователь может внести страницу вместе с параметрами в список Favorites своего браузера.
Серверные методы
Application
Контейнер Application является глобальным и доступным из всех страниц, http модулей и http хэндлеров веб-приложения. Он должен использоваться для сохранения информации общей для всего приложения. Как и все вышеуказанные контейнеры, Application является коллекцией типа ключ-значение. Поскольку этот контейнер был доступен еще в предыдущих версиях ASP, то для совместимости Application разделен на две коллекции Contents и StaticObjects. Коллекция Contents является динамической, т.е. добавлять в нее и удалять из нее значения можно в процессе работы приложения (runtime). Доступ к ней производится либо через индексатор Application, либо через свойство Contents. Следующий пример демонстрирует ее использование:
// два эквивалентных способа доступа
// через индексатор Application
Application["Message"] = "My string";
Application["AppStartTime"] = DateTime.Now;
string s = (string)Application["Message"];
DateTime startTime = (DateTime)Application["AppStartTime"];
// через свойство Contents
Application.Contents["Message"] = "My string";
Application.Contents["AppStartTime"] = DateTime.Now;
string s = (string)Application.Contents["Message"];
DateTime startTime = (DateTime)Application.Contents["AppStartTime"];
Коллекция StaticObjects является read-only коллекцией, т.е. добавление и удаление элементов в нее в runtime запрещено. Элементы в этой коллекции определяются в файле Global.asax с помощью тега <object runat="server" scope="application">. Например:
<object runat="server" scope="application" id="AppStr" class="System.Text.StringBuilder"/>
Таким образом, в коллекцию добавлен новый объект типа StringBuilder с ключом AppStr.
Использовать его можно либо непосредственно из aspx файла:
<form id="Form1" method="post" runat="server">
AppStr value: <%= AppStr %>
</form>
либо из кода на C# или на другом языке программирования:
StringBuilder sb = (StringBuilder)Application.StaticObjects["AppStr"];
sb.Append("My String");
Label1.Text = Application.StaticObjects["AppStr"].ToString();
Отметим еще раз, что этот способ введен исключительно для совместимости с предыдущими версиями ASP и использовать его в данный момент не рекомендуется.
Поскольку значения контейнера Application доступны из всех страниц, то в случае одновременного обращения нескольких потоков к одному значению возникает проблема синхронизации. В качестве решения используются два метода: Lock() и Unlock(). Вызов метода Lock заставляет ASP.NET блокировать все попытки доступа любых других потоков к любой информации из Application. Блокировка снимается вызовом метода Unlock из потока инициировавшего блокировку. Следующий пример демонстрирует их использование:
Application.Lock();
Application["SomeGlobalCounter"] = (int)Application["SomeGlobalCounter"] + 1;
Application.UnLock();
Во избежание ситуации deadlock блокировка автоматически снимается в таких случаях: после успешного выполнения запроса, после ошибки тайм-аута соединения либо после любого неперехваченного исключения, возникшего в процессе обработки запроса пользователя.
Одним из недостатков Application является неограниченное время жизни его объектов. Т.е. значения, записанные в эту коллекцию, будут в ней существовать до тех пор, пока они не будут явно удалены (методы Remove, RemoveAll, RemoveAt, присвоение null).
/ удаление SomeGlobalCounter из Application методом Remove
Application.Remove("SomeGlobalCounter");
// удаление SomeGlobalCounter из Application присвоением null
Application["SomeGlobalCounter"] = null;
В противном случае они будут существовать до завершения работы приложения либо перезагрузки веб-сервера. С другой стороны, при перезапуске веб-сервера уничтожаться все значения Application, что может усложнить жизнь администратора сервера.
Также Application неприменим при развертывании приложения на нескольких физических серверах (web farm). В этом случае у каждого отдельного сервера будет своя собственная коллекция, что в целом приведет к рассинхронизции.
Session
Контейнер Session похож на Application, с той лишь разницей, что для каждого пользователя приложения создается своя собственная сессия со своими собственными значениями. Для идентификации пользователей ASP.NET использует 120-битный ключ, именуемый SessionID и состоящий только из ASCII-символов, которые допустимы для использования в URL. В зависимости от настроек веб-приложения, этот ключ сохраняется либо в Cookie либо включается как часть URL.
Если приложение настроено на использование URL для хранения SessionID, то написав в браузере, например, http://localhost/StateManagement/SessionStateTest.aspx, после загрузки страницы мы увидим вместо него нечто такое http://localhost/StateManagement/(evzobo45mp1rat55b3qm5o55)/SessionStateTest.aspx, где evzobo45mp1rat55b3qm5o55 - тот самый SessionID
При поступлении запроса от пользователя, у которого нет соответствующего Cookie или он не имеет SessionID в URL запроса, инициируется создание новой сессии. При этом происходит генерация нового уникального SessionID. После чего возникает событие Session_Start веб-приложения.
Сессия существует до тех пор, пока пользователь работает с веб-приложением и еще немного после. Для этого в настройках аппликации выставляется таймаут сессии, который по умолчанию составляет 20 минут. Т.е. если пользователь в течение 20 минут не совершил ни одного запроса к приложению, то сессия этого пользователя уничтожается. При этом возникает событие Session_End приложения.
Также как Application, для совместимости с предыдущими версиями ASP, контейнер включает в себя две коллекции Contents и StaticObjects. Для доступа к Contents можно либо использовать одноименное свойство, либо индексатор Session. Например:
// два эквивалентных способа доступа
// через индексатор Session
Session["Message"] = "My string";
Session["SessionStartTime"] = DateTime.Now;
string s = (string)Session["Message"];
DateTime startTime = (DateTime)Session["SessionStartTime"];
// через свойство Contents
Session.Contents["Message"] = "My string";
Session.Contents["SessionStartTime"] = DateTime.Now;
string s = (string)Session.Contents["Message"];
DateTime startTime = (DateTime)Session.Contents["SessionStartTime"];
Коллекция StaticObjects является read-only коллекцией, ее элементы определяются в файле Global.asax с помощью тега <object runat="server" scope="session">. Например:
<object runat="server" scope="session" id="SessionStr" class="System.Text.StringBuilder"/>
Использовать его можно аналогично вышеуказанному примеру для Application:
%3c%21-- в aspx файле --%3e
<form id="Form1" method="post" runat="server">
SessionStr value: <%= SessionStr %>
</form>
// в коде на C#
StringBuilder sb = (StringBuilder)Session.StaticObjects["SessionStr"];
sb.Append("My String");
Label1.Text = Session.StaticObjects["SessionStr"].ToString();
Механизм сессий, в отличие от Application, предоставляет несколько способов сохранения. Какой из них использовать в приложении, определяется атрибутом mode тега <sessionState> в файле web.config. Этот атрибут может принимать пять значений:
-
Off. Сессии отключены, т.е. данные сессии никак не сохраняются.
-
InProc. Способ, унаследованный от предыдущих версий ASP. Сессия храниться в локальной памяти веб-сервера. В результате при сбое приложения или перезапуске веб-сервера все данные теряются. Это может быть вызвано, например, настройками из тега <processModel> в файле machine.config (либо web.config). Один из возможных сценариев - преувеличение объема памяти, указанного в атрибуте memoryLimit. Поскольку каждый пользователь имеют свою собственную сессию, то увеличение нагрузки на сервер может потребовать достаточно большого объема ресурсов. Также перезапуск может вызвать изменение одного из файлов Global.asax или Web.config, либо директории \Bin приложения.
-
StateServer. Сохранение сессии в отдельном процессе на отдельном сервере. Для использования этого метода необходимо убедиться, что ASP.NET State Service запущен на удаленном сервере, который планируется использовать для хранения сессии. Этот сервис инсталлируется вместе с ASP.NET и Visual Studio .NET и может быть найден по такому пути:
systemroot\Microsoft.NET\Framework\versionNumber\aspnet_state.exe
В web.config необходимо в атрибуте connectionString указать название удаленного сервера и используемый порт. В результате web.config будет содержать подобную строку:<sessionState mode="StateServer" stateConnectionString="tcpip=sessionserver:42424" cookieless="true" timeout="20"/>
Этот способ защищает от проблем, связанных со сбоями веб-сервера. Кроме того, в случае развертывания приложения на нескольких серверах (web farm), предоставляет единую синхронизированную сессию для всех веб-серверов. С другой стороны, при возможном рестарте удаленного сервера, либо процесса ASP.NET State Service сессия все равно будет потеряна. Также необходимо учитывать, что при использовании данного способа все сохраняемые объекты должны быть сериализуемы.
SqlServer
Сохранение сессии в базе данных SQL Server на отдельном сервере. Похож на предыдущий способ, но объекты сессии сохраняются в базе данных. Поэтому даже при перезапуске SQL Server они не будут потеряны. Чтобы использовать этот метод необходимо создать базу данных для хранения сессии (в ASP.NET 1.x необходимо создавать вручную, в ASP.NET 2.0 использовать утилиту aspnet_regsql.exe). Для этой цели существует два файла со скриптами InstallSqlState.sql и InstallPersistSqlState.sql. Оба скрипта создают базу данных, именуемую ASPState, состоящую из таблиц ASPStateTempApplications и ASPStateTempSessions и нескольких хранимых процедур. Разница между скриптами состоит в том, что InstallSqlState.sql создает базу данных как временную, т.е. при перезапуске SQL Server она теряется, а InstallPersistSqlState.sql создает обычную базу данных. Оба скрипта можно найти здесь:
-
systemroot\Microsoft.NET\Framework\versionNumber
В файле web.config необходимо указать connection string к используемому серверу БД. Пример настроек web.config:<sessionState
mode="SQLServer"
sqlConnectionString="Integrated Security=SSPI;data source=dataserver;"
cookieless="true"
timeout="20"
/>
Этот режим также используют для организации web farm либо отказоустойчивого кластера веб-серверов. Как и в предыдущем способе, сохраняемые объекты должны быть сериализуемы.
Custom
Используется в случае, если ни один из вышеперечисленных методов не подходит. Этот способ доступен только начиная с ASP.NET 2.0 и позволяет реализовать свой собственный метод хранения. При этом реализовывать всю функциональность необязательно, в большинстве случаев достаточно заменить одну из составляющих сеансового модуля, куда входит блок работы с данными, элемент состояния сеанса, словарь данных и идентификатор сеанса. Для замены одного из этих компонентов, необходимо создать соответствующие классы и указать их в настройках сессии в web.config. Следующий пример демонстрирует замену стандартного провайдера классом MyProviderClass из сборки MyProviderAssembly:
<sessionState mode="Custom" customProvider="MyProviderClass,MyProviderAssembly" cookieless="true" timeout="20"/>
Cache
Как и все остальные контейнеры, Cache представляет собой коллекцию типа ключ-значение. В Cache хранятся данные, доступные всем пользователям из всех точек приложения, но за счет использования ключей, специфичных определенному пользователю, его можно использовать для хранения пользовательской информации. Содержимое этого контейнера может сохраняться в течение долго времени, но, к сожалению, теряется при перезапуске веб-приложения или веб-сервера. Характерной особенностью Cache является невероятная гибкость. Он позволяет эффективно хранить любую по объему информацию и содержит механизм устаревания кэшируемой информации в зависимости от ряда условий.
Для каждого значения в Cache можно установить период устаревания информации по времени (абсолютной дате или относительному промежутку времени), либо на основе изменений файла. Также имеется возможность реализовать callback-функцию, которая вызывается при удалении значения из коллекции. Это позволяет проверить не существует ли более свежая версия данных и при необходимости занести ее в коллекцию.
Также важным аспектом является возможность установки приоритетности каждого значения. При чрезмерном увеличении кэша ASP.NET инициирует удаление из него значений, причем первыми удаляются элементы с более низким приоритетом и лишь при необходимости элементы с более высоким приоритетом.
Контейнер поддерживает как стандартный доступ к элементам с помощью индексатора, что показано на примере:
/ добавление строки в Cache
Cache["myKey"] = "My Value";
// чтение строки из Cache
string s = (string)Cache["myKey"];
так и ряд других методов. Наиболее используемый из них метод Insert. Он позволяет при записи нового значения в Cache установить период устаревания, зависимость от файла, указать callback-функции и приоритетность информации. Следующие несколько примеров демонстрируют ее использование:
// устаревание при изменении файла
Cache.Insert("My Key", "My Value", new CacheDependency(Server.MapPath(@"\myServer\myConfig.xml")));
// устаревание по абсолютной дате
Cache.Insert("My Key", "My Value", null, new DateTime(2007,1,1), Cache.NoSlidingExpiration);
// устаревание через 30 секунд
Cache.Insert("My Key", "My Value", null, Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(30));
// устаревание через 30 секунд, значение с повышенным приоритетом
Cache.Insert("My Key", "My Value", null, Cache.NoAbsoluteExpiration, TimeSpan.FromSeconds(30),
CacheItemPriority.High, null);
// устаревание по абсолютной дате c использованием callback-функции
// объявление callback-функции
private void RemovedCallback(String key, Object val, CacheItemRemovedReason reason)
{
// некоторые действия
}
// добавление в Cache
Cache.Insert("My Key", "My Value", null, new DateTime(2007,1,1),
Cache.NoSlidingExpiration, CacheItemPriority.Low,
new CacheItemRemovedCallback(RemovedCallback));
Context.Items
Контейнер Context.Items содержит информацию, относящуюся к определенному запросу определенного пользователя. Жизненный цикл этого контейнера ограничивается жизненным циклом самого запроса. Контейнер Context.Items позволяет сохранять любые объемы информации, но из-за короткого времени жизни в основном используется для небольших временных объектов.
Этот контейнер доступен как через одноименное свойство веб-страницы, так и через свойство System.Web.HttpContext.Current, что позволяет использовать его из всех Http модулей и Http хэндлеров веб-приложения. Типичным применением является запись некоторой конфигурационной информации, которая будет использоваться из подключаемых Http модулей.
// добавление значения
Context.Items["myKey"] = myValue;
// чтение значения
Response.Write(Context["myKey"]);
Как мы видим, ASP.NET предоставляет достаточно много способов управления состоянием, у каждого из которых есть свои достоинства и недостатки. ViewState лучше использовать для кратковременного хранения небольшого объема информации, которая относиться непосредственно к определенной странице. Cookie следует использовать для долговременного хранения простых структур данных на клиенте, как правило, это различного рода настройки пользователя. Application в данный момент считается устаревшим и не рекомендуется использовать, хотя он вполне подойдет для хранения какой-нибудь общей для всего приложения информации, например connection string к базе данных. Session необходимо использовать для хранения пользовательской информации. Этот контейнер предоставляет достаточный уровень защищенности для хранения в нем секретной информации пользователя и позволяет защититься от возможной потери информации при сбоях веб-приложения или веб-сервера. Однако не рекомендуется хранить в сессии слишком большие объемы информации, поскольку при существенном увеличении пользователей приложения могут возникнуть проблемы с нехваткой ресурсов на сервере. Для кэширования больших объемов лучше использовать Cache. В этом контейнере, как правило, сохраняют редко изменяющиеся справочники. Кэширование данных позволяет многократно ускорить скорость получения данных по сравнению с той же операцией с СУБД. Context.Items в основном служит для передачи данных между HTTP модулями.
Александр Басюк