Некоторые подводные камни при использовании сессии
Рассмотрим типичный случай взаимодействия двух страниц в ASP.NET, одна из которых это таблица, а вторая это форма по редактированию записей: Grid и Edit. Кроме основной функции, Edit может также создавать новую запись, поэтому правильней будет назвать её Add/Edit.
Итак, какие сценарии у нас предусмотрены:
Для поддержки обоих сценариев, Add/Edit должна определить режим работы для правильного фунционирования. Поскольку для редактирования записи нужно каким то образом передать идентификатор, можно возпользоваться коллекцией Session и соответственно переключать режимы работы в зависимости от наличия идентификатора в сессии.
Таким образом в Grid мы кладем Id в сессию:
Конечно, надо очистить Session[«EditId»], иначе Add режим никогда не включится. Но если мы очистим этот ключ, то на следующем постбэке, страница перейдет в Add режим, поскольку ключа в сессии уже нет… Можно, правда, переместить идентификатор в ViewState поскольку он сохраняется между постбэками. Модифицируем Add/Edit:
Вроде бы всё работает, и мы может сдать код на тестирование, если бы не одно но. А что если пользователь перейдет на Add/Edit из Grid, и просто обновит страницу? То есть нажмет F5. В таком случае постбэка не будет, и в сессии идентификатора тоже не будет (т.к. мы его успешно убрали), и страница внезапно перейдет в режим Add. Мда… согласитесь, неприятно. Особенно неприятно это выглядит с точки зрения пользователя, который при обновлении страницы не ожидает таких кардинальных изменений!
А так же фабрику классов:
и переписываем логику в Grid:
и в 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 комментариев