Некоторые подводные камни при использовании сессии
Рассмотрим типичный случай взаимодействия двух страниц в 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 }
Заключение
Какие плюсы моего решения?
- Правильная обработка сценариев работы
- Строгая типизация передаваемых параметров
- Дополнительный слой защиты от подмены данных
- Инкапсулирование и достаточно удобное использование
- Корректная работа с историей браузера
Минусы
- Все созданные связи хранятся в сессии и поэтому доступны лишь в рамках жизни сессии.
- Динамическое создание типов (хоть и делается единожды)
На мой взгляд, плюсы существенно перевешивают минусы, так что задачу считаю выполненной.