Рекомендации по использованию различных типов хранилищ
Glossary Item Box
Общие положения
Хранение бизнес-данных в bpm'online реализовано в виде реляционной базы данных (MS SQL или Oracle). Эти данные используются при решении различных пользовательских задач, выполнении бизнес-процессов и т. п. Помимо этого, приложение bpm'online обеспечивает:
- Хранение данных пользователя и приложения (профиль пользователя, сессионные данные и т. п.).
- Обмен данными между узлами веб-фермы.
- Промежуточное хранение информации в момент перезапуска приложения или узла веб-фермы.
- Выполнение промежуточных действий с данными перед помещением их в хранилище.
Для решения таких задач в архитектуре bpm'online реализована технология хранилищ данных. В основе технологии — объектная модель классов, которая представляет собой унифицированный API для доступа из приложения к данным, расположенным во внешнем хранилище (на текущий момент в bpm'online в качестве такого внешнего хранилища используется Redis).
Хранилища bpm'online в первую очередь ориентированы на выполнение служебных функций организации работы с данными, но при этом они могут быть использованы для решения пользовательских задач (в конфигурационной бизнес-логике).
Redis как сервер хранилищ bpm'online
Сервером хранилищ bpm'online в настоящее время является Redis — высокопроизводительное нереляционное хранилище данных.
Модель данных Redis основана на принципе пар "ключ-значение". Redis поддерживает доступ только по уникальному ключу, а в качестве хранимого значения могут выступать сложные структуры с дополнительными операциями. В bpm'online помещаемые в хранилище данные хранятся в виде бинарных сериализованных объектов.
Redis поддерживает несколько стратегий хранения данных:
- Хранение данных только в памяти. Персистентная база данных преобразуется в некий кэширующий сервер.
- Периодическое сохранение данных на диск (по умолчанию). Периодическое создание копий (snapshot) один раз в 1–15 минут в зависимости от времени создания предыдущей копии и количества измененных ключей.
- Лог транзакций. Синхронная запись каждого изменения в специальный append-only лог-файл.
- Репликация. Каждому серверу можно указать мастер-сервер, после чего все изменения на мастере будут воспроизводиться и на подчиненном.
Конкретный способ хранения данных определяется путем конфигурирования сервера Redis. На текущий момент в bpm'online хранение данных осуществляется в памяти с периодическим сохранением дампа на диск, однако не поддерживается возможность репликации данных.
Более подробно информация по Redis представлена в официальной документации.
Виды хранилищ bpm'online. Хранилище данных и кэш
В bpm'online реализована поддержка двух видов хранилищ — хранилища данных и кэша.
Хранилище данных предназначено для промежуточного хранения редко изменяемых, "долгосрочных" данных. В кэше же, как правило, хранится оперативная информация.
Дополнительно для каждого вида хранилища определены свои логические уровни размещения данных (табл. 1, 2).
Табл. 1. — Уровни хранилища данных
Уровень | Описание |
---|---|
Request | Уровень запроса. Данные доступны только в течение времени обработки текущего запроса. Соответствует значению перечисления Terrasoft.Core.Store.DataLevel.Request. |
Session | Уровень сессии. Данные доступны только в сессии текущего пользователя. Соответствует значению перечисления Terrasoft.Core.Store.DataLevel.Session. |
Application | Уровень приложения. Данные доступны для всего приложения. Соответствует значению перечисления Terrasoft.Core.Store.DataLevel.Application. |
Табл. 2. — Уровни кэша
Уровень | Описание |
---|---|
Session | Уровень сессии. Данные доступны только в сессии текущего пользователя. Соответствует значению перечисления Terrasoft.Core.Store.CacheLevel.Session. |
Workspace | Уровень рабочего пространства. Данные доступны для всех пользователей одного и того же рабочего пространства. Соответствует значению перечисления Terrasoft.Core.Store.CacheLevel.Workspace. |
Application | Уровень приложения. Данные доступны всем пользователям приложения вне зависимости от рабочего пространства. Соответствует значению перечисления Terrasoft.Core.Store.CacheLevel.Application. |
Такая дифференциация хранилищ введена с целью логического разделения помещаемой в хранилище информации и дальнейшего удобства использования ее в программном коде. Кроме того, такое разделение позволяет:
- Изолировать данные между рабочими пространствами и между сессиями пользователей.
- Условно классифицировать данные.
- Управлять временем жизни данных.
Физически все данные хранилища и кэша могут располагаться на абстрактном сервере хранения данных. Исключение составляют данные хранилища уровня Request, которые хранятся непосредственно в памяти.
В настоящее время сервером хранилищ bpm'online является Redis. Однако в общем случае это может быть произвольное хранилище, доступ к которому осуществляется через унифицированные интерфейсы. При этом необходимо учитывать, что операции обращения к хранилищу являются ресурсоемкими, так как связаны с сериализацией/десериализацией данных и сетевым обменом.
Хранилище данных и кэш предоставляют следующие возможности по работе с данными:
- доступ к данным по ключу для чтения/записи;
- удаление данных из хранилища по ключу.
Ключевым различием между хранилищем данных и кэшем является подход к управлению временем жизни объектов, хранящихся в них.
Данные, помещаемые в хранилище, будут содержаться в нем до момента их явного удаления. Время жизни таких объектов ограничено временем выполнения запроса (для данных хранилища уровня Request) или временем существования сессии (для данных хранилища уровня Session).
Для данных, хранящихся в кэше, существует такое понятие, как время устаревания, которое определяет граничный срок актуальности конкретного элемента кэша. При этом, независимо от времени устаревания, все элементы удаляются из кэша:
- при завершении сессии (элементы кэша и хранилища данных уровня Session);
- при явном удалении рабочего пространства (элементы кэша уровня Workspace).
Элементы хранилища данных и кэша уровня Application сохраняются на протяжении всего периода существования приложения и могут быть удалены из него только путем непосредственной очистки внешнего хранилища.
ВАЖНО Данные могут быть удалены из кэша в произвольный момент времени. В связи с этим могут возникать ситуации, когда программный код пытается получить закэшированные данные, которые на момент обращения уже удалены из кэша. В этом случае вызывающему коду необходимо просто получить эти данные из постоянного хранилища и положить их в кэш. |
Объектная модель хранилищ bpm'online
Классы и интерфейсы bpm'online для работы с хранилищем данных и кэшем
Для работы с хранилищем данных и кэшем в bpm'online реализован ряд классов и интерфейсов, которые расположены в пространстве имен Terrasoft.Core.Store (".NET библиотеки классов ядра платформы"). Ниже кратко перечислены основные из них.
Базовое хранилище IBaseStore
Базовые возможности всех типов хранилищ определяет интерфейс IBaseStore. Свойства и методы этого интерфейса реализуют:
- Доступ к данным по ключу для чтения/записи (индексатор this[string key]).
- Удаление данных из хранилища по заданному ключу (метод Remove(string key)).
- Инициализация хранилища заданным списком параметров (метод Initialize(IDictionary parameters)). На текущий момент параметры для инициализации хранилищ bpm'online вычитываются из конфигурационного файла. Списки параметров задаются в секциях storeDataAdapter (для хранилища данных), storeCacheAdapter (для кэша). Но в общем случае параметры могут задаваться произвольным образом.
Хранилище данных IDataStore
Интерфейс определяет специфику работы с хранилищами данных. Является наследником базового интерфейса хранилищ IBaseStore. Дополнительно в интерфейсе определена возможность получения списка всех ключей хранилища (свойство Keys).
ВАЖНО Свойство Keys при работе с хранилищами данных желательно использовать только в исключительных случаях, когда решение задачи альтернативными способами невозможно. |
Хранилище кэша ICacheStore
Интерфейс определяет специфику работы с кэшами. Является наследником базового интерфейса хранилищ IBaseStore. В интерфейсе определено свойство GetValues(IEnumerable keys), которое возвращает словарь объектов кэша с заданными ключами. Данный метод позволяет оптимизировать работу с хранилищем при одновременном получении набора данных.
Класс Store
Статический класс для доступа к кэшам и хранилищам данных различных уровней. Уровни хранилища данных и кэша определены в перечислениях Terrasoft.Core.Store.DataLevel и Terrasoft.Core.Store.CacheLevel соответственно (см. табл. 1 и 2).
Класс Store имеет два статических свойства:
- Свойство Data возвращает экземпляр провайдера хранилища данных.
- Свойство Cache возвращает экземпляр провайдера кэша.
В примере 1 продемонстрирована работа с кэшем и хранилищем данных с использованием класса Store.
Пример 1
// Получение ссылки на хранилище данных приложения уровня сессии. IDataStore dataStore = Store.Data[DataLevel.Session]; // Помещение в хранилище данных значения "Data Test Value" с ключом "DataKey". dataStore["DataKey"] = "Data Test Value"; // Получение ссылки на кэш приложения уровня рабочего пространства. ICacheStore cacheStore = Store.Cache[CacheLevel.Workspace]; // Удаление из кэша элемента с ключом "CacheKey". cacheStore.Remove("CacheKey");
Доступ к хранилищам данных и кэшам из UserConnection
Доступ к хранилищам данных и кэшам приложения из конфигурационного кода можно получить, используя свойства статического класса Store пространства имен Terrasoft.Core.Store. Альтернативным вариантом доступа к хранилищу данных и кэшу в конфигурационной логике, который позволяет избежать использования длинных имен свойств и подключения дополнительных сборок, является доступ через экземпляр класса UserConnection.
В классе UserConnection реализован ряд вспомогательных свойств, позволяющих получить быстрый доступ к хранилищам данных и кэшу различных уровней:
- ApplicationCache возвращает ссылку на кэш уровня приложения.
- WorkspaceCache возвращает ссылку на кэш уровня рабочего пространства.
- SessionCache возвращает ссылку на кэш уровня сессии.
- RequestData возвращает ссылку на хранилище данных уровня запроса.
- SessionData возвращает ссылку на хранилище данных уровня сессии.
- ApplicationData возвращает ссылку на хранилище данных уровня приложения.
ВАЖНО В большинстве случаев обращение к свойствам UserConnection идентично обращениям к свойствам Store.Cache и Store.Data с указанием соответствующих уровней. Однако в некоторых ситуациях (например, при запуске бизнес-процессов с помощью планировщика) может использоваться другая реализация. Поэтому в конфигурационной логике для доступа к хранилищам рекомендуется использовать свойства объекта UserConnection. |
Пример 2
// Ключ, с которым в кэш будет помещаться значение. string cacheKey = "SomeKey"; // Помещение значения в кэш уровня сессии через свойство UserConnection. UserConnection.SessionCache[cacheKey] = "SomeValue"; // Получение значения из кэша через свойство класса Store. // В результате переменная valueFromCache будет содержать значение "SomeValue". string valueFromCache = Store.Cache[CacheLevel.Session][cacheKey] as String;
Использование кэша в EntitySchemaQuery
В EntitySchemaQuery реализован механизм работы с хранилищем (кэшем bpm'online либо произвольным хранилищем, определенным пользователем). Работа с кэшем позволяет оптимизировать эффективность выполнения операций за счет обращения к закэшированным результатам запроса без дополнительного обращения к базе данных. При выполнении запроса EntitySchemaQuery данные, полученные из базы данных на сервере, помещаются в кэш, который определяется свойством Cache с ключом, который задается свойством CacheItemName. По умолчанию в качестве кэша запроса EntitySchemaQuery выступает кэш bpm'online уровня сессии с локальным хранением данных. В общем случае в качестве кэша запроса может выступать произвольное хранилище, которое реализует интерфейс ICacheStore.
Ниже приведен пример работы с кэшем приложения при выполнении запроса EntitySchemaQuery (пример 3). В примере строится запрос, который возвращает список всех городов схемы City. При получении результатов выполнения запроса (после вызова метода GetEntityCollection()) эти результаты помещаются в кэш, откуда затем могут использоваться при повторном получении коллекции элементов запроса без дополнительного обращения к базе данных.
Пример 3
// Создание экземпляра запроса EntitySchemaQuery с корневой схемой City. var esqResult = new EntitySchemaQuery(UserConnection.EntitySchemaManager, "City"); // Добавление в запрос колонки с наименованием города. esqResult.AddColumn("Name"); // Определение ключа, под которым в кэше будут храниться результаты выполнения запроса. // В качестве кэша выступает кэш уровня сессии с локальным кэшированием данных (так как не // переопределяется свойство Cache объекта). esqResult.CacheItemName = "EsqResultItem"; // Выполнение запроса к базе данных для получения результирующей коллекции объектов. // После выполнения этой операции результаты запроса будут помещены в кэш. При дальнейшем обращении к // esqResult для получения коллекции объектов запроса (если сам запрос не был изменен) эти объекты будут // браться из сессионного кэша. esqResult.GetEntityCollection(UserConnection);
Прокси-классы хранилища и кэша
Понятие и назначение прокси-классов
Доступ к хранилищам и кэшам в bpm'online может быть осуществлен как напрямую (через свойства класса Store), так и через прокси-классы.
Прокси-классы — это специальные объекты, которые представляют собой промежуточное звено между хранилищами и вызывающим кодом. Прокси-классы позволяют выполнять промежуточные действия над данными перед их чтением/записью в хранилище. Особенностью прокси-классов является то, что каждый из них является хранилищем.
Области применения прокси-классов
1. Первоначальная настройка и конфигурирование приложения
В конфигурационном файле Web.config можно настроить использование прокси-классов для хранилищ данных и кэшей. Настройка прокси-классов для соответствующего хранилища данных или кэша осуществляется в секциях storeDataAdapter и storeCacheAdapter соответственно. В них добавляется секция proxies, в которой перечисляются все прокси-классы, применяемые к хранилищу (пример 4).
Пример 4
<storeDataAdapters> <storeAdapter levelName="Request" type="RequestDataAdapterClassName"> <proxies> <proxy name="RequestDataProxyName1" type="RequestDataProxyClassName1" /> <proxy name="RequestDataProxyName2" type="RequestDataProxyClassName2" /> <proxy name="RequestDataProxyName3" type="RequestDataProxyClassName3" /> </proxies> </storeAdapter> </storeDataAdapters> <storeCacheAdapters> <storeAdapter levelName="Session" type="SessionCacheAdapterClassName"> <proxies> <proxy name="SessionCacheProxyName1" type="SessionCacheProxyClassName1" /> <proxy name="SessionCacheProxyName2" type="SessionCacheProxyClassName2" /> </proxies> </storeAdapter> </storeCacheAdapters>
При загрузке приложения настройки считываются из конфигурационного файла и применяются к соответствующему виду хранилища. Таким образом можно выстраивать цепочки прокси-классов, которые будут выполняться последовательно один за другим. Порядок прокси-классов в цепочке выполнения соответствует их порядку в конфигурационном файле. При этом первым в цепочке выполнения выступает прокси-класс, перечисленный последним в секции proxies, то есть выполнение осуществляется "снизу вверх".
При этом необходимо учитывать следующее:
- "Конечной точкой" применения цепочки прокси-классов является конкретный кэш или хранилище данных, для которого эта цепочка определяется.
- Отдельно взятый прокси-класс знает, с каких кэшем или хранилищем он работает – ссылка на него определяется свойствами ICacheStoreProxy.CacheStore или IDataStoreProxy.DataStore. Однако, на что именно ссылается это свойство (на другой прокси-класс, на конечное хранилище или кэш), для прокси-класса неизвестно. При этом сам прокси может выступать в качестве хранилища, с которым будут работать другие прокси-классы.
Так, в соответствии с настройками, приведенными в примере 4, цепочка выполнения прокси-классов, например, хранилища данных, будет следующей: RequestDataProxyName3 > RequestDataProxyName2 > RequestDataProxyName1 > RequestDataAdapterClassName (конечное хранилище данных уровня запроса).
2. Разграничение данных различных пользователей приложения
С помощью прокси-классов решается задача изоляции данных между пользователями. Наиболее простым решением этой задачи является трансформация ключей значений перед помещением их в хранилище (например, путем добавления к ключу дополнительного префикса, специфичного для конкретного пользователя). Использование таких прокси-классов обеспечивает уникальность ключей хранилища. Это, в свою очередь, позволяет избежать потери и искажения данных при одновременной попытке разных пользователей записать в хранилище различные значения с одинаковым ключом. Пример работы с прокси-классами трансформации ключей приведен ниже. В общем случае в прокси-классах можно реализовать насколько угодно сложную логику.
3. Выполнение других промежуточных действий с данными перед помещением их в хранилище
В прокси-классах можно реализовывать логику выполнения любых произвольных действий с данными перед помещением их в хранилище или получения их из него. Вынесение логики обработки данных в прокси-класс позволяет избежать дублирования кода, что, в свою очередь, облегчает его модификацию и сопровождение.
Базовые интерфейсы прокси-классов
Чтобы класс можно было использовать как прокси для работы с хранилищами, он должен реализовывать один или оба интерфейса пространства имен Terrasoft.Core.Store:
- IDataStoreProxy — интерфейс прокси-классов хранилища данных.
- ICacheStoreProxy — интерфейс прокси-классов кэша.
Каждый из этих интерфейсов имеет одно свойство — ссылку на то хранилище (или кэш), с которым работает данный прокси-класс. Для интерфейса IDataStoreProxy это свойство DataStore, а для интерфейса ICacheStoreProxy — свойство CacheStore.
Прокси-классы трансформации ключей
В bpm'online реализован ряд прокси-классов, реализующих логику трансформации ключей значений, помещаемых в хранилища.
1. Класс KeyTransformerProxy
Это абстрактный класс, который является базовым классом для всех прокси-классов, преобразующих ключи кэша. Он реализует методы и свойства интерфейса ICacheStoreProxy. Чтобы избежать дублирования логики, при создании пользовательских прокси-классов для трансформации ключей рекомендуется наследоваться от этого класса.
2. Класс PrefixKeyTransformerProxy
Прокси-класс, преобразующий ключи кэша путем добавления к ним заданного префикса.
В примере 5 демонстрируется работа с кэшем уровня сессии через прокси-класс PrefixKeyTransformerProxy.
Пример 5
// Ключ, с которым значение будет помещаться в кэш через прокси. string key = "Key"; // Префикс, который будет добавляться к ключу значения прокси-классом. string prefix = "customPrefix"; // Создание прокси-класса, который будет использоваться для записи значений в кэш уровня сессии. ICacheStore proxyCache = new PrefixKeyTransformerProxy(prefix, Store.Cache[CacheLevel.Session]); // Запись значения с ключом key в кэш через прокси-класс. // Фактически это значение записывается в глобальный кэш уровня сессии с ключом prefix + key. proxyCache[key] = "CachedValue"; // Получение значения по ключу key через прокси. var valueFromProxyCache = (string)proxyCache[key]; // Получение значения по ключу prefix + key непосредственно из кэша уровня сессии. var valueFromGlobalCache = (string)Store.Cache[CacheLevel.Session][prefix + key];
В итоге переменные valueFromProxyCache и valueFromGlobalCache будут содержать одинаковое значение "CachedValue".
3. Класс DataStoreKeyTransformerProxy
Прокси-класс, преобразующий ключи хранилища данных путем добавления к ним заданного префикса.
В примере 6 демонстрируется работа с хранилищем данных через прокси-класс DataStoreKeyTransformerProxy.
Пример 6
// Ключ, с которым значение будет помещаться в хранилище через прокси. string key = "Key"; // Префикс, который будет добавляться к ключу значения прокси-классом. string prefix = "customPrefix"; // Создание прокси-класса, который будет использоваться для записи значений в хранилище уровня сессии. IDataStore proxyStorage = new DataStoreKeyTransformerProxy(prefix) { DataStore = Store.Data[DataLevel.Session] }; // Запись значения с ключом key в хранилище через прокси-класс. // Фактически это значение записывается в глобальное хранилище уровня сессии с ключом prefix + key. proxyStorage[key] = "StoredValue"; // Получение значения по ключу key через прокси. var valueFromProxyStorage = (string)proxyStorage[key]; // Получение значения по ключу prefix + key непосредственно из хранилища уровня сессии. var valueFromGlobalStorage = (string)Store.Data[DataLevel.Session][prefix + key];
В итоге переменные valueFromProxyStorage и valueFromGlobalStorage будут содержать одинаковое значение "StoredValue".
Локальное кэширование данных
На базе прокси-классов в bpm'online реализован механизм локального кэширования данных. Основная цель кэширования данных — снижение нагрузки на сервер хранения данных и времени выполнения запросов при работе с редко изменяемыми данными.
Логику механизма локального кэширования реализует внутренний прокси-класс LocalCachingProxy. Этот прокси-класс выполняет кэширование данных на текущем узле web-фермы. Класс выполняет мониторинг времени жизни кэшированных объектов и получает данные из глобального кэша только в том случае, если кэшированные данные не актуальны.
Для применения механизма локального кэширования на практике используются методы расширения для интерфейса ICacheStore из статического класса CacheStoreUtilities:
- WithLocalCaching() — переопределенный метод, который возвращает экземпляр класса LocalCachingProxy, выполняющего локальное кэширование.
- WithLocalCachingOnly(string) — метод, выполняющий локальное кэширование данных заданной группы элементов с мониторингом их актуальности.
- ExpireGroup(string) — метод устанавливает признак устаревания для заданной группы элементов. При вызове этого метода все элементы заданной группы становятся неактуальными и не возвращаются при запросе данных.
В примере 7 демонстрируется работа с кэшем рабочего пространства с использованием локального кэширования.
Пример 7
// Создание первого прокси-класса, который выполняет локальное кэширование данных. Все элементы, // записываемые в кэш через этот прокси, принадлежат к группе контроля актуальности Group1. ICacheStore cacheStore1 = Store.Cache[CacheLevel.Workspace].WithLocalCaching("Group1"); // Добавление элемента в кэш через прокси-класс. cacheStore1["Key1"] = "Value1"; // Создание второго прокси-класса, который выполняет локальное кэширование данных. Все элементы, // записываемые в кэш через этот прокси, также принадлежат к группе контроля актуальности Group1. ICacheStore cacheStore2 = Store.Cache[CacheLevel.Workspace].WithLocalCaching("Group1"); cacheStore2["Key2"] = "Value2"; // Для всех элементов группы Group1 устанавливается признак устаревания. Устаревшими считаются элементы // с ключами Key1 и Key2, так как они принадлежат к одной группе контроля актуальности Group1, несмотря на // то, что добавлены в кэш через разные прокси. cacheStore2.ExpireGroup("Group1"); // Попытка получения значений из кэша по ключам Key1 и Key2 после того, как эти элементы были помечены как // устаревшие. В результате переменные cachedValue1 и cachedValue2 будут содержать значение null. var cachedValue1 = cacheStore1["Key1"]; var cachedValue2 = cacheStore1["Key2"];
Особенности использования хранилищ данных и кэша
Для повышения эффективности работы с хранилищами данных и кэшами в конфигурационной логике и в логике реализации бизнес-процессов необходимо учитывать следующее:
1. Все объекты, помещаемые в хранилища, должны быть сериализуемыми. Такое требование обусловлено спецификой работы с хранилищами ядра приложения. Так, при сохранении данных в хранилища (за исключением хранилища данных уровня Request) объект предварительно сериализуется, а при получении — десериализуется.
2. Обращение к хранилищу является относительно ресурсоемкой операцией. В связи с этим в коде необходимо избегать излишних обращений к хранилищу. В примерах 8 и 9 приводятся корректные и некорректные варианты работы с кэшем.
Пример 8
Неоптимальный код:
// Выполняется сетевое обращение и десериализация данных. if (UserConnection.SessionData["SomeKey"] != null) { // Повторно выполняется сетевое обращение и десериализация данных. return (string)UserConnection.SessionData["SomeKey"]; }
Оптимальный код (вариант 1):
// Получение объекта из хранилища в промежуточную переменную. object value = UserConnection.SessionData["SomeKey"]; // Проверка значения промежуточной переменной. if (value != null) { // Возврат значения. return (string)value; }
Оптимальный код (вариант 2):
// Использование расширяющего метода GetValue(). return UserConnection.SessionData.GetValue<string>("SomeKey");
Пример 9
Неоптимальный код:
// Выполняется сетевое обращение и десериализация данных. if (UserConnection.SessionData["SomeKey"] != null) { // Повторно выполняется сетевое обращение и десериализация данных. UserConnection.SessionData.Remove("SomeKey"); }
Оптимальный код:
// Удаление выполняется сразу, без предварительной проверки. UserConnection.SessionData.Remove("SomeKey");
3. Необходимо понимать, что любые изменения состояния объекта, полученного из хранилища или кэша, происходят локально в памяти и не фиксируются в хранилище автоматически. Поэтому, чтобы эти изменения отобразились в хранилище, измененный объект необходимо явно в него записать (пример 10).
Пример 10
// Получение словаря значений из хранилища данных сессии по ключу "SomeDictionary". Dictionary<string, string> dic = (Dictionary<string, string>)Store.Data[DataLevel.Session]["SomeDictionary"]; // Изменение значения элемента словаря. Изменения в хранилище не зафиксировались. dic["Key"] = "ChangedValue"; // Добавление нового элемента в словарь. Изменения в хранилище не зафиксировались. dic.Add("NewKey", "NewValue"); // В хранилище данных по ключу "SomeDictionary" записывается словарь. Теперь все внесенные изменения // зафиксированы в хранилище. Store.Data[DataLevel.Session]["SomeDictionary"] = dic;