Alexey Suvorov dev blog

Мой разработческий блог

Archive for the ‘C#’ Category

Type с null в FullName

leave a comment »

При попытке получить тип generic аргумента метода в не generic классе или generic классе непараметризованного каким либо типом-параметром, FullName результата будет null. При этом GetHashCode и Equals не будут корректно работать по сравнению с типом который можно получить через typeof для open generic. Пример:

// то же для Generic<T>
public class NotGeneric
{
    public void GenericMethod<T>(IEnumerable<T> seq){}
}

var m = typeof(NotGeneric).GetMethod("GenericMethod")
var t = m.GetParameters()[0].ParameterType;
Assert.Null(t.FullName);

Также можно видеть следующие лично для меня неожиданные эффекты:

// то же для Generic<T>
public class NotGeneric
{
    public void GenericMethod<T>(IEnumerable<T> seq){}
}

var m = typeof(NotGeneric).GetMethod("GenericMethod")
var t = m.GetParameters()[0].ParameterType;

Assert.False(t == typeof(IEnumerable<>));
Assert.False(t.GetHashCode() == typeof(IEnumerable<>).GetHashCode());

На самом деле generic уже параметризован типом с пустым GUID. Знания данной детали в принципе достаточно чтобы отличить все типы перегрузок generic методов и вызвать корректный.

// то же для Generic<T>
public class NotGeneric
{
    public void GenericMethod<T>(IEnumerable<T> seq){}
}

var m = typeof(NotGeneric).GetMethod("GenericMethod")
var t = m.GetParameters()[0].ParameterType;

Assert.True(t.GetGenericArguments()[0].GUID == Guid.Empty);

Written by alexeysuvorov

15.12.2014 at 6:28 пп

Опубликовано в .net, C#

Детали реализации стека — часть первая (перевод)

2 комментария

В последнее время мне снова выпала возможность провести несколько собеседований и часто на вопрос «что такое значимые и ссылочные типы» я слышал «значимые типы — это типы, экземпляры которых располагаются на стеке, а ссылочные — это типы, экземпляры которых располагаются в куче». Так что сегодня я решил сделать перевод очень старой, но не потерявшей свою актуальность статье Eric-а Lippert-а. Оригинал статьи можно найти тут. Я постарался сделать перевод как можно более читаемым и лёгким для восприятия на Русском языке, так что он существенно отличается от оригинала по форме, но не по смыслу.

Какое-то время назад я писал о том, что «ссылки» — это не «адреса», когда речь идёт о C# и размещении его объектов в памяти.Хотя это действительно так, но это всего лишь деталь реализации, но не смысл «ссылки». Другая деталь реализации, которую часто путают с сутью — это то, что «память под значимые типы (value types) выделяется на стеке». Я часто это вижу, потому что именно так написано в нашей документации.

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

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

К сожалению документация не сфокусирована на наиболее значимых характеристиках, но сфокусирована на деталях реализации и упускает суть значимых типов. Я бы очень хотел, чтобы все те статьи, которые объясняют, «что такое стек» вместо этого объясняли бы что такое «копирование по значению» и как непонимание этого механизма может стать причиной ошибок.

Утверждение о том, что значимые типы располагаются на стеке в общем случае не верно. В документации на MSDN правильно замечено, что значимые типы располагаются на стеке иногда. Например, поле типа int в ссылочном типе — это часть объекта этого типа и, как и весь объект его поле расположено в куче. Та же история с локальными переменными, которые попадают в замыкание анонимных методов (*), потому что они по сути становятся полями скрытого класса и тоже располагаются в куче, так что локальные переменные могут располагаться в куче даже если они значимого типа.

Короче говоря, мы имеем объяснение, которое ничего не объясняет. Отбросив соображения производительности что ещё, с точки зрения ожиданий разработчика может заставить CLRjitter разместить переменную типа int на стеке, а не в куче? Ничего, пока не нарушается спецификация система может выбирать наиболее эффективную стратегию генерирования кода.

Ага, никто не обещал, что операционная система поверх которой реализован CLI предоставляет массив размером в 1 мегабайт под названием «стек». Windows обычно делает это и этот одно мегабайтный массив отличное место для того, чтобы эффективно хранить небольшие объекты с коротким временем жизни, но нет никаких требований или гарантий, что операционная система предоставляет такого рода структуру или что jitter будет её использовать. В принципе Jitter может решить создавать все локальные переменные в куче не смотря на потерю производительности, это будет работать пока семантические требования к значимым типам выполняются.

Ещё хуже думать, что значимые типы «быстрые и маленькие» а ссылочные «большие и медленные». Действительно, значимые типы могут быть сгенерированные jitter-ом в код на стеке, который очень быстр как при выделении, так и при очистке памяти. Но при этом большие структуры, создаваемые на куче, такие как массив элементов значимого типа, тоже создаются очень быстро при условии, что они инициализируются значениями по умолчанию. И ссылочные типы занимают дополнительное место в памяти. И конечно существуют такие условия, когда значимые типа дают большой выигрыш в производительности. Но в подавляющем большинстве программ то, как локальные переменные создаются и уничтожаются не может быть узким местом с точки зрения производительности.

Нано оптимизации по превращению ссылочных типов в значимые дающие несколько наносекунд выигрыша не стоят того. Это нужно делать только если данные профайлера показали, что существуют настоящие проблемы которые могут быть решены использованием значимых типов вместо ссылочных. У меня нет таких данных. При выборе использовать ссылочный или значимый тип я всегда руководствуюсь тем как тип, который я создаю должен вести себя семантически.

(*) справедливо и для блока итератора.

Written by alexeysuvorov

27.04.2014 at 1:33 пп

Опубликовано в .net, C#

Fody — обзор плагинов

leave a comment »

Итак, Fody — инструмент для постобработки .net сборок с целью внедрения в них своего IL кода через Cecil.Emit. Fody представляет из себя msbuild таску которая имеет конфигурационный файл в каждом из проектов, для которых будет запущена. Вот такая строка будет добавлена в каждый ProjectName.project файл

<Import Project="Fody.targets" />

Всю работу по внедрению IL делают Fody плагины, перечисленные в конфигурационном файле.

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
  <SuperPluginName />
</Weavers>

Максимально ничего не делающий плагин можно себе представить, как:

public class ModuleWeaver {
    // Можно писать в MSBuildLog
    public Action<string> LogInfo { get; set; }
    // Экземпляр Mono.Cecil.ModuleDefinition для текущей сборки
    public ModuleDefinition ModuleDefinition { get; set; }
    public void Execute() {
    }
}

Если возникает вопрос зачем Fody вообще нужен, то скажу лишь что это очень удобно. Он инсталлируется через nuget и написав свой плагин можно просто указать его как зависимость. Главное понять, как Fody будет искать этот плагин и создать правильную структуру файлов и папок в packages.

Плагины

Сам по себе fody не имеет большого смысла, поэтому представляю обзор набора плагинов с официального сайта https://github.com/Fody/Fody. Описание каждого плагина очень краткое, но я надеюсь достаточное чтобы принять решение стоит ли тратить время на чтение документации. Я разбил их по собственным убеждениям в 3 группы. Полезные плагины — это те которыми я уже пользуюсь или вижу в них смысл и непротиворечивость. Вторая группа — откровенно бесполезные или нарушающие какие-либо принципы хорошего кода. Третья — группа плагинов для оценки полезности которых у меня не хватает компетенции или я сомневаюсь в оценке.

Полезные


AsyncErrorHandler
https://github.com/Fody/AsyncErrorHandler
Заключает async в try-catch блок в котором выполняет
AsyncErrorHandler.HandleException(exception);
Мы не пользуемся, но выглядит заманчиво, один из кандидатов на более плотное тестирование и внедрение при необходимости.

Costura
https://github.com/Fody/Costura/
Для фанатов ILMerge. Склеивает зависимые сборки в одну.

MethodCache
https://github.com/Dresel/MethodCache
Мемоизация методов помеченных аттрибутами. Реализация кеша оставлена нам, что конечно радует. С удовольствием посмотрю поближе и потестирую если возникнет подходящая задача.

MethodDecorator
https://github.com/Fody/MethodDecorator
Декорирует методы помеченные атрибутами. Позволяет поймать момент входа в метод, выхода из него и эексепшен если произошёл. Широко используем в проекте, правда не оригинальную, а мой форк https://github.com/alexeysuvorov/MethodDecorator, который я допиливал под нужды проекта и заодно заимплементил несколько пожеланий из обсуждения оригинального плагина. Умеет захватывать параметры передаваемые в метод. Я про него писал. Не стесняйтесь файлить в Issues на гитхабе.

Mixins.Fody
https://bitbucket.org/skwasiborski/mixins.fody/wiki/Home
Множественное наследование через примеси. Альтернатива Castle’s dynamic proxy mixins. Я не поборник морали, так что вполне себе допускаю что вернусь к этому плагину, как только .net загонит меня в необходимость наследоваться и от класса Framework и от моего класса. Действительно было такое один раз.

ModuleInit
https://github.com/Fody/ModuleInit
Собеседуя кандидатов я выяснил для себя то, что не все .net программисты в курсе, что сборка содержит в себе модули. Это плагин позволяет задать конструктор для модуля. Для тех, кто понимает.

NullGuard
https://github.com/Fody/NullGuard
Первый в очереди на использование. Генерирует проверки на null если явно неуказано что null возможен. По моему мнению такие вещи просто необходимы в языках, допускающих null.

Obsolete
https://github.com/Fody/Obsolete
Позволяет использовать Obsolete аттрибут эффективно. Можно задать версию с которой при обращении к Obsolete будут происходить ошибки компиляции и версию сборки начиная с которой она не скомпилируется с классом помеченным таким атрибутом. Очень удобно я думаю.

PropertyChanged
https://github.com/Fody/PropertyChanged
Бывший NotifyPropertyWeaver. Для тех, кто не знаком – этот плагин добавляет оповещение (вызов) PropertyChanged эвента для всех сеттеров пропертей объектов, унаследованных от INotifyPropertyChanged или помеченных атрибутов. Совершенно незаменимая вещь если вы хоть раз пытались писать под WPF и его подмножества. Строго рекомендую к использованию если у вас есть UI на WPF.

PropertyChanging
https://github.com/Fody/PropertyChanging
Тоже самое, но для интерфейса INotifyPropertyChanging.

Resourcer
https://github.com/Fody/Resourcer
Облегчает доступ к ресурсам сборки позволяя не писать AssemblyName. Отличный вариант для сборок с тестами. В очереди на применение в самое ближайшее время.

Validar
https://github.com/Fody/Validar
Повзоляет встроить реализацию IDataErrorInfo и INotifyDataErrorInfo в класс, реализующий INotifyPropertyCanged. Во все классы, помеченные атрибутом InjectValidation будет заинжекчена реализация IDataErrorInfo и INotifyDataErrorInfo из класса с именем ValidationTemplate конструктор которого принимает INotifyPropertyChanged. Если пишите под WPF – обязательно посмотрите, возможно это именно то, что нужно.

Не могу придумать где использовать/звисит от ситуации


Bix.Mixers
https://github.com/rileywhite/Bix.Mixers.Fody
Интеграция с BixMix. Сам BixMix похоже не так давно стартанул и информации очень мало, так что пожелаем парням удачи и пойдём дальше.

Catel.Fody
https://github.com/Catel/Catel.Fody
Делает DependencyProperty из обычных если класс унаследован от DataObjectBase или ViewModelBase. Как я понялл Catel позиционирует себя как фреймворк для бизнеса поверх .net framework. Если пользуетесь Catel, то наверное будет удобно.

Commander.Fody
https://github.com/DamianReeves/Commander.Fody
Реализует ICommenc для MVVM. В непонятном состоянии, документация не дописана.

ExtraConstraints
https://github.com/Fody/ExtraConstraints
Позволяет задать delegate, enum и struct ограничения на тип параметер. C# не поддерживает такие ограничения, но в IL они вполне себе валидны. Возможно появятся в следующих версиях С# http://stackoverflow.com/questions/1331739/enum-type-constraints-in-c-sharp/1331811#1331811.

Freezable
https://github.com/Fody/Freezable
Во все классы унаследованные от спец интерфейса IFreezable инжектится код, который бросается исключениями на проперти сеттерах после вызова метода Freeze. Мой проеденный f# с его immutability мозг не может себе представить сценарии когда это можно использовать вместо объектов где readonly проперти задаются в конструкторе и не имеют свойств, но возможно будет полезен.

Janitor
https://github.com/Fody/Janitor
Автоматически реализует IDisposable для всех классов унаследованных от IDisposable. Использует volatile int для определения что Dispose уже был вызван. Если Вы разделяете взгляд автора плагина на то, как должен быть реализовал IDisposable, то это неплохой плагин.

JetBrainsAnnotations
https://github.com/Fody/JetBrainsAnnotations
Jet Brains и тут подсуетился, молодцы чё.

MethodTimer
https://github.com/Fody/MethodTimer
Облегчённая версия MethodDecorator, только меряет время которое заняло выполнение метода. Гадит в Debug или в логгер, который Вы ему предоставите. Возможно полезен, хотя MethodDecorator на мой взгляд удобнее.

RemoveReference.Fody
https://github.com/icnocop/RemoveReference.Fody
Можно указать в атрибуте уровня сборки какие сборки вы хотите удалить из References после билда. Насколько я понял таким шаманством автор предлагает лечить вот этот баг https://connect.microsoft.com/VisualStudio/feedback/details/779370/vs2012-incorrectly-resolves-mscorlib-version-when-referencing-pcl-assembly. Он всё ещё открыт.

Stamp
https://github.com/Fody/Stamp
Добавляет в AssemblyVersionInformation хешик последнего git коммита. На вкус и цвет.

Stiletto
https://github.com/benjamin-bader/stiletto
Ещё один вариант IoC контейнера. Может похвастаться завидной всеядностью потому как работает там, где нет Reflection.Emit. Если пишете на Xamarin под iOS, то посмотрите в эту сторону, возможно это не даст совсем загрустить без IoC.

Бесполезные


Anotar
https://github.com/Fody/Anotar
Заменяет вызов одного метода на создание/резольвинг логгера и его вызов. Имеет несколько встроенных интеграций с основными логгинг системами. Полезность сомнительна, но тут на вкус и цвет.

AssertMessage
https://github.com/Fody/AssertMessage

Assert.AreEqual(expectedCustomer.Money, actualCustomer.Money); =>
Assert.AreEqual(expectedCustomer.Money, actualCustomer.Money, "Assert.AreEqual(expectedCustomer.Money, actualCustomer.Money);");

Почему бы и нет. Если Вы пользуетесь Assert — ами и не против подождать несколько лишних секунд при билде пока этот плагин дописывае за Вас строчки.

Caseless
https://github.com/Fody/Caseless
Для строк заменяе == на string.Equals с соответствующим StringComparison. Очень сомневаюсь что хочу чтобы за меня это делал IL инжектор.

FodyDependencyInjection
https://github.com/jorgehmv/FodyDependencyInjection
Очень странный плагин. Резольвит зависимости из одного из 3-х IoC контейнеров Ninject, Autofac или Spring.Net в свойства объектов минуя конструктор. Не стал разбираться что там и как внутри, потому что посыл изначально неправильный. Я считаю, что все зависимости должны быть объявлены как параметры конструктора, а иначе черт ногу сломит разбираться в коде.

EmptyConstructor
https://github.com/Fody/EmptyConstructor
Добавляет пустой конструктор к классу если он не определён. Сомнительно полезен.

EmptyStringGuard
https://github.com/thirkcircus/EmptyStringGuard
Переписыват сеттеры и публичные методы принимающие строки, добавляе проверку на то, что строка не пустая. Если строка может быть пустая, то её можно пометить аттрибутом AllowEmpty. Возможно некоторые найдут оплезным, но меня больше беспокоят null чем пустые строки.

EnableFaking.Fody
https://github.com/philippdolder/EnableFaking.Fody
Ещё один «антиплагин» (первый был FodyDependencyInjection), всем классам добавляет virtual для всех мемберов публичных классов (на самом деле там ряд условий). Предполагается что это удобно для тестирования и перед продакшеном это нужно выключать. Если нужно использовать этот плагин, значит с дизайном что-то не так.

Equals
https://github.com/Fody/Equals
Для помеченных объектов реализует методы Equals на основе свойств входящих в объект. Если у Вас куча DTO объектов и правила их сравнения примитивно просты, то возможно это то, что нужно.

Fielder
https://github.com/Fody/Fielder
Кровь кишки и расчленёнка — перефарширует все проеперти в филды.

InfoOf
https://github.com/Fody/InfoOf
Предоставляет methodof, propertyof и fieldof, но конечно никакой магии и всё нужно писать строками, которые потом перепишутся в соответствующие IL команды (да, methodof, propertyof и fieldof есть в IL). На любителя.

Ionad
https://github.com/Fody/Ionad
Заменяет все вызовы методов статических классов на Ваши имплементации с такими же сигнатурами. Наверное, полезно для тестинга, но Fake (бывший moles) делает всё лучше.

Mutable.Fody
https://github.com/ndamjan/Mutable.Fody
Антипод к Freeze. Делает иммутабельное мутабельным, правда уже после того как компилятор всё скомпилировал. Имеет смысл если в F# нужно десериализовать из XML, но Newton.Json уже добавил нормальную десериализцию для immutable f# типов, так что не вижу большого смысла, только если не legacy xml.

Publicize
https://github.com/Fody/Publicize
Делает приватные поля публичными «скрытыми». Мотивацию автор не стал уточнять, оставим это на его совести.

Scalpel
https://github.com/Fody/Scalpel
Вырезает тестовые методы и классы из сборок с кодом по конвеншену или по аттрибуту. Судя по всему, Саймон (автор Fody и большинства плагинов) практикует тестирование «не отходя от кассы». Мне трудно представить мотивацию, но конечно это не самый безумный плагин, хотя уверенно входит в топ 5.

ToString
https://github.com/Fody/ToString
Как и Equals генерирует код на основе полей и как Вы, наверное, уже догадались по название генерирует метод ToString. Если очень не хочется писать ToString для отладки, то это нужный плагин, но польза сомнительна, я не могу вспомнить, когда переопределял ToString не в отладочных целях.

Usable
https://github.com/Fody/Usable
Заворачивает в using локальные переменные имплементирующий IDisposable. Пишите лучше ручками, надёжнее и понятней.

Visualize
https://github.com/Fody/Visualize
Расставляет DebuggerDisplay атрибуты и если класс содержит поля, то генерирует «правильный» код который позволяет смотреть значение полей в режиме дебаггера. Интересно, что для последовательностей генерируется прокси класс. Очень интересная возможность, но я не очень люблю сидеть в отладчике предпочитае ему юнит тесты, так что платить временем сборки проекта за возможность всё посмотреть я бы не стал.

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

Written by alexeysuvorov

24.04.2014 at 3:28 дп

Опубликовано в C#, Fody

Fody.MethodDecorator

with one comment

AOP — акроним который некоторые читают как «больше не нужно писать много кода». На данный момент из бесплатных тулов, на мой взгляд лидирует Fody с неимоверным количеством полезных и бесполезных плагинов казалось бы на все случаи жизни. При ближайшем рассмотрении однако часто оказывается, что инструмент не такой зрелый как хочется. Но исходный код открыт и лицензия не запрещает лепить по образу и подобию. Примерно так и появился MethodDecoratorEx — мой форк Fody.MethodDecorator.

Первоначально задача стояла реализовать бизнес журналирование с набором метрик который мог бы легко расширяться. Для этой задачи отлично подходит MethodDecorator, но проблема была в том, что MethodDecorator:
1 — требовал чтобы атрибут которым помечается метод для логирования был в коренном namespace (читай вообще без namespace)
2 — требовал чтобы атрибут был в той же сборке что и декорируемый код, хотя обычно такие вещи выносят в общие сборки
3 — не умел получать параметры декорируемого метода

Для того чтобы устранить эти и некоторые другие недостатки я начал свою ветку разработки: https://github.com/alexeysuvorov/MethodDecorator

Как всем этим пользоваться

Устанавливаем nuget package

>Install-Package MethodDecoratorEx.Fody

Он автоматически установит Fody в выбранный проект (проекты). Устанавливать MethodDecorator имеет смысл только в проекты, которые содержат методы которые необходимо декорировать. Fody и MethodDecorator не навязывают использование ссылок на какие либо сборки.

После того как пакет добавился настало время выбирать по какому пути пойти в реализации атрибута. Пути всего два:
1 — Создать аттрибут содержащий методы OnEnter, OnExit, OnException (OnInit если нужно получать параметры метода и this) определённых сигнатур и методы, которые необходимо декорировать и сборку или модуль в которых этот код находится этим же самым атрибутом. Т.е. Флаги на аттрибуте будут: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Assembly | AttributeTargets.Module)]. Модуль или сборку мы помечаем для того, чтобы MethodDecorator мог найти этот атрибут не перебирая все атрибуты всех методов убийственно замедляя время компиляции. Атрибут может находиться в какой угодно сборке в которой он может успешно реализовать логику внутри методов.
2 — В папке пакета есть сборка MethodDecoratorInterfaces с интерфейсом IMethodDecorator. Атрибут реализующий этот метод будет найден MethodDecorator-ом и все помеченные им методы будут декорированы кодом вызывающим методы этого атрибута.

Одна из возможных реализаций

// Atribute should be "registered" by adding as module or assembly custom attribute
[module: Interceptor]

// Any attribute which provides OnEntry/OnExit/OnException with proper args
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor | AttributeTargets.Assembly | AttributeTargets.Module)]
public class InterceptorAttribute : Attribute, IMethodDecorator {
    // instance, method and args can be captured here and stored in attribute instance fields
    // for future usage in OnEntry/OnExit/OnException
    public void Init(object instance, MethodBase method, object[] args) {
        TestMessages.Record(string.Format("Init: {0} [{1}]", method.DeclaringType.FullName + "." + method.Name, args.Length));
    }
    public void OnEntry() {
        TestMessages.Record("OnEntry");
    }

    public void OnExit() {
        TestMessages.Record("OnExit");
    }

    public void OnException(Exception exception) {
        TestMessages.Record(string.Format("OnException: {0}: {1}", exception.GetType(), exception.Message));
    }
}

public class Sample {
    [Interceptor]
    public void Method()
    {
        Debug.WriteLine("Your Code");
    }
}

Соответственно метод Sample.Method будет декорирован и примет вид

public class Sample {
    public void Method(int value) {
        InterceptorAttribute attribute = 
            (InterceptorAttribute) Activator.CreateInstance(typeof(InterceptorAttribute));

        // in c# __methodref and __typeref don't exist, but you can create such IL 
        MethodBase method = MethodBase.GetMethodFromHandle(__methodref (Sample.Method), 
                                                           __typeref (Sample));

        object[] args = new object[1] { (object) value };

        attribute.Init((object)this, method, args);

        attribute.OnEntry();
        try {
            Debug.WriteLine("Your Code");
            attribute.OnExit();
        }
        catch (Exception exception) {
            attribute.OnException(exception);
            throw;
        }
    }
}

Подробней остановлюсь на методе Init. Если этого метода нет в атрибуте, то код создания массивов параметров тоже не будет генерироваться. В статических классах параметр this будет null, так что его всегда следует проверять. Как видно из примера инстанс атрибута создаётся на один вызов метода, так что не стоит беспокоится об изменении состояния внутри класса.

Совсем недавно был добавлен атрибут IntersectMethodsMarkedByAttribute (имя атрибута фиксировано, MethodDecorator ищет его по имени). Он позволяет декорировать методы помеченные атрибутами не имеющими реализаций методов OnEnter/OnExit/OnException. В этом случае будут вызваны методы самого IntersectMethodsMarkedByAttribute. Самый простой сценарий который можно представить — это

[module:IntersectMethodsMarkedBy(typeof(TestMethod))]

В данном случае будут декорированы все тестовые методы помеченные майкрософтовским тестовым атрибутом.

Это вроде всё. Напишу потом поподробней как MethodDecorator ищет классы и атрибуты если будет интерес.

Written by alexeysuvorov

24.04.2014 at 2:53 дп

Опубликовано в .net, C#, Fody

Сериализация классов из динамически загружаемых сборок (плагинов)

with one comment

Сериализация объектов для передачи в сообщениях задача с первого взгляда тривиальная, ровно до момента пока все сериализуемые классы достижимы на момент компиляции. Проект freespace по своей сути является набором плагинов объединенных системой коммуникации на основе xmpp. Xmpp протокол предусматривает общение с помощью сообщений в XML формате, т.е. Сборка отвечающая за коммуникацию будет сериализовывать и десериализовывать теоретически неограниченное разнообразие классов из динамически подгружаемых через MEF плагинов. Нужно отметить что я буду говорить о data contract сериализации, а не о стандартной, основная причина – использование сериализайии на основе контрактов не накладывает требования иметь публичный конструктор без параметров. Сперва код, который не работает:

public static Data InitializeFrom<T>(this Data obj, T payload)
    where T : SomeBaseClassForAllMessages
{
    Contract.Requires(obj != null, "Data object must be pre-initialized");
    var s = new DataContractSerializer(payload.GetType());
    var sb = new StringBuilder();
    using (var xw = XmlWriter.Create(sb))
    {
    s.WriteObject(xw, payload);
    xw.Flush();
    obj.Add(XElement.Parse(sb.ToString()));
    }

    return obj;
}

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

//stream и object - внешние параметры
var knownTypes =
    (from assembly in AppDomain.CurrentDomain.GetAssemblies()
        //can be any perfix, or without it at all
        where assembly.FullName.StartsWith("ActiveMesa.FreeSpace")
        select assembly.GetTypes()
        .Where(t => t.IsSubclassOf(typeof(FreeSpaceMessage))))
        .Aggregate((prev, next) => prev.Concat(next));
var des = new DataContractSerializer(obj.GetType(), KnownTypes);
des.WriteObject(stream,obj);

И на последок чуть чуть опитмизации, создание сериализатора / десериализатора очень затратная операция и когда сообщений много (во FreeSpace их действительно много) то это может больно ударить по производительности

internal static class DataContractSerializersStorage
{
    private static readonly Dictionary _serializers = new Dictionary();

    private static readonly object _syncobj = new object();
    private static volatile IEnumerable _knownTypes;
    private static IEnumerable KnownTypes {
        get
        {
            if (null == _knownTypes)
            {
                lock (_syncobj)
                {
                    if(null == _knownTypes){
                        var tmp = (from assembly in AppDomain.CurrentDomain.GetAssemblies()
                            where assembly.FullName.StartsWith("ActiveMesa.FreeSpace")
                            select assembly.GetTypes()
                                .Where(t => t.IsSubclassOf(typeof(FreeSpaceMessage))))
                                .Aggregate((prev, next) => prev.Concat(next));
                        _knownTypes = tmp;
                    }
                }
            }
            return _knownTypes;
        }
    }
    public static DataContractSerializer GetSerializer(Type t)
    {
        var name = t.FullName;
        if(!_serializers.ContainsKey(name))
        {
            lock (_serializers)
            {
                if (!_serializers.ContainsKey(name))
                {
                _serializers.Add(name, new DataContractSerializer(t, KnownTypes));
                }
            }
        }
        return _serializers[name];
    }
}

В конце хочется отметить что лично мне больше по вкусу json сериализация, слишком уж XML широк многословен.

Written by alexeysuvorov

21.08.2011 at 8:44 дп

Опубликовано в .net, C#

Tagged with , ,

MongoDB C# driver (10gen) — продолжение

2 комментария

После первого поста про mongodb я на несколько недель переключался на f# и вернувшись понял, что всё забыто. Пришлось перечитывать собственный пост, и видимо не одному мне раз даже Dmitry Nesteruk влепил мне like. Лирическое отступление закончилось, дальше будут несколько вещей, которые так или иначе отняли у меня время на расследование как это работает, а иногда и на копание в исходных кодах драйвера. Сразу оговорюсь, в данной статье как и в предыдущей речь пойдёт о драйвере, ссылка на который размещена на сайте монго, это драйвер от 10gen.

Частичная загрузка объектов

Начав переводить один из реальных проектов на Монго я сразу столкнулся с необходимостью загружать объекты без вложенных коллекций, которые он содержит. Надо заметить, что рассматриваемый драйвер работает очень просто. Через классы хелперов и визардов он просто складывает строки чтобы получить работающий запрос на родном для mongo javascript. Чтобы не загружать какое то поле, достаточно в запросе указать field_name:0

  db.users.find({}, {thumbnail:0});

В драйвере всё не так очевидно, но после некоторого копания удалось получить вот такой код:

MongoCollection coll = GetCollection();
FieldsBuilder fbExclude = Fields.Exclude(new string[]{“thumbnail”});
//can be FindAllAs<TEntity>()
MongoCursor result = coll.FindAll().SetFields(fbExclude);

Все результаты в курсоре будут без поля thumbnail, проверено, работает замечательно. Все не загруженные поля будут инициализированы значениями по умолчанию, для коллекций null. Тут есть одна тонкость, если вы не загрузите какое либо поле, а потом запишете этот объект, то он совершенно законно перетрёт старый в базе и вот такой тест упадёт на последнем Assert-е:

public class Data
{
  [BsonId]
  public ObjectId Id {get;set;}
  public int Area {get;set;}
}
Data x = new Data();
x.Area = 20;
var db = GetDb();
var coll = db.GetCollection<Data>(typeof(Data).FullName);
db.ClearColl<Data>();
coll.Save(x);
Data y = coll.FindAllAs<Data>().SetFields(Fields.Exclude(new string[]{"Area"})).FirstOrDefault();
Assert.AreEqual(0, y.Area);
coll.Save(y);
Data z = coll.FindAllAs<Data>().SetFields(Fields.Exclude(new string[] { "Area" })).FirstOrDefault();
//fail of course
Assert.AreEqual(20, z.Area);

Я думаю это как то решается через Find and modify, но пока такой потребности не стоит и всё что нужно менять грузится полностью.

Выбор по элементу вложенного массива

В моём случае была задача выбрать регион на основе почтового кода одной европейской страны. Т.к. по историческим причинам общая логика работала не во всех случаях, то регион должен был определятся по следующему алгоритму: если есть точное совпадение, то выбираем найденный регион, если совпадения нет, то оставляем первые 2 цифры и делаем предположение о регионе на основе диапазона почтовых кодов в регионе, причём самих диапазонов может быть много. Объект выглядит как то так:

public class HighLevelCodeInterval 
{
  public HighLevelCodeInterval() { }
    
  public HighLevelCodeInterval(int mn, int mx) 
  {
    Min = mn; Max = mx;
  }
  public int Min { get; set; }
  public int Max { get; set; }
}
public class RegionObject 
{
  public RegionObject() 
  {
    PostalCodes = new List<int>();
  }
 
  [BsonId]
  public int Id { get; set; }
  public string Name { get; set; }
  public List<HighLevelCodeInterval> HighLevelCodes { get; set; }
  public List<int> PostalCodes { get; set; }
}

Тогда запрос выборки по конкретному коду будет:

QueryComplete q = Query.EQ("PostalCodes", postCode);
MongoCollection coll = GetCollection();
RegionObject result = coll.FindOneAs<RegionObject>();

Я бы сказал неожиданно просто. Для поиска по диапазонам приведу только сам запрос:

int nCodeValue = …;
Query.And(     
     	Query.LTE("HighLevelCodes.Min", nCodeValue),
                	Query.GTE("HighLevelCodes.Max", nCodeValue)
)

Надо признать, что поиска по вложенным массивам в монго предстален очень достойно.

Идентификаторы

Драйвер поддерживает 2 встроенных генератора идентификаторов: для Guid и ObjectId типов, т.е. достаточно декорировать свойства одного из этих типов аттрибутом BsonId и дальше всё сделается само. Но идентификатор может быть любым, у меня отлично работают целочисленные идентификаторы, но за их уникальностью следит внешняя по отношению к монго программа. Так же утверждают, есть возможность самом написать генератор:

public class EmployeeIdGenerator : IIdGenerator 
{
  object GenerateId(){ ... }
  bool IsEmpty(object id) { ... }
}
public class Employee 
{ 
  [BsonId(IdGenerator = typeof(EmployeeIdGenerator)]
  public int Id { get; set; }
  // other fields or properties
}

Сам я правда не пробовал.

Заключение

Документо ориентированные базы данных подходят далеко не для всех проектов. Если не получается красиво, то может просто DDD неприменим. Я нашёл что один из моих проектов идеально укладывается в DDD и несказанно рад исчезновению огромного числа таблиц, если всё будет хорошо, то скоро будут тесты производительности в духе MondoDB vs MSSQL на реально работающем приложении.

PS: Хочу упомянуть утилиту , которая очень помогает посмотреть что записалось в базу в результате выполненных операций – это Mongo Vue

Written by alexeysuvorov

11.03.2011 at 11:08 пп

Опубликовано в .net, C#, MongoDb

Mole IDisposable.Dispose in HttpWebResponse

5 комментариев

Тестируя код использующий HttpWebRequest внутри себя с помощью Moles столкнулся с таким сообщением: WebResponse.System.IDisposable.Dispose() was not moled.

WebResponse.System.IDisposable.Dispose() was not moled.

Сам код:

[TestMethod]
[HostType("Moles")]
public void TestWebRequestMole() {
    MHttpWebResponse resp1Mole = new MHttpWebResponse();
    MHttpWebRequest.AllInstances.GetResponse = x => resp1Mole;
    using (var file1 = File.OpenRead("1.html")) {
        resp1Mole.GetResponseStream = () => file1;
        var request = HttpWebRequest.Create("http://valid.url");
        using(var response = request.GetResponse())
        {
            using (var reader = new StreamReader(response.GetResponseStream())) {
                var content = reader.ReadToEnd();
                Assert.IsNotNull(content);
                //check content
            }
        }
    }
}

HttpWebRequest при вызове GetResponse возвращает HttpWebResponse, который сам не имплементирует IDisposable напрямую, более того базовый WebResponse не содержит Mole свойства с именем Dispose потому что реализует IDisposble явно, зато у WebResponse есть другое свойство — SystemIDisposableDispose. Moles использует полную сигнатуру метода при реализации интерфейса явно. Код который работает:

[TestMethod]
[HostType("Moles")]
public void TestWebRequestMole() {
    MHttpWebResponse resp1Mole = new MHttpWebResponse();

    MWebResponse.AllInstances.SystemIDisposableDispose = (x) => { };

    MHttpWebRequest.AllInstances.GetResponse = x => resp1Mole;
    using (var file1 = File.OpenRead("1.html")) {
        resp1Mole.GetResponseStream = () => file1;
        var request = HttpWebRequest.Create("http://valid.url");
        using(var response = request.GetResponse())
        {
            using (var reader = new StreamReader(response.GetResponseStream())) {
                var content = reader.ReadToEnd();
                Assert.IsNotNull(content);
                //check content
            }
        }
    }
}

Written by alexeysuvorov

22.01.2011 at 3:14 пп

Опубликовано в .net, C#, moles, tests

Две вещи которых мне сразу не хватает в C# после F#

with one comment

Я не считаю себя превередливым человеком, но есть вещи в которых очень сложно себе отказать. Я думаю любой кто хотя бы пару дней посидел за f# со мной согласится.
|> и seq.map:

namespace Common.LangExt
{
    using System;
    using System.Collections.Generic;

    public static class λExtension
    {
        public static U ApplyFunction<T, U>(this T t, Func<T, U> f) { return f(t); }
        public static void ApplyFunction<T>(this T t, Action<T> f) { f(t); }

        public static IEnumerable<T> ForEach<U, T>(this IEnumerable<U> t, Func<U, T> f)
        {
            foreach (U x in t) yield return f(x);
        }
        public static void ForEach<U>(this IEnumerable<U> t, Action<U> f)
        {
            foreach (U x in t) f(x);
        }
    }
}

Для тех кто не брезгует символами, которых нет на клавиатуре — своё пространство имён

namespace Common.LangExt.Light
{
    usingCommon.LangExt;
    using System;
    using System.Collections.Generic;

    public static class λExtensionLight
    {
        public static U à<T, U>(this T t, Func<T, U> f)
        {
            return t.ApplyFunction(f);
        }
        public static void à<T>(this T t, Action<T> f)
        {
            t.ApplyFunction(f);
        }

        public static IEnumerable<T> ƒ<U, T>(this IEnumerable<U> t, Func<U, T> f)
        {
            return t.ForEach(f);
        }
        public static void ƒ<U>(this IEnumerable<U> t, Action<U> f)
        {
            t.ForEach(f);
        }
    }
}

Written by alexeysuvorov

14.01.2011 at 3:41 дп

Опубликовано в .net, C#, F#

MongoDB и официальный cssharp-driver

2 комментария

Для MongoDb существует много драйверов, но вот, не так недавно появился официальный. Насколько я понял он разрабатывается компанией http://www.10gen.com и скачать его можно здесь . В этом посте я не буду рассуждать что лучше и зачем вообще нужен MongoDb, я просто пройдусь по основным действиям и моментам, которые показались мне неочевидными при использовании драйвера.

Идентификаторы

У каждого объекта должен быть идентификатор. К нему предъявляется несколько требований: не должен быть null, не должен быть значение по умолчанию (например 0 для int).  Также идентификатор может быть структурой, например:

public struct Pair
{
public int Low{get;set;}
public int High{get;set;}
}

В базе это будет выглядеть так:
MongoDb csharp-driver

Теоретически идентификатором может быть любой объект который может быть сериализован в BsonValue (а это практически любой объект), но в этом случае перед тем как искать по такому объекту его нужно секонвертировать в BsonDocument или BsonValue. Так же есть ObjectId специальный тип в библиотеке MongoDb.Bson, хранит в себе временную метку, идентификатор компьютера на котором создан и какой то pid (возможно идентификатор процесса), если всё равно что использовать в качестве идентификатора, то стоит рассмотреть использование этого типа, потому что сериализуется он на ура и монго сам присвоит ему уникальное значение при сохранении в базу.

Поле идентификатора выбирается для энтити в соответствии с конвенцией или атрибутом. По умолчанию идентификатором будет поле с именем Id если ни одно другое поле не помечено атрибутом [BsonId]. Подробнее про conventions и атрибуты сериализации можно почитать тут

Поиск объекта по идентификатору:

Поиск по простому идентификатору

public class CustomIdEntity 
{
  public string Id { get; set; }
  public string Name { get; set; }
}

var result = demoCollection.FindOneByIdAs<CustomIdEntity>("alexey.suvorov");

Поиск по составному идентификатору

public struct Pair 
{
  public int Low { get; set; }
  public int High { get; set; }
}

public class CustomIdEntity 
{
  [BsonId]
  public Pair ComplexId { get; set; }
  public string Name { get; set; }
  public string Id { get; set; }
}

var result = demoCollection.FindOneByIdAs<CustomIdEntity>((new Pair() { High = 1, Low = 2 }).ToBsonDocument());

Ещё одна особенность MongoDb — как бы не было названо свойство в энтити в базе оно всегда называется _id, возможно это можно изменить с помощью переопределения маппинга, но по умолчанию следующий код работать не будет:

public class DemoEntity 
{
  [BsonId]
  public ObjectId Id { get; set; }
  public string Name { get; set; }
}

[TestMethod]
public void TestInsertDelete() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  var demoCollection = db.GetCollection<DemoEntity>(typeof(DemoEntity).FullName);
  const string ConstName = "Some name";

  demoCollection.RemoveAll();
  var de1 = new DemoEntity();
  de1.Name = ConstName;
  demoCollection.Insert<DemoEntity>(de1);
  Assert.IsFalse(de1.Id == ObjectId.Empty);
  //тут всё хорошо, потому что драйвер знает как правильно искать по id 
  var objFromDb = demoCollection.FindOneByIdAs<DemoEntity>(de1.Id);
  Assert.IsNotNull(objFromDb);
  Assert.AreEqual(ConstName, objFromDb.Name);
  //а вот тут вернётся null, не смотря на то, что запрос логически безупречен
  var searchResult = demoCollection.FindOneAs<DemoEntity>(Query.EQ("Id", de1.Id));
  //этот Assert не проходит
  Assert.IsNotNull(searchResult);
}

На самом деле невелика беда, но если нужен метод  RemoveById, то писать его нужно как то так

public static class MongoExtensions 
{
  private const string ID_COLUMN_NAME = "_id";
  public static void RemoveById(this MongoCollection collection, BsonValue val) {
    collection.Remove(Query.EQ(ID_COLUMN_NAME, val));
  }
}

Запросы

Данный драйвер не поддерживает LINQ, либо авторы не почувствовали в себе силы реализовать эту функцию, либо они просто сочли что LINQ «не ложиться» на идею документо ориентированной БД и не вписывается в DDD, вопрос спорный, но я тоже не вижу как и зачем может быть реализован join для документо ориентированной базы данных. Классы запросов находятся в пространстве имён MongoDB.Driver.Builders и я успел попробовать не все классы, но уже кое что потребовало копания в сорцах, чтобы выяснить как заставить это работать.

Начнём с поиска на основе данных во вложенных объектах. В mongo действует dot notation, подробнее тут , пример:

public class ChildChild 
{
  public int SomeVal { get; set; }
}

public class Child 
{
  public int Age { get; set; }
  public ChildChild ChildChildInstance { get; set; }
}

public class Parent 
{
  public ObjectId Id { get; set; }
  public string Name { get; set; }
  public Child ChildInstance { get; set; }
}

[TestMethod]
public void TestQueryToChild() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  ClearCollection<Parent>(db);
  const string NameTemplate = "Parent {0}";
  
  var parentCollection = db.GetCollection<Parent>(typeof(Parent).FullName);

  for (int i = 0; i < 10; i++) 
  {
    var cc = new ChildChild() { SomeVal = i };
    var c = new Child() { Age = i, ChildChildInstance = cc};
    var p = new Parent() {
      Name = string.Format(NameTemplate, i),
      ChildInstance = c
    };
    parentCollection.Insert(p);
  }

  //запрос через dot notation
  var resColl = parentCollection.FindAs<Parent>(Query.EQ("ChildInstance.Age", 5));
  Assert.AreEqual(1, resColl.Count());
  Assert.AreEqual(string.Format(NameTemplate, 5), resColl.First().Name);

  //запрос через совпадение объекта
  var childChildDoc = (new ChildChild() { SomeVal = 5 }).ToBsonDocument();
  var resultColl = parentCollection.FindAs<Parent>(Query.EQ("ChildInstance.ChiъldChildInstance", childChildDoc));

  Assert.AreEqual(string.Format(NameTemplate, 5), resultColl.First().Name);
}

Тут есть принципиальная разница между запросами. Запрос через dot notation подразумевает частичное совпадение объекта, т.е. конкретного поля или полей, а вот запрос на основе объекта подразумевает полное совпадение объекта по которому ищем (совпадение бинарных данных после сериализации в BSON).

Соответственно Query.EQ (равно, если кто ещё не понял) возвращает нам CompletedQuery, т.е. не подразумевающий добавления дальнейших условий, тогда как  Query.GT (строго больше) или Query.LTE (меньше равно) возвращают QueryConditionList к которому условия могут быть добавлены, что для меня было не очень очевидно и я сначала пытался сделать проверку на вхождения значения в интервал через Query.And. Правильно сделать:

//постарше, но в рамках 😉
collection.FindAs<Girl>(Query.GT("Age", 25).LT(35));

Вернёмся к LINQ. Все разнообразные Find* методы возвращают MongoCursor;

подробнее о нём тут. По сути это IEnumerable, и к нему подходят стандартные методы LINQtoObject, но они не будут транслированы в запросы на сервере, простой тест:

[TestMethod]
public void TestLinq() 
{
  var server = new MongoServer(new MongoUrl(ConnectionString));
  var db = server.GetDatabase(TestDatabaseName);
  int nCount = 1000000;
  ClearCollection<DemoPerson>(db);

  var collection = db.GetCollection<DemoPerson>(typeof(DemoPerson).FullName);

  for (int i = 0; i < nCount; i++) 
  {
    collection.Insert(new DemoPerson() { Age = i, Name = string.Format("Name {0}",i) });
  }
  var start = DateTime.Now;
  var res = collection.Find(Query.Null).Where(x => x.Age < 99440 && x.Age < 99450);
  Assert.AreEqual(9, res.ToList().Count);
  var end = DateTime.Now;
  
  var start2 = DateTime.Now;
  var res2 = collection.Find(Query.GT("Age", 99440).LT(99450));
  Assert.AreEqual(9, res2.ToList().Count);
  var end2 = DateTime.Now;
  
  //много секунд и памяти
  var diff = end - start;
  //мало секунд и памяти
  var diff2 = end2 - start2;
}

Так что в запросах разбираться всё таки придётся и тут немного примеров, правда они не на C#.

Заключение

За бортом остались индексы, частичная загрузка объектов и map reduce, но я сам пока до этого не добрался. Хочу так же отметить, что Mongo оставляет более приятное впечатление чем тот же RavenDB от которого веет Nhibernate в каждой строчке. Кода не будет, потому что у приведённых примеров нет общей идеи и целосности, если что то не очень понятно я постараюсь оперативно отвечать в комментариях.

Использованные версии
MongoDb: 1.6.5
Driver: 0.9.0.3992

Written by alexeysuvorov

20.12.2010 at 3:12 дп

Опубликовано в .net, C#, MongoDb