2012-01-03

VerifyArgs: Проверка параметров методов

Несколько недель назад, работая над очередным .NET-проектом, я обратил внимание на повсеместно используемый класс для простых проверок параметров методов (например, проверок на null, пустую строку/коллекцию и т.д.). Код проверки выглядел примерно так (тут CheckUtil — статический класс со вспомогательными методами для проверок):

Этот код проверяет "str" на null/пустоту и в случае ошибки выбрасывает исключение с информацией об имени ошибочного параметра. Мне в подобном коде не нравится тот факт, что имя параметра приходится передавать отдельно — это неудобно, засоряет код, плюс рефакторинг усложняется (при переименовании параметров метода нужно не забывать менять значения в строках). А встречаются такие утилитарные классы почти в каждом проекте. Поэтому мне в голову пришла идея написать библиотеку для проверок параметров методов, в которой не нужно было бы явно передавать имена параметров.

Моя идея заключалась в использовании анонимных объектов для передачи значений параметров методов вместе с их именами. Например, код "new { str }" создаст новый объект анонимного типа со свойством "str", которое вернёт значение параметра "str"; впоследствии из этого объекта можно получить как имя параметра, так и его значение для проверки.

Библиотека VerifyArgs использует эту идею для проверки значений параметров методов. Вот пример кода, использующего VerifyArgs:

Кроме того, что VerifyArgs не требует явной передачи имён проверяемых параметров, она также позволяет выполнять проверки сразу для нескольких параметров методов и использовать цепочки вызовов для выполнения нескольких проверок сразу. Работает библиотека быстро, потому что вместо reflection использует кодогенерацию во время выполнения.

Скачать VerifyArgs можно используя NuGet или со страницы на CodePlex (там же можно найти подробную информацию о библиотеке).

2011-12-22

Расширение возможностей LINQ to Objects

В проектах, которые активно используют LINQ вообще и LINQ to Objects в частности (т.е. практически во всех проектах на .NET 3.5+), часто можно увидеть подобный код:

Такие самописные методы-расширения появляются потому, что в стандартной поставке LINQ to Objects (System.Linq) недостаточно методов для решения всех типовых задач, и ForEach() — только один из примеров. Другой пример — методы получения элемента, в котором заданный член минимален/максимален (стандартные методы Min()/Max() возвращают сам минимальный/максимальный элемент).

Как результат, многие проекты содержат один и тот же набор велосипедов-расширений для LINQ. Естественно предположить, что раз многим нужен ForEach() для IEnumerable, то где-то должна быть библиотека, в котором этот и другие методы уже реализованы. Я нашёл несколько таких библиотек:

  • MoreLINQ — содержит только методы-расширения для IEnumerable общего назначения. Есть в NuGet.
  • LinqLib — содержит также методы для арифметических операций (например, сложить элементы двух последовательностей и получить третью) и методы для работы с числами Фибоначчи/простыми числами. Есть в NuGet.
  • Cadenza — в ней намешано много всего, в том числе и расширения для IEnumerable.

Все эти библиотеки имеют схожую функциональность для работы с IEnumerable (например, во всех из них есть метод-расширение ForEach), так что выделить какую-то одну из них тяжело. Лично мне интерфейс библиотеки MoreLINQ показался достаточно грамотно спроектированным (ничего лишнего), плюс она есть в NuGet; в общем случае выбор придётся делать, отталкиваясь от нужд конкретного проекта.

2011-12-14

Expression Trees: Трансформация переменных в константы

Работая над кешированием LINQ to Entities запросов (см. пост про protobuf-net) я столкнулся с проблемой вычисления уникального ключа запроса. В рамках этой проблемы необходимо было решить две задачи, связанные с обработкой LambdaExpression-ов в IQueryable, для которого генерируется ключ:

  1. Для одинаковых лямбда-выражений, зависящих от переменной с разными значениями, должны генерироваться разные ключи.
  2. Для разных лямбда-выражений, зависящих от переменных с одинаковыми значениями, должны генерироваться одинаковые ключи.

Эти задачи можно сформулировать в виде кода:

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

Этот код «отрезает» лямбда-выражение от контекста, в котором оно было создано, и превращает все замкнутые в выражении переменные в константы. Кроме генерации уникального ключа по выражению приведённый выше код можно, например, использовать в своём Query Provider-е для примитивной нормализации лямбда-выражений и получения значений всех параметров запроса.

2011-12-12

protobuf-net на практике

Не так давно я участвовал в оптимизации работы одного ASP.NET MVC веб-приложения. В частности, занимался улучшением производительности подсистемы кеширования. Сам проект использует Entity Framework 4.1, а означенная подсистема позволяет кешировать результаты отдельных LINQ to Entities запросов в memcached. Результат запроса ложится в кеш как массив сущностей (если точнее - массив экземпляров EF proxies) в сериализованном виде, при взятии из кеша его надо, соответственно, десериализовать.

Для (де)сериализации в приложении использовался старый добрый BinaryFormatter, производительность которого оставляла желать лучшего. Вообще говоря, приложение агрессивно использует кеш, поэтому медлительность BinaryFormatter-а была весьма заметна. Захотелось использовать более производительную библиотеку сериализации, желательно еще и генерирующую footprint поменьше. Да еще и такую, чтобы прикрутить было попроще. Поиск дал результаты, советующие взглянуть на protobuf-net, и в итоге мы стали использовать эту библиотеку в нашем проекте.

Краткое описание

protobuf-net это (наиболее живая) .NET реализация Protocol Buffers — кросс-платформенного протокола для обмена двоичными сообщениями от Google. Одна из особенностей этого протокола - уменьшение размера результирующего двоичного потока за счёт уменьшения избыточности кодирования (сжатия там нет, но вот трюки вроде Base 128 Varints есть) и уменьшения количества сериализуемой информации о типах (в контракте сериализуемого типа жёстко задаётся порядок полей, так что хранить имя поля и его тип нет необходимости).

В теории получается, что protobuf-net генерирует сериализованный поток меньшего размера и, значит, может (де)сериализовать быстрее, чем BinaryFormatter. Но для сериализации типа через protobuf-net контракт этого типа должен быть предварительно зарегистрирован — сериализовать любой [Serializable] класс «из коробки» protobuf-net не умеет.

Практика использования

protobuf-net можно или скачать с официального сайта, или поставить через NuGet. Классический пример использования protobuf-net выглядит примерно так:

У этого кода есть две особенности, которые могут сделать переход на protobuf-net неприятным:

  1. Для сериализуемого типа явно (с помощью атрибутов) указан контракт — какие поля/свойства необходимо сериализовать и в каком порядке. К сожалению, в существующем коде может быть много сериализуемых типов, и прописывать новые атрибуты для всех из них может быть тяжело/неудобно/невозможно.
  2. Необходимо явно указывать десериализуемый тип. Проблема тут в том, что на практике в момент десериализации он не всегда известен.

Добавления [Proto*] атрибутов к существующим классам можно избежать, задав контракт во внешнем .proto файле, используя атрибуты [DataContract]/[DataMember] или задавая контракт в рантайме через классы TypeModel/RuntimeTypeModel. Для существующих классов последний способ может оказаться наиболее полезным. Вот пример кода, который создаёт контракты для всех сериализуемых типов из текущей сборки:

Конечно, это только пример, и этот код придётся адаптировать под свои нужды — например, сериализовать все public properties или выбирать типы по пространству имён. А вот для проблемы десериализации неизвестного типа есть «официальное» решение — сначала сериализовать имя типа, а потом уже сам объект; соответствующий код можно подсмотреть в файле ProtoTranscoder.cs из SVN-репозитория protobuf-net.

Производительность

Для нашего проекта использование protobuf-net улучшило производительность (де)сериализации примерно в 2.5-3 раза (уменьшение размера сериализованного потока было для нас не так важно) — очень хороший результат, учитывая малые затраты на изменение кода. Вообще в интернетах есть несколько синтетических тестов с участием protobuf-net.

Интереса ради я написал и свой тест для сравнения protobuf-net с BinaryFormatter, код которого можно забрать на GitHub. В моём тесте:

  • Сериализуется граф из 90 объектов (сущности 3-х типов).
  • Граф содержит циклические ссылки.
  • Для protobuf-net сериализуется информация о корневом типе.

Результаты теста, скомпилированного в Release, на моей машине:

Особенности и подводные камни

Используя protobuf-net, надо помнить о некоторых особенностях Protocol Buffers:

  • Protocol Buffers разработан для сериализации деревьев объектов т.е. не поддерживает циклические ссылки и при сериализации сохраняет объект, на который есть ссылки из нескольких полей, как несколько независимых объектов. protobuf-net позволяет обходить эти ограничения и хранить объекты по ссылке, для этого нужно указать AsReference = true в метаданных сериализуемого поля.
  • Protocol Buffers должен заранее знать о типах всех сериализуемых объектов и наследованиях между ними — эта информация должна явно указываться в контракте. Опять же, protobuf-net позволяет включить сериализацию информации о типе для поля, если в метаданных поля указать DynamicType = true.
  • protobuf-net специальным образом обрабатывает прокси-типы, генерируемые NHibernate и Entity Framework-ом — если контракт прокси-типа не зарегистрирован, используется контракт его базового типа.
  • Можно контролировать информацию о типе, сериализуемую для DynamicType полей, обрабатывая событие TypeModel.DynamicTypeFormatting. Например, в проекте, над которым я работал, этот обработчик использовался для регистрации контрактов неизвестных типов и сериализации EF прокси-объектов как объектов базового типа.

Это то, с чем я лично столкнулся, наверняка есть и другие детали работы библиотеки. Кроме того, в protobuf-net есть и баги — например, пустой массив может десериализоваться как null (я специальным образом обрабатываю этот случай в коде).

С документацией по protobuf-net дела обстоят не супер — хороших статей я не нашёл, пользовался такими источниками информации:

И, конечно, очень помог код, потому что open source.