Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

Генерация типа из слаботипизированной коллекции

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

Представьте что вы работаете с документо-ориентированной базой, для которой у вас нет схемы. Вы получаете из этой базы одну запись, и хотите воссоздать из неё объект для использования в data binding-ориентированном сценарии – например, показать структуру объекта в таблице. В этом посте я хочу показать про то, как это сделать.

Общее положение

Для данного примера, я воспользуюсь базой данных MongoDB а также драйвером mongodb-csharp. Этот драйвер достаточно прост в использовании – мы создаем объект типа Mongo, получаем коллекцию, ну а дальше работаем с ней. Тут есть одна тонкость – работать с коллекцией можно либо через сильнотипизированный интерфейс, либо без типизации вообще. Например,

  • db.GetCollection<Person>() вернет вам коллекцию объектов Person
  • db.GetCollection(«Person») вернет вам коллекцию объектов типа MongoDB.Document

Хитрый класс Document представляет из себя проекцию BSON на IEnumerable<KeyValuePair<string,object>> – я не знаю почему авторы не смогли просто взять и использовать Dictionary<string,object>, ну да поделом. Соответственно, вся «начинка» такого объекта – это пары вроде, например, KeyValuePair<string,string>.

Помимо BCL-типов, с которыми особых проблем нет, существуют также типы вроде, например, Oid – это пресловутый тип драйвера для идентификации объекта, который мэпится на Mongo’вский _id. Тем самым, для того чтобы динамически создать какой-то тип, нам нужно иметь жесткую привязку к драйверу. (Нарушение паттернов?)

Конструкция типов через CodeDom

Итак, мы хотим создать тип на основе данных полученных из MongoDB. Давайте разделим это на две задачи – создание структуры нашего объекта, и компиляцию этого объекта в сборку. Поговорим сначала о первом.

Чтобы такое компиляция сборки прямо в .Net-приложении? Фактически, это создание временного .cs-файла и вызов на него csc.exe вашим приложением. Соответственно, есть два механизма создания этого самого cs-файла.

Первый механизм – это написать исходный код как текст самому. Берем StringBuilder и начинаем писать, строчку за строчкой, точно так же как обычный исходный код.

Второе решение – посложнее. Технология CodeDom, ныне практически мертвая (!!!), позволяет создавать с помощью объектных структур нечто, что в последствии превратится в код. И все бы хорошо, но де факто у CodeDom много проблем которые не существенны для нашего сценария, но могут быть критичны для более сложных.

В чем главная проблемы? А в том, что CodeDom написан так, чтобы из одной модели можно было генерировать код на разных языках, будь то C# или Visual Basic. Это звучит хорошо пока вы не начинаете понимать, что все фичи которых нет в VB не реализованы и в C#. Например readonly поля или auto-properties. Понятное дело что такие механизмы можно обойти, но как-то это неопрятно. Пример такого хака мы кстати увидим позднее.

Начинаем!

Итак, в иерархии CodeDom, у нас есть структура CompilationNode, которая отражает сборку которую нужно компилировать. В ней у нас NamespaceNode, а внутри CodeTypeDeclaration’ы с разной начинкой. Соответвенно, нашей целью является на основе имени и набора параметров сгенерировать класс с автосвойствами.

private static IEnumerable<CodeTypeDeclaration> BuildDeclarations(KeyValuePairs keyValuePairs, string name)
{
  ...
}

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

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

var t = new CodeTypeDeclaration(name);
t.IsClass = true;
t.TypeAttributes = System.Reflection.TypeAttributes.Public;

Сразу после этого идет обход свойств. Чтобы получить авто-свойство, происходит небольшой «хак» с добавлением get и set прямо в название.

foreach (var p in keyValuePairs)
{
  var prop = new CodeMemberField();
  prop.Attributes = MemberAttributes.Public | MemberAttributes.Final;
  prop.Name = p.Key + " {get;set;}//"; // hack
  var type = (p.Value ?? new object()).GetType();
  ...
}

Интересный corner case тут будет в тех случаях когда значение полученное из базы данных равно null – в этом случае мы в принципе не можем угадать тип, и поэтому используем новый object как единственно приемлимый вариант.

Теперь задача делится на 3 состовляющие, а именно

  • Свойство которое является некомпозитным, т.е. например строка, число, Oid
  • Свойство которое является композитным, т.е. является, по сути дела, KeyValuePair<string,Document>
  • Свойство, которое отображает коллекцию, т.е. List<Document>

Начнем с простого – задать тип для примитивного свойства легко:

prop.Type = new CodeTypeReference(type);

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

if (type == typeof(Document))
{
  var innerEntities = BuildDeclarations(p.Value as KeyValuePairs, p.Key + "Type");
  foreach (var ie in innerEntities)
    yield return ie;
  prop.Type = new CodeTypeReference(innerEntities.First().Name);
}

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

Теперь насчет коллекций – реализация тут аналогичная, с той лишь разницей что мы точно знаем тип (List) и тот факт что его generic-аргумент нужно подменить. Делается это легко:

else if (type == typeof(List<Document>))
{
  var first = (p.Value as List<Document>).FirstOrDefault();
  if (first != null)
  {
    var innerEntities = BuildDeclarations(first, p.Key + "Type");
    foreach (var ie in innerEntities)
      yield return ie;
    prop.Type = new CodeTypeReference(
      "System.Collections.Generic.List<{0}>".ƒ(innerEntities.First().Name));
  }
}

Проверка на null выше – это легкий элемент паранойи. В принципе, специфика MongoDB вне зависимости от драйвера в том, что Document в принципе не может быть коллекцией из 0 (нуля) элементов – либо он «с начинкой», либо его нет вообще. Наводит на мысли о недовольствах некоторых людей об отсутствии поддержки non-nullable classes.

Вот собственно и всё – в конце этой эпопеи мы выдаем корневой тип (yield return t), и в игру вступает компилятор.

Компиляция

Итак, мы пишем еще один метод, целью которого будет все собрать в in-memory сборку. Для начала, мы создаем пустое пространство имен:

public Type BuildEntityFromDictionary(KeyValuePairs keyValuePairs, string name)
{
  var unit = new CodeCompileUnit();
  var ns = new CodeNamespace(nsName);
  ns.Imports.Add(new CodeNamespaceImport("MongoDB"));

Теперь, мы запускаем злой рекурсивный алгоритм для создания типов:

foreach (var t in BuildDeclarations(keyValuePairs, name))
  ns.Types.Add(t);
unit.Namespaces.Add(ns);

Далее, готовим компилятор C#.

var providerOptions = new Dictionary<string, string>();
providerOptions.Add("CompilerVersion", "v4.0");
var provider = new CSharpCodeProvider(providerOptions);

Компилятор, увы, даже не имеет в своем названии соответствующее слово. Давным-давно, был класс под названием CSharpCompiler, но он ныне deprecated – впрочем, его использование никто не запрещает.

Тем временем мы подходим к ответственной части – созданию опций компилятора. Тут важно не ошибиться и добавить все сборки на которые может (в принципе) полагаться ваш тип.

var cp = new CompilerParameters();
cp.GenerateInMemory = true;
cp.GenerateExecutable = true;
cp.CompilerOptions = "/target:library";
cp.ReferencedAssemblies.Add("MongoDB.dll");
cp.ReferencedAssemblies.Add("System.Xml.dll");

Также, выставление свойства GenerateInMemory означает, что на диске не создается отдельная сборка – она создается в памяти, и сразу подгружается в процесс – это на самом деле здорово т.к. не нужно вручную делать какой-нть AssemblyLoad. Единственная проблема (но не в данном случае) – это то, что сборки нельзя отгружать. Если вы считаете что это огромный фейл, добро пожаловано на Connect.

Апогеем нашего типостроения является компиляция типа. Как только мы скомпилировали этот тип, он сразу загружается, и тем самым мы можем смело возвращать ссылку на этот Type из метода:

var results = provider.CompileAssemblyFromDom(cp, unit);
foreach (var e in results.Errors)
  Console.WriteLine(e);
return results.CompiledAssembly.GetType("{0}.{1}".ƒ(nsName, name));

Вот собственно и всё – у вас если сильнотипизированный класс, который более-менее (см. ниже) отражает структуры документно-ориентированной базы данных.

Где мои метаданные?

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

Главная проблема – это то, что у вложенных сущностей нет имени типа. Это значит что если у вас есть Person и у него есть, скажем, HomeAddress и WorkAddress, то алгоритм сгенерирует вам 2 разных класса вмест того, чтобы сделать один единственный класс Address. Понятное дело что можно пробовать детектировать схожие структуры (хочется верить, что это реально!) но какие названия давать таким структурам?

Аналогичная проблема встречается в том случае, когда у вас есть коллекция объектов. Как ее назвать? Меня всегдя смущали алгоритмы, конвертирующие между единичными и множественными именованиями объектов – сколько бы их не оттачивали, там всегда будут проблемы, и Entity Framework designer тому пример.

Ну и последняя, уму не постижимая и нерешабельная проблема. Допустим вы дополняете схему базы. В этом случае, сделав FindOne(), вы получите неполную сруктуру. А как получить полную? Обойти все дерево данных (!!!) и сделать один коллективный merge по всем объектам. Естественно, это решение не жизнеспособно, т.к. если в базе не один-два документа а миллион, пользователю придется очень долго ждать.

Реклама

Written by Dmitri

6 сентября 2010 в 19:46

Опубликовано в .NET, c sharp, C#, MongoDB

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

Subscribe to comments with RSS.

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

    Denis Gladkikh

    6 сентября 2010 at 22:32

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

      Dmitri

      6 сентября 2010 at 23:13

  2. Меня одного смущает вопрос, почему нельзя было использовать Dynamic?

    Gengzu

    8 сентября 2010 at 19:55

  3. Не считая того, что сама ситуация, описанная автором, из ряда вон.

    Gengzu

    8 сентября 2010 at 19:56

    • Во-первых, ситуация не «из ряда вон» — это продакшн код. А насчет dynamic я вообще не понимаю. Как прикажете его использовать?

      Dmitri

      8 сентября 2010 at 20:11

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

        Gengzu

        8 сентября 2010 at 20:15

  4. еще совсем не ясно как использовать этот Ваш пример. код есть, примера использования нет. или я просмотрел?

    Gengzu

    8 сентября 2010 at 22:41

    • Эммм, пока нет т.к. открылась масса проблем с хэшами итп.

      Dmitri

      8 октября 2010 at 14:31

  5. > не знаю почему авторы не смогли просто взять и использовать Dictionary

    Чтобы не терять упорядоченность пар?

    Andrey Popp

    8 октября 2010 at 14:26


Оставить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: