четверг, 24 июля 2008 г.

NHibernate и валидация

Замечательный проект затеял Dario Quintana в следующей версии библиотеки дополнений NHibernate Contrib:

NHV-logo-white-background

Его предназначение - декларирование и централизованная проверка правил валидации где-нибудь поближе к модели домена.

Например, правила можно описывать в виде атрибутов:

public class User
{
 public virtual int Id
 {
     get { return id; }
 }

 [NotEmpty, NotNull]
 public virtual string UserName
 {
     get { return userName; }
     set { userName = value; }
 }

 [Email]
 public virtual string Email
 {
     get { return email; }
     set { email = value; }
 }

 [Past]
 public DateTime CreatedDate
 {
   get { return createdDate; }
   set { createdDate = value; }
 }

 [Min(18, Message="You are to young!")]
 public int Age
 {
   get { return age; }
   set { age = value; }
 }

 [CreditCardNumber]
 public string CreditCardNumber
 {
   get { return creditCardNumber; }
   set { creditCardNumber = value; }
 }

 ///...
}

Или же, если вам нужна возможность гибкой настройки этих правил, скажем, для согласования с констрейнтами в базе данных, то правила можно определить в файле конфигурации:

<nhv-mapping xmlns="urn:nhibernate-validator-1.0">
 <class name="NHibernate.Validator.Demo.Winforms.Model.Customer, NHibernate.Validator.Demo.Winforms">   
   <property name="FirstName">
     <not-empty/>
     <not-null/>
   </property>   
 
   <property name="Email">
     <email/>
   </property>

   <property name="Zip">
     <pattern  regex="^[A-Z0-9-]+$" message="Examples of valid matches: 234G-34DA | 3432-DF23"/>
     <pattern  regex="^....-....$" message="Must match ....-...."/>
   </property>   
  
 </class> 
</nhv-mapping>

Ну и пример того, как происходит процесс валидации:

Для начала создаем экземпляр валидатора:

NHVConfiguration nhvc = new NHVConfiguration();
nhvc.Properties[Environment.ApplyToDDL] = "false";
nhvc.Properties[Environment.AutoregisterListeners] = "true";
nhvc.Properties[Environment.ValidatorMode] = "UseAttribute";
nhvc.Mappings.Add(new MappingConfiguration("NHibernate.ValidatorDemo.Model", null));

ValidatorEngine validator = new ValidatorEngine();
validator.Configure(nhvc);

Ну и потом пользуемся им:

public void Create([DataBind("user")] User user)
{
 InvalidValue[] errors = validator.Validate(user);

 if (errors.Length > 0)
 {
   Flash["errors"] = errors;
   RedirectToAction("index");
 }
 else
 {
   repository.Create(user);
 }
}

Процесс валидации можно попробовать сделать более прозрачным, например, используя фильтры в ASP.NET MVC, или реализовав какие-то аспекты прямо на уровне модели домена.

P.S. Пост написан мо мотивам этого поста.

понедельник, 7 июля 2008 г.

Распределение сессий ASP.NET с помощью memcached

Для многих разрабатываемых сайтов, расчитанных на успех, и, как следствие, на большой трафик, одним из важных требований является удобное горизонтальное масштабирование и высокий уровень доступности приложения. При решении этих задачи приходится решать задачи уровня системы и уровня приложения. На системном уровне все решается настройкой всевозможных кластеров, систем резервного копирования, диспетчеров нагрузки и т.д. И очень важно эти вынести эти системные задачи за пределы кода приложения, иначе добавление лишнего сервера в кластер может превратиться бесконечный бой со своим кодом.

В общем случае, главной проблемой, мешающей клонированию приложения или сервиса на многие сервера с сохранением общей функциональности является использование состояний клиента. Другими словами - использование сессий. Эта проблема связана с чисто программной реализацией сессий поверх несессионного протокола HTTP, когда идентификатор сеанса отправляется в cookies каждому клиенту, а потом по нему сервер, обслуживающий запросы "достает" нужные данные. И по умолчанию, данные сессий хранятся локально в памяти или файловой системе сервера. А в случае, когда приложение "живет" на нескольких серверах, то идентификатор сеанса каждый раз будет обрабатываться разными серверами и сессии утратят свой смысл.

В качестве решения этой проблемы в ASP.NET предлагается использовать особый режимы StateServer и SQLServer, когда состояние будет храниться на удаленном сервере, либо вообще в базе SQL Server. У первого способа главным недостатком является то, что сам по себе сервер состояний не масштабируется вообще. В случае же использования режима SQLServer и размещении базы данных состояний на отдельном кластере, мы получим масштабируемую систему, но в этом случае сильно потеряем в производительности. Конечно же, когда идет о масштабируемости, локальной производительностью можно пренебречь, посчитав, что ее можно увеличить за счет включения лишних боксов в кластер. Но что делать начинающим стартаперам, которые пока еще считают деньги? Как получить систему, относительно быструю, как в случае со StateServer, и масштабируемую, как в режиме работы с SQL Server?

Ответ прост - использование систем распределенной памяти вобще и memcached в частности, как распространенный, проверенный, простой и бесплатный продукт. Познакомиться с ним можно в статье Обзор memcaced. А в этом посте мы попробуем решить с его помощью проблемы с сессиями в ASP.NET.

Вкратце, нам хотелось бы получить систему, которая бы прозрачно интегрировалась с уже готовым приложением с минимальными изменениями в коде, достаточно быструю и надежную, не теряющую данные, как при перезагрузке отдельных приложений, так и при перезагрузке серверов и всего кластера целиком. И все это реализуется достаточно просто :) Итак, приступим:

  1. Так как среда обитания наших приложений - это Windows, то скачиваем отсюда или отсюда бинарники, скомпилированые для win32. Распаковываем его куда-нибудь и запустим единственный exe-файл. Сервер memcached запущен :) Весь процесс настройки производится через параметры запуска. Ознакомиться с ними можно запустив сервер с параметром -h.
  2. Теперь нам нужно чем-то подключаться к этому сервису. И в качестве клиента предлагается использовать enyim.com Memcached Client, и его можно использовать уже в составе готовых Memcached Providers, что максимально упрощает использование memcached. Скачав и разархивировав билиотеку куда-нибудь, добавляем в наш проект ссылки на файлы Enyim.Caching.dll, MemcachedProviders.dll и log4net.dll.
  3. В коде приложения необходимо найти все классы, экземпляры которых помещаются в сессию и отметить их атрибутом Serializable.
  4. Осталось дело за малым - сконфигурировать наше приложение должным образом:
    • В разделе configSections регистрируем секции:
      <sectionGroup name="enyim.com">
      <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" />
      </sectionGroup>
      <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
    • Добавляем секцию настройке клиента memcached:
      <enyim.com>
      <memcached>
        <servers>
          <add address="127.0.0.1" port="11211" />
        </servers>
        <socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00" />
      </memcached>
      </enyim.com>
      Ключевым элементом здесь является секция servers, в которой перечисляются все memcached серверы, обслуживающие приложение. Самым распространенным вариантом использования является установка экземпляра memcached на каждый из серверов, на котором работает приложение.
    • А теперь настраиваем работу сессий:
      <sessionState cookieless="true" regenerateExpiredSessionId="true" mode="Custom" customProvider="MemcachedSessionProvider">
      <providers>
        <add name="MemcachedSessionProvider" type="MemcachedProviders.Session.SessionStateProvider, MemcachedProviders"
              connectionStringName="SqlSessionServices"
              dbType="SQL"
              writeExceptionsToEventLog="false" />
      </providers>
      </sessionState>
      Интересной здесь является строка dbType="SQL" - она указывает, какого типа СУБД будет использоваться для бэкапов данных сессии. Для отключения режима бэкапа данных нужно просто указать dbType="None".
    • Заключительным этапом будет настройка параметров подключения к базе данных для бэкапов. Для этого в секцию connecitonStrings нужно добавить такую запись:
      <add name="SqlSessionServices" connectionString="..."/>
      (троеточие следует заменить на реальную строку подключения).
    • Опционально можно добавить найстройки для log4net.

Осталось запустить приложение.

При обращении к сессии, провайдер обращается к клиенту memcached, который в свою очередь посылает асинхронные запросы на данные всем memcached-серверам, используюя идентификатор сессии, как ключ к данным. И вот здесь происходит самое инетресное: если это не первый запрос к сессии и какие-то данные там уже размещены, но предыдущий запрос обслуживал другой сервер, то клиент, а за ним и провайдер получат данные от другого сервера. А если же предыдущий запрос обслуживался текущим сервером, то он получит данные от от процесса, работающего на этом же сервере, чтобы будет гораздо быстрее, чем если бы он получал их по сети. Таким образом, в случае, если нагрузка равномерно распределяется между серверами, то гарантированное попадание в локальный сервис будет примерно в каждом втором случае. А если же сюда добавить условие, что как браузеры, так и сетевое оборудование оптимизирует работу с соединениями, "придерживая" их в пулле и используя так называемые sticky connections, то количество попаданий будет стремиться к 100%.

В случае, если ни один из сервисов не вернет данных по запросу, провайдер обратится в базу данных. Этот же сценарий будет срабатывать в случае, если ни один из memcached-сервисов не будет запущен. Можно сказать, что в этом случае сессии будут работать так же, как в режиме SQLServer.

При записи данных в сессию они в первую очередь будут сохранены в памяти первого сервиса в списке и в фоновом режиме будут сохранены в базе данных. Таким образом, если в какой-то момент времени весь кластер целиком будет перезагружен, то все клиентские сессии будут подтянуты из бэкапа и пользователи смогут работать, как ни в чем не бывло.

Практической выгодой от использования этого сервиса является то, что теперь сессией можно пользоваться без оглядки на то, что приложения будут перегружены и все данные пропадут, или же, что это будет ударом по производительности. В качестве типичного варианта использования можно предложить хранить в сессии данные по корзине в инетрнет-магазине.

Недостатоком этой конфигурации является то, что она не работает в случае, когда не доступен SQL Server. Если указать режим бэкапа dbType="None", то в случае недоступности memcached-сервисов будет использоваться стандартный режим работы сессий, когда все данные хранятся в памяти рабочего процесса. Очень хотелось бы, чтобы провайдер стал умнее и стал использовать стандартный режим в случае, если оба хранилища не доступны. Но код провайдера доступен, логика его работы более чем проста и добавление этой функциональности не является проблемой.


скачать пример