понедельник, 17 декабря 2007 г.

IIS 7.0 и Custom Errors

Разработчики, начавшие использовать IIS 7.0, могут столкнуться с интересной проблемой, которая не была актуальна для систем, работающих на базе IIS 6.0. Суть проблемы заключается в том, что если для приложения разработан свой обработчик ошибок, обрабатывающий разные типы исключений и выполняющий редирект на разные страницы, то этот редирект будет проигнорирован и пользователь увидит стандартный "желтый экран" IIS.

Решение же проблемы оказалось куда банальнее, чем сама проблема :)

Исследования выявили интересное нововведение в классе HttpRequest, которое появилось в версиях .NET 3.5, 3.0 SP1 и 2.0 SP1 - свойство TrySkipIisCustomErrors. Оно указывает IIS7, игнорировать ли стандартный обработчик ошибок, разрешая пользовательскую обработку, или обрабатывать ошибки сугубо стандартными средствами. При этом, "ручное" выставление этого свойства ни к чему не приводит. По все видимости, это свойство играет сугубо сервисную роль и используется самой инфраструктурой ASP.NET. Да и завязка на наличие SP у .NET как-то не очень вселяет оптимизм.

Впрочем, все решилось достаточно изящно. В документации к этому свойству нашлось:

The TrySkipIisCustomErrors property is used only when your application is hosted in IIS 7.0. When running in Classic mode in IIS 7.0 the TrySkipIisCustomErrors property default value is true. When running in Integrated mode, the TrySkipIisCustomErrors property default value is false.

Дейтсвительно, стоило для приложения указать использование Classic Pool вместо Default Pool, и все заработало точно так же, как на старом добром IIS 6.0 :)

четверг, 26 апреля 2007 г.

Метапрограммирование в Nemerle.

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

Язык программирования Nermele во многом известен именно благодаря своей мощной системе метапрограммирования, которая достаточно органично вписывается в мир .NET, позволяя использовать язык со всеми его возможностями уже "здесь и сейчас". Язык предоставляет возможность разработки расширений к процессу компиляции с помощью максросов, написанных на этом же языке и подключаемых к нужному проекту как обычная библиотека.

Для того, чтобы опробовать все эти возможности, достаточно установить последний билд компилятора и интеграции с Visual Studio. Скачать это можно с сайта проекта интеграции. Установив Nemerle и интеграцию, в студии вам будет доступна новая группа проектов.

Пример, который мы рассмотрим будет достаточно простым и хорошо всем занкомым. Предположим, вы смоделировали предметную область и подошло время воплотить эту модель в коде. Каждая сущность этой модели будет представленная в виде неких персистентных классов. Кроме логики домена вам необходимо реализизовать т.н. логику приложения, которая включает в себя механизмы ORM, Concurrency и Audit Trail. Логика приложения привносит в каждый класс вашей модели свой специфичный набор полей:

  • Каждый класс должен иметь поле идентификатора. Конечно, эта задача решается путем наследования от некого супер-класса. Но наследование может сыграть злую роль в задачах передачи данных за границы приложения. Например, в случаях какой-то экзотической сериализации. В случае распределенного приложения вообще лучше всего иметь классы с минимальной детализацией для передачи между подсистемами. Поэтому идеальным случаем было бы добавление поля идентификатора к каждому классу в домене;
  • В задаче совместнго доступа нам нужно поле с временной меткой записи, по которой мы будем определять перед каждой операцией записи, была ли сущность изменена кем-то еще;
  • Для простейшей системы аудита нам необходимо добавить поля, в которых будет хранится информация о том, кто запись создал и кто ее в последний раз изменял.

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

Пусть мы имеем следущий класс:

public class Enumeration
{
    mutable value : string;
    mutable description : string;
}

Его аналогом в C# был бы следующий код:

public class Enumeration
{
    string value;
    string description;
}

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

public class Enumeration
{
    [Accessor (flags = WantSetter)]
    mutable value : string;
    [Accessor (flags = WantSetter)]
    mutable description : string;
}

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

public class Enumeration
{
    // Fields
    private string description;
    private string value;

    // Properties
    public string Description
    {
        [DebuggerStepThrough]
        get
        {
            return this.description;
        }
        [DebuggerStepThrough]
        set
        {
            this.description = value;
        }
    }

    public string Value
    {
        [DebuggerStepThrough]
        get
        {
            return this.value;
        }
        [DebuggerStepThrough]
        set
        {
            this.value = value;
        }
    }
}

Но вернемся к нашим полям. С точки зрения разработчика было бы удобвно наделить класс Enumeration всем необходимым свойствами с помощью похожего макроса-атрибута:

[PersistentObject]
public class Enumeration
{ ....

И выглядеть этот макрос будет следующим образом (для краткости мы будем добавлять только идентификатор):

using Nemerle;
using Nemerle.Utility;

[MacroUsage (MacroPhase.WithTypedMembers, MacroTargets.Class)]
macro PersistentObject (t : TypeBuilder)
{
    t.Define(<[ decl: 
        [Accessor]
        mutable id : Guid;
    ]>);
}

Как видите, ничем сложным разработка макросов не является. Изучим этот пример детальнее.

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

Обязательным первым параметром макросов, которые применяются как атрибуты к классам является переменная типа TypeBuilder. Эта переменная указывает на объект, умеющий манипулировать целевым типом. Если бы наш макрос применялся к полям, то первый параметр был бы типа FieldBuilder.

Единственной операцией в нашем макросе является вызов методв Define у переменной типа TypeBuilder. Этот метод принимает в качестве параметра экземпляр синтаксического дерева, соответствующего коду, который мы хотим добавить в наш класс. Мы можем, конечно же создать это экземпляр вручную, добавляя все синтаксические элементы, как узлы этого дерева. Но Nemerle предоставляет нам возможность избежать этого, позволяя нам описать не код, строящий дерево, а код, который и должен быть представлен в виде дерева. Эта возможность называется "цитированием". Для цитирования используется подобная конструкция:

def ast : PExpr = 
<[ 
    /* Код, который будет представлен в виде дерева */ 
]>

Обратите внимание, что мы внутри макроса использовали другой макрс, а именно, известный нам Accessor. Таким образом класс  Enumeration будет расширен приватным полем и свойствам к нему.

Забегая вперед, хочется отметить, что возможности макросистемы Nemerle позволяют добавлять новые классы в сборки, основанные на метаданных. Например, мы можем расширить наш макрос PersistentObject так, чтобы он для каждого класса генерировал все необходимые классы DAO и даже подготавливал базу данных. И все это на этапе компиляции!...

понедельник, 12 марта 2007 г.

Выходные с Nemerle

Наконец-то дошли руки до популярного ныне в своих кругах языка программирования Nemerle. За развитием этого языка я слежу давно, наблющаю за активным обсуждением этого языка на сайте RSDN. Пробовал писать на этом языке какие-то вещи, но дальше примеров дело не заходило. На сей раз к этому языку меня подтолкнуло любопытство, связанное с парадигомой функционального программирования. Как практику, мне необходимо было выбрать язык программирования для своих экспериментов, а Nemerle изначально был заявлен, как поддерживающий функциональное программирование, да и один из его предков язык ML функционален по определению. Ну и хотелось иметь язык, который нативно впишется в среду .NET. Ну и его "гибридность" теоретически может позволить "быстрый ввод" и легкое "переключение" между парадигмами в зависимости от решаемых задач. Кроме того, система метапрограммирования, реализованная в этом языке, делают его настоящим комбайном, выразительные способности которого ограничиваются исключительно фантазией пишущих на нем. К слову стоит отметить, что большая часть исходников Nemerle написана на самом Nemerle.

Итак, в качестве первого "функционального" примера я решил попробовать реализовать функцию для быстрой сортировки. Этот пример очень показателен для функционального программирования, так как очень здорово иллюстрирует идею о том, что функциональные программы близки внешне к описанию формулы в математической нотации, описывающей результат, чем к пошаговому описанию алгоритма, приводящего к заданному результату в случае императивного программирования. Чтобы проверить эту идею, в принципе, достаточно ознакомится с реализацией этого алгоритма на разных языках в этой статье. Сравните реализацию на С и на Haskell. Впечатляет?
Итак, в общем случае метод сортировки описывается следующей формулой:
   quickSort([]) = []
 qs

Нам ничего не остается, кроме как просто описать эту формулу в несколько иной нотации, а другими словами - реализовать на языке Nemerle:
def qs(l)
{
    match(l)
    {
        | [] => [];
        | x :: xs  => qs( $[ y | y in xs, y < x ] ) + [x] + qs( $[y | y in xs, y >= x] );
    }
}
Неправда ли, очень похоже на исходную формулу?
Ну и пример использования:
System.Console.WriteLine(qs([1, 9, 6, 96, 9, 6, 69, 698, 98, 69, 89, -228, -234, -27, -23, -23, 63407075, 003, 069, 036, 306, 3096]));
Результатом сортировки будет:
[-234, -228, -27, -23, -23, 1, 3, 6, 6, 9, 9, 36, 69, 69, 69, 89, 96, 98, 306, 698, 3096, 63407075]
Ура, первый эксперимент удался! Будем продолжать наши опыты и результат сразу же опубликовывать.
В качестве источника научной информации в данном эксперементе использовался цикл лекций "Основы функционального программирвоания".

Создание собственного дистрибутива Microsoft SQL Server Express

Все разработчики desktop-приложений, работающих с базами данных, всегда сталкивались с проблемами установки этой самой базы данных на машины пользователей. Сценариев, которые можно реализовать для решения данной задачи достаточно много:
  • Использование файловых баз данных, таких, как Microsoft Access, SQLite, Paradox - разворачивание базы данных представляет собой обычное копирование неких эталонных файлов в какое-нибудь общедоступное место;
  • Использование существующих серверов баз данных - в этом случае нужно либо в момент установки, либо в момент запуска приложения настроить соединение, сохранить параметры и создать структуру базы данных;
  • Установка собственных серверов баз данных вместе с приложением - этот вариант я и рассмотрю ниже.

В качестве сервера, который мы будем "встраивать" в наше приложение мы выберем Microsoft SQL Server Express. По функциональным возможностям эта редакция сервера аналогична SQL Server 2005 Standard Edition, с некоторыми ограничениями по размеру базы данных, количеству используемой памяти и процессоров. Но для небольших приложений всех этих возможностей будет более чем достаточно. Нужно отметить, что эта редакция намного удобнее в использовании предыдущей версии MSDE 2000.
Рассмотрим следующий сценарий развёртывания:
  1. Пользователь устанавливает приложение самым обычным образом (пусть даже просто вручную копирует бинарные файлы);
  2. При первом запуске приложение проверяет наличие сконфигурированной базы данных и, если таковая не обнаруживается, то пользователю предлагается установить и настроить сервер баз данных;
  3. Если пользователь соглашается, то запускается процесс установки сервера баз данных с заданными параметрами безопасности, названием экземпляра сервера и паролем для пользователя sa; при этом процесс установки должен быть максимально скрыт от пользователя для того, чтобы не нагружать его лишней информацией о приложении;
  4. На только что установленном сервере создаем базу данных, выполнив установочный DDL-скрипт.
Способы определения наличия уже установленной базы данных и последующее создание новой базы мы отложим и рассмотрим подробнее третий пункт нашего сценария.
Для осуществления нашего плана мы должны иметь в наличии некий исполняемый файл, запустив который из нашего приложения мы и получим по завершению установленный экземпляр SQL Server с заданными параметрами.
Итак, рассмотрим процесс по шагам.
  1. Скачиваем дистрибутив с сайта Microsoft SQL Server Express.
  2. Распаковываем установочные файлы.
    На этом сайте доступны версии, упакованные в некий файл вида SQLEXPR32_RUS.EXE, а нам нужно получить доступ непосредственно к файлу Setup.exe, который можно будет запустить с заданными параметрами. В этом месте мы должны выполнить фокус: дело в том, что перво-наперво скачанный инсталятор просто распаковывает содержимое инсталляционного пакета во временную директорию. Наша задача эти файлы оттуда перехватить. Задача это достаточно проста: после запуска инсталляции этот путь можно получить из такого окошка: 

    Распаковка файлов

    После того, как запустится непосредственно сам инсталлятор из архива, копируем содержимое этот папки, потому как после завершения работы инсталлятора - эта папка будет удалена.
  3. Конфигурация инсталлятора.
    В директории с инсталлятором вы найдете файл template.ini, в котором можно прописать все необходимые параметры установки по умолчанию. Дело в том, что правильно сделанный инсталятор должен уметь принимать все параметры установки из командной строки для того, чтобы всю эту установку автоматизировать. Помогает это в тех случаях, когда, например, от конечного пользователя нужно спрятать всю рутину, связанную с этим процессом, доверившись параметрам по умолчанию. Ведь даже опытные пользователи в 90% случаев установки в основном нажимают кнопку Next, не изменяя параметров по умолчанию.
    В этом файле можно указать все параметры, которые запрашиваются в диалогах. А для того, чтобы запустить установку с этими параметрами нужно указать инсталлятору, откуда брать эти параметры. Это делается с помощью такого запуска:

    setup.exe /qb /settings c:\template.ini

    • Ключ /qb указывает инсталлятору, что нужно принять все параметры и не дать пользователю их изменить через всякие диалоги. Таким образом пользователь будет отслеживать процесс установки, но не сможет ничего изменить. Альтернативный этому ключ /qn указывает инсталлятору, что процесс установки нужно проводить полностью в фоновом режиме.
    • Ключ /settings указывает, что параметры установки следует взять из указанного файла.
    Альтернативой этому способу является указание всех параметр прямо в командной строке:

    setup.exe /qn ADDLOCAL=SQL_Engine SAPWD=password SECURITYMODE=SQL

    Таким образом можно установить экземпляр сервера по умолчанию с заданным паролем для пользователя sa в совершенно невидимом режиме. Завершающем штрихом в этом пункте будет создание некого Install.bat со строкой запуска установки с заданными параметрами. Но мы на этом не остановимся.
  4. Выбор архиватора.
    Если вы помните, скачанный дистрибутив был представлен в виде исполняемого файла SQLEXPR32_RUS.EXE, размер которого был во много раз меньше развёрнутого инсталлятора. Наша задача - получить в результате нечто подобное, с той лишь разницей, что запуск нашего дистрибутива сразу начнёт установку с заданными параметрами. Опустив сравнительный анализ разных архиваторов, остановимся на замечательном архиваторе 7-zip. Для нашей задачи он показался мне наиболее подходящим.
    Следующим шагом будет создание архива максимальной степени сжатия примерно с такими параметрами: 

    Добавить в архив

    Размер архива получился около 36 мегабайт, что очень близко к оригинальному дистрибутиву.
  5. Создание SFX-архива.
    В предыдущем пункте мы сознательно не создали SFX-архив сразу, потому что нам нужна не просто возможность распаковать архив на произвольной машине, а распаковать в директорию по умолчанию и запустить после установки на выполнение установку с заданными параметрами. Все эти задачи делаются легко и просто с помощью существующих SFX-модулей. Детали того, как это делается с помощью стандартных средств 7-zip можно найти по этому адресу. Но мы воспользуемся альтернативным модулем, найти который можно по этому адресу. 
    Из скачанного пакета альтернативных модулей выбираем файл 7ZSD_LZMA.sfx и помещаем его рядом с архивом, который мы хотим "оживить". Если у вас есть желание, то можно с помощью утилиты ResourceHacker и этой заметки поменять иконку у SFX модуля на Иконка, предварительно извлечённую с помощью все того же ResourceHacker-а из инсталлятора Microsoft SQL Server Express. Затем размещаем в этой же директории текстовый файл Config.txt такого содержимого:

    ;!@Install@!UTF-8!
    RunProgram="setup.exe /qn ADDLOCAL=SQL_Engine SAPWD=password SECURITYMODE=SQL"
    GUIMode="2"
    ;!@InstallEnd@!


    После чего в командной строке выполняем следующую команду:

    copy /b 7ZSD_LZMA.sfx + config.txt + SQL2005E.7z SQL2005E.exe

    Файл SQL2005E.exe, полученный в результате этой операции и будет нашим дистрибутивом, который можно распространять со своим приложением.
  6. Интеграция в приложение.
    Этот пункт, пожалуй, самый простой. В зависимости от того, каким образом вы собираетесь устанавливать Microsoft SQL Server Express (в момент установки приложения или после проверки при каждом запуске) и будет проходить интеграция. Это может быть либо Custom Action для Windows Installer, либо в коде вашего приложения будет присутствовать подобный код:
    ProcessStartInfo psi = new ProcessStartInfo();
    psi.FileName = filePath; 
    Process p = new Process();
    p.StartInfo = psi;
    p.Start();
    p.WaitForExit(); 
    if (p.ExitCode != 0)
       throw new ApplicationException("Ошибка при установке");
Более подробную информацию о встраивании Microsoft SQL Server Express в свои приложения можно найти в статье Embedding SQL Server Express into Custom Applications.