Некоторые подводные камни при использовании сессии

Рассмотрим типичный случай взаимодействия двух страниц в ASP.NET, одна из которых это таблица, а вторая это форма по редактированию записей: Grid и Edit. Кроме основной функции, Edit может также создавать новую запись, поэтому правильней будет назвать её Add/Edit.

Итак, какие сценарии у нас предусмотрены:
  • Редактирование существующей записи
  • Добавление новой записи



Для поддержки обоих сценариев, Add/Edit должна определить режим работы для правильного фунционирования. Поскольку для редактирования записи нужно каким то образом передать идентификатор, можно возпользоваться коллекцией Session и соответственно переключать режимы работы в зависимости от наличия идентификатора в сессии.
Таким образом в Grid мы кладем Id в сессию:
Session["EditId"] = id;
, а в Add/Edit смотрим существует ли Id:
if (Session["EditId"] != null) { //enable edit mode } else { //enable add mode }


Конечно, надо очистить Session[«EditId»], иначе Add режим никогда не включится. Но если мы очистим этот ключ, то на следующем постбэке, страница перейдет в Add режим, поскольку ключа в сессии уже нет… Можно, правда, переместить идентификатор в ViewState поскольку он сохраняется между постбэками. Модифицируем Add/Edit:
if (!IsPostBack && Session["EditId"] != null)
{
 idToEdit = (string) Session["EditId"];
 ViewState["EditId"] = idToEdit;
 Session.Remove("EditId");
 ...
}


Вроде бы всё работает, и мы может сдать код на тестирование, если бы не одно но. А что если пользователь перейдет на Add/Edit из Grid, и просто обновит страницу? То есть нажмет F5. В таком случае постбэка не будет, и в сессии идентификатора тоже не будет (т.к. мы его успешно убрали), и страница внезапно перейдет в режим Add. Мда… согласитесь, неприятно. Особенно неприятно это выглядит с точки зрения пользователя, который при обновлении страницы не ожидает таких кардинальных изменений!

Итак, что же делать?
Тут уместно вспомнить, что такое сессия, вьюстейт и что нам надо. Сессия это коллекция доступная всем страницам. Вьюстейт доступен только одной странице и только между постбэками. Нам же надо наладить связь между двумя страницами, причем эта связь должна быть уникальной (чтобы история браузера работала) и надежной (чтобы неожиданности не случались при обновлении страницы). Подумав немного, я написал класс PageState, инкапсулирующий эту логику и добавляющий немного фишек.
public class BasePageState
 {
 protected const string QueryStringParameter = "PageState";
 private readonly string id;

public BasePageState()
 {
 this.id = GenerateCode(8);
 }

protected BasePageState(string id)
 {
 this.id = id;
 if (this.ExpectedUrl != HttpContext.Current.Request.RawUrl)
 throw new InvalidOperationException("Expected url does not match current url");
 }

public BasePageState(BasePageState sourcePageState)
 {
 this.ReturnUrl = sourcePageState.ReturnUrl;
 }

public string Id
 {
 get { return this.id; }
 }

public string ExpectedUrl
 {
 get { return this.GetValue("ExpectedUrl") as string; }
 set { this.SetValue("ExpectedUrl", value); }
 }

public string QueryStringPair
 {
 get { return QueryStringParameter + "=" + this.Id; }
 }

public string ReturnUrl
 {
 get { return (string)this.GetValue("ReturnUrl"); }
 set { this.SetValue("ReturnUrl", value); }
 }

protected object this[string name]
 {
 get { return HttpContext.Current.Session[this.Id + name]; }
 set { HttpContext.Current.Session[this.Id + name] = value; }
 }

protected object GetValue(string name)
 {
 return this[name];
 }

protected void SetValue(string name, object obj)
 {
 this[name] = obj;
 }
 
 public static string GenerateCode(int digits)
 {
 byte[] random = new Byte[digits];
 //RNGCryptoServiceProvider is an implementation of a random number generator.
 RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
 rng.GetNonZeroBytes(random); // The array is now filled with cryptographically strong random bytes, and none are zero.
 StringBuilder s = new StringBuilder(digits);
 for (int i = 0; i < digits; i++)
 {
 int code = ((random[i] - 1) * 34) / 255;
 if (code < 9) // 9 to leave out 0 (zero)
 s.Append((char)(code + '1'));
 else
 {
 char c = (char)((code - 9) + 'A');
 if (c == 'O') // leave out O (oh)
 c = 'Z';
 s.Append©;
 }
 }
 return s.ToString();
 }

public static string GetReturnUrl(HttpRequest request)
 {
 BasePageState pageState = new BasePageState(request[QueryStringParameter]);
 return pageState.ReturnUrl ?? string.Empty;
 }

public void RedirectTo(string url, string attributes)
 {
 if (url == null) throw new ArgumentNullException(url);
 string attrib = pageState.QueryStringPair;
 if (!string.IsNullOrEmpty(attributes))
 attrib += "&" + attributes;

this.ExpectedUrl = GetUrl(portalUrl.GetUrl(), attrib);
 if (this.ReturnUrl == null)
 this.ReturnUrl = HttpContext.Current.Request.RawUrl;
 HttpContext.Current.Response.Redirect(this.ExpectedUrl);
 }

private static string GetUrl(string url, string attrib)
 {
 string result = BuildUrl(url, attrib);
 result = HttpContext.Current.Response.ApplyAppPathModifier(result);
 return result;
 }

private static string BuildUrl(string url, string attributes)
 {
 StringBuilder sb = new StringBuilder();

sb.Append(url);
 if(attributes != null && attributes != "")
 {
 if(attributes.StartsWith("?"))
 attributes = attributes.Substring(1);

if(url.IndexOf("?") < 0)
 sb.Append("?");
 else
 sb.Append("&");
 sb.Append(attributes);
 }
 return sb.ToString();
 }
 }

А так же фабрику классов:
public static class PageStateFactory where T : BasePageState
 {
 private const string QueryStringParameter = "PageState";
 private static readonly Type ConcreteType = BuildType();

public static T Create()
 {
 return (T)Activator.CreateInstance(ConcreteType);
 }

public static T Load(HttpRequest request)
 {
 return (T)Activator.CreateInstance(ConcreteType, request[QueryStringParameter]);
 }

public static bool Exists(HttpRequest request)
 {
 return request.QueryString[QueryStringParameter] != null;
 }

public static T Copy(HttpRequest request)
 {
 var existingPageState = Load(request);
 return (T)Activator.CreateInstance(ConcreteType, existingPageState);
 }

public static T Copy(T sourcePageState)
 {
 return (T)Activator.CreateInstance(ConcreteType, sourcePageState);
 }

private static Type BuildType()
 {
 const MethodAttributes methodAttributes = 
 MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;
 Type baseType = typeof(T);

AppDomain appDomain = AppDomain.CurrentDomain;
 AssemblyName name = new AssemblyName("PageStates.Concrete");
 AssemblyBuilder assemblyBuilder = appDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run);
 ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name.Name);
 TypeBuilder typeBuilder = moduleBuilder.DefineType("ConcretePageState", TypeAttributes.Class, baseType);
 

typeBuilder.DefineDefaultConstructor(methodAttributes | MethodAttributes.RTSpecialName);
 var constructorBuilder = typeBuilder.DefineConstructor(
 methodAttributes | MethodAttributes.RTSpecialName, 
 CallingConventions.HasThis, new[] { typeof(string) }, null, null);
 
 var constructorIL = constructorBuilder.GetILGenerator();
 constructorIL.Emit(OpCodes.Ldarg_0);
 constructorIL.Emit(OpCodes.Ldarg_1);
 constructorIL.Emit(OpCodes.Call, baseType.GetConstructor(
 BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, 
 null, new[] { typeof(string) }, null));
 constructorIL.Emit(OpCodes.Ret);

var copyConstructorBuilder = typeBuilder.DefineConstructor(
 methodAttributes | MethodAttributes.RTSpecialName,
 CallingConventions.HasThis, new[] { baseType }, null, null);

var copyConstructorIL = copyConstructorBuilder.GetILGenerator();
 copyConstructorIL.Emit(OpCodes.Ldarg_0);
 copyConstructorIL.Emit(OpCodes.Call, baseType.GetConstructor(
 BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public,
 null, new Type[0], null));
 
 
 MethodInfo baseGetValue = baseType.GetMethod("GetValue", BindingFlags.Instance | BindingFlags.NonPublic);
 MethodInfo baseSetValue = baseType.GetMethod("SetValue", BindingFlags.Instance | BindingFlags.NonPublic);
 foreach (var info in baseType.GetProperties(BindingFlags.Public | BindingFlags.Instance))
 {
 if (info.GetGetMethod().IsAbstract)
 {
 #region getter and setter for property
 MethodBuilder getBuilder = typeBuilder.DefineMethod(
 "get_" + info.Name, methodAttributes | MethodAttributes.Virtual, info.PropertyType, null);
 var getIL = getBuilder.GetILGenerator();
 getIL.Emit(OpCodes.Ldarg_0);
 getIL.Emit(OpCodes.Ldstr, info.Name);
 getIL.EmitCall(OpCodes.Call, baseGetValue, null);
 getIL.Emit(info.PropertyType.IsValueType ? OpCodes.Unbox_Any : OpCodes.Castclass, info.PropertyType);
 getIL.Emit(OpCodes.Ret);
 MethodBuilder setBuilder = typeBuilder.DefineMethod(
 "set_" + info.Name, methodAttributes | MethodAttributes.Virtual, null, new[] { info.PropertyType });
 var setIL = setBuilder.GetILGenerator();
 setIL.Emit(OpCodes.Ldarg_0);
 setIL.Emit(OpCodes.Ldstr, info.Name);
 setIL.Emit(OpCodes.Ldarg_1);
 if (info.PropertyType.IsValueType)
 setIL.Emit(OpCodes.Box, info.PropertyType);
 setIL.EmitCall(OpCodes.Call, baseSetValue, null);
 setIL.Emit(OpCodes.Ret); 
 #endregion

copyConstructorIL.Emit(OpCodes.Ldarg_0);
 copyConstructorIL.Emit(OpCodes.Ldarg_1);
 copyConstructorIL.EmitCall(OpCodes.Callvirt, getBuilder, null);
 copyConstructorIL.EmitCall(OpCodes.Call, setBuilder, new[] { info.PropertyType });
 }
 }
 copyConstructorIL.Emit(OpCodes.Ret);
 return typeBuilder.CreateType();
 }
 }


Пример использования
Вернемся к нашему примеру выше. Допустим там таблица аккаунтов и соответственно надо на страницу Add/Edit передать Id аккаунта. Для этого создает абстрактный класс AccountPageState:
public abstract class AccountPageState : BasePageState
{
 protected AccountPageState() {}

protected AccountPageState(string id) : base (id) {}

public abstract int AccountId { get; set; }
}


и переписываем логику в Grid:
AccountPageState pageState = PageStateFactory.Create();
pageState.AccountId = id;
pageState.RedirectTo("/EditPage.aspx");



и в Add/Edit:

if (PageStateFactory.Exists(this.Request))
{
 //edit mode
 var pageState = PageStateFactory.Load(this.Request);
 var id =pageState.AccountId;
 // и тут же мы знаем куда идти назад:
 Response.Redirect(pageState.ReturnUrl);
} else {
 //add mode
}

<h5>Заключение</h5>

Какие плюсы моего решения?
<ul>
<li>Правильная обработка сценариев работы</li>
<li>Строгая типизация передаваемых параметров</li>
<li>Дополнительный слой защиты от подмены данных</li>
<li>Инкапсулирование и достаточно удобное использование</li>
<li>Корректная работа с историей браузера</li>
</ul>

Минусы
<ul>
<li>Все созданные связи хранятся в сессии и поэтому доступны лишь в рамках жизни сессии.</li>
<li>Динамическое создание типов (хоть и делается единожды)</li>
</ul>

На мой взгляд, плюсы существенно перевешивают минусы, так что задачу считаю выполненной.


0 комментариев

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.