суббота, 13 октября 2012 г.

Strongly-typed Expand and async queries.

Очередной рабочий день разбирался с особенностями WCF Data Services.

Как я уже раньше писал, клиентская часть по умолчанию использует ленивую синхронную подгрузку. Чтобы этого избежать, пишутся простенькие методы, которые делают явную загрузку (eager loading). Благодаря избавлению от ObservableCollection<T>, это сделать легко.
Сегодня в дополнение к методу WhereAsync добавил методы FirstOrDefaultAsync и CountAsync, т.к. именно эти методы востребованы членами моей команды (судя по количеству не очень красивого кода, где они это сделали через WhereAsync). Код ниже.

Вторая важная особенность -- типизированный Expand. Наш предыдущий мега-проект работает на WCF RIA Services + SL, поэтому все уже привыкли писать инклуды. Тут, казалось бы, заменяем слово Include на Expand - и вуаля! Как бы не так.
.Expand(a => a.Author)
.Expand(a => a.Customers.Select(c => c.Person))
Первый работает, второй нет. Сегодня работая с проектом заметил, что коллеги решили не париться и писать строковые, не-типизированные Expand'ы. Кровь взыграла: как же так, нарушение идеологии!
Нашёл вот здесь код для примера, допилил чуток, и вот он - метод Include!
Если кому пригодится, кинул на pasteBin.

Третья важная особенность - это не EF. Поскольку все привыкли к риа сервисам, возникает куча проблем на ровном месте, как-то:
  • сущность уже загружена сервисом, как мне её выцепить по Id? Снова лезть на сервисы?
  • надо сделать отмену изменений свойств сущности (Modified -> Unchanged). Как, заново брать её от сервисов?
  • хз есть сущность или нет. Подгружаем на всякий случай -- падает Exception: The context is already tracking a different entity with the same resource Uri.
  • в модели много FK связей. Присвоение навигационного свойства не меняет скалярный Id и наоборот. SetLink иногда помогает, иногда нет.
  • как сделать SaveChanges только для одного графа, не затрагивая другой? Несколько контекстов и attach/detach? Вы это серьёзно?
  • как писать валидацию? Дублировать код с обеих сторон? Пока делаем client-side only, но есть идея делать всё в одном месте: серверный EF, автоматическая генерация некоторых проверок в SQL и прокидывание на клиента с возможностью расширения на клиенте.
Короче, проблем хватает, наверное интереснее будет реализовать подмножество функций EF для решения описанных проблем, благо T4 имеются.

понедельник, 8 октября 2012 г.

OData Client T4 template

Недавно начали писать приложение с использованием WCF Data Services. Сервисы получаются простыми, лёгкими, быстрыми - то что нужно для скоростного написания прототипа.

Недостатки, конечно, присутствуют. Вот что помешало работе в первый же день:
  • Синхронная ленивая загрузка данных.
  • Клиентские классы без возможности кастомизации.
  • Функционал контекста конечно далёк от EF и RIA Services.
  • Не прокидываются DataAnnotations, про которые знает EF.
Первый недостаток решается путём написания метода с названием типа WhereAsync, который к тому же легко сделать awaitable. Однако практически сразу приложение начало падать.
Дело в том, что в клиентских сущностях для коллекций используется класс DataServiceCollection<T> : ObservableCollection<T>. Решение мотивируется тем, что это позволяет напрямую биндить коллекции сущностей в UI. Однако класс ObservableCollection<T> не является thread-safe.
Что происходит: при материализации объектов, связанных с уже загруженными объектами, DataServiceContext вызывает метод InsertItem, который кидает InvalidOperationException.

Пришлось не побояться и попробовать T4 темплейты.
Столкнулся сразу с несколькими особенностями темплейтов:
  • Не подцепляется пространство имён. Логично, правда? Решается дописыванием строчки TransformContext.Namespace = "MyProject.Client.MyService";
  • Версия протокола определяется как V1, из-за чего компилятор тут же начинает ругаться на методы Any. Поскольку сервисы у нас свои и в том же солюшне, закомментарил строчку //version = dataServiceVersion.Major.ToString();
  • Не тащатся DefaultValue из edmx. Вот тут пришлось повозиться. Инструкция:
    1. В метод WriteTypeProperties для primitive types добавить:
      var structuralType = property as IEdmStructuralProperty;
      string defaultValue = string.Empty;
      if (structuralType != null && structuralType.DefaultValueString != null)
             defaultValue = structuralType.DefaultValueString.ToString();
    2. Этот самый defaultValue передавать в WriteTypeProperty и оттуда в WriteTypePropertyFields.
    3. В самом WriteTypePropertyFields убрать слово new и добавить его в другие WriteTypeProperty методы.
    4. Добавить обработчик строкового значения:
      if (!string.IsNullOrEmpty(defaultValue)
      && primitiveType.PrimitiveKind == EdmPrimitiveTypeKind.String)
      {
           defaultValue = "\"" + defaultValue + "\"";
      }
    5. В методе WriteTypeStaticCreateMethod не обрабатывать свойства, у которых есть defaultValue:
      var structuralType = property as IEdmStructuralProperty;
      if (structuralType == null || structuralType.DefaultValueString == null)
               nonNullableProps.Add(property); 
        
Благодаря темплейтам поменял тип коллекций на List<T>, и асинхронная загрузка стала работать как ожидается. Я уверен, что до выкладывания в продакшн возникнут и другие проблемы, но если есть полный контроль над процессом (T4, исходники на codeplex) - большинство проблем можно решить.
P.S. Столько удовольствия приносят эти "развлечения", когда на день-два приходится отвлечься от "экстенсивного" кодинга (генерации контента) к "интенсивному" почти-программированию (исследованию технологий).