Linq to SharePoint. Часть 4. Dynamic LINQ

Часть 1. First()/FirstOrDefault(), T-SQL IN, Path
Часть 2. Count(), Take(), Skip(), JOIN
Часть 3. Анонимный доступ, Получение списка по URL'у, Cross-Site запросы
Часть 4. SPListItem -> LINQ, Dynamic Linq to SharePoint
Часть 5. Поля Choice и MultiChoice в Linq to SharePoint
Часть 6. Сравнение производительности Linq to SharePoint и Camlex.NET

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

Linq to SharePoint и SPListItem

Предположим, что у нас есть очень сложный CAML-запрос, который откуда только не выбирает данные с безумным фильтром. Или, что более реально, у нас есть SPListItem в EventReceiver'е. И в обоих случаях мы хотим получить объект ZhukBlogLinqExamples.Model.Employee.

Создадим конструктор в базовом классе, чтобы эта возможность появилась во всех дочерних классах и, чтобы, в случае создания новых классов, нам не приходилось писать новый код:

  1. public class ZhukDataItem : ITrackEntityState, ITrackOriginalValues,
  2.                             INotifyPropertyChanged, INotifyPropertyChanging
  3. {
  4.     public ZhukDataItem(SPItem item)
  5.     {
  6.     }
  7. }

Чтобы заполнить свойства будем использовать атрибут ColumnAttribute наших свойств:

  1. public ZhukDataItem(SPItem item)
  2. {
  3.     if (item == nullreturn;
  4.     // Текущий тип
  5.     var objType = GetType();
  6.     // Получаем свойства текущего объекта
  7.     var properties = objType.GetProperties();
  8.     foreach (var property in properties)
  9.     {
  10.         // Получаем атрибуты типа ColumnAttribute
  11.         var attributes = property.GetCustomAttributes(typeof(ColumnAttribute), false);
  12.         foreach (ColumnAttribute att in attributes)
  13.         {
  14.             // Берем поле для хранения значения, 
  15.             // указанное в атрибуте ColumnAttribute
  16.             var field = objType.GetField(att.Storage, 
  17.                 BindingFlags.NonPublic | BindingFlags.Instance);
  18.             // Если такого поля в классе нет, то просматриваем базовые классы
  19.             while (field == null
  20.             {
  21.                 objType = objType.BaseType;
  22.                 if (objType == nullbreak;
  23.                 field = objType.GetField(att.Storage, 
  24.                     BindingFlags.NonPublic | BindingFlags.Instance);
  25.             }
  26.             if (field != null)
  27.             {
  28.                 // Если поле Lookup (LookupId;#LookupValue), то разбираем его
  29.                 if (att.FieldType == "Lookup")
  30.                 {
  31.                     try
  32.                     {
  33.                         var fv = new SPFieldLookupValue(
  34.                             (item[att.Name] ?? string.Empty).ToString());
  35.                         if (att.IsLookupId)
  36.                         {
  37.                             field.SetValue(this, fv.LookupId);
  38.                         }
  39.                         else
  40.                         {
  41.                             field.SetValue(this, fv.LookupValue);
  42.                         }
  43.                     }
  44.                     catch (ArgumentException) // Если Lookup поле равно null
  45.                     {
  46.                         field.SetValue(this, item[att.Name]);
  47.                     }
  48.                 }
  49.                 else
  50.                 {
  51.                     // Остальные поля записываем как есть
  52.                     field.SetValue(this, item[att.Name]);
  53.                 }
  54.             }
  55.         }
  56.     }
  57. }

Дополнительно можно проверять на реализацию объектом интерфейса ICustomMapping и вызывать метод MapFrom. Объект мы получили, теперь надо присоединить его к контексту данных. Для этого используем метод Attach:

  1. using (var ctx = new ZhukDataContext(siteUrl))
  2. {
  3.     // Получаем объект SPListItem
  4.     SPListItem employeeListItem = GetEmployeeListItem(employeeId);
  5.     // Создаем экземпляр класс Employee
  6.     var entity = new Employee(employeeListItem);
  7.     // Присоединяем объект к контексту
  8.     ctx.Employees.Attach(entity);
  9.     
  10.     // Проводим необходимые манипуляции
  11.  
  12.     // Сохраняем изменения
  13.     ctx.SubmitChanges();
  14. }

Dynamic Linq to SharePoint

В первой части я писал о том, что Linq to SharePoint не поддерживает Contains(). Теперь я покажу способ это ограничение обойти. Для этого напишем метод расширитель, который будет строить выражения для каждого значения из массива (тот самый IN из T-SQL) и затем объединять их:

  1. public static Expression<Func<T, bool>> EqualsAny<T, TValue>(this Expression<Func<T, TValue>> selector, 
  2.     IEnumerable<TValue> values)
  3. {
  4.     // Если значений нет, то возвращаем выражение x=> false
  5.     if (!values.Any()) return x => false;
  6.     // Аналогично поступаем в случае, когда кол-во параметров не равно одному
  7.     if (selector.Parameters.Count != 1) return x => false;
  8.     // Берем параметр селектора.
  9.     // Он понадобиться для построения выражений
  10.     var p = selector.Parameters.First();
  11.     // Для каждого значения строим выражение
  12.     var equals = values
  13.         .Select(v => (Expression)Expression.Equal(selector.Body, 
  14.                                  Expression.Constant(v, typeof(TValue))));
  15.     // Объдиняем получившиеся выражения
  16.     var body = equals.Aggregate(Expression.Or);
  17.     // Возвращаям получившиеся выражение
  18.     return Expression.Lambda<Func<T, bool>>(body, p);
  19. }

Аналогично можно сделать методы для текстовых значение (StartsWitAny, ContainsAny и др.). А вот объединять произвольные выражения у меня не получилось. Чтобы их объединить приходилось пользоваться методом Expression.Invoke. После чего SPLinqProvider выбрасывал исключение: Lambda Parameter not in scope. Аналога методов WhereAny и WhereAll из CamleX'а у меня не получилось.

Использовать новый метод можно даже в достаточно сложной конструкции:

  1. using (var ctx = new ZhukDataContext(siteUrl))
  2. {
  3.     var ids = new int?[] { 1, 3, 5, 7, 9, 11, 13, 15 };
  4.     var predicate = EqualsAny<Employee, int?>(emp => emp.Id, ids);
  5.     var employees = ctx.Employees
  6.         .ScopeToFolder(string.Empty, true)
  7.         .Where(emp => emp.ManagerId == 2)
  8.         .Where(predicate)
  9.         .Where(emp => emp.AccessLevel > 2)
  10.         .OrderBy(emp => emp.Title)
  11.         .Take(5)
  12.         .ToList();
  13.     //...
  14. }

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

  1. <View Scope='RecursiveAll'>
  2.   <Query>
  3.     <Where>
  4.       <And>
  5.         <And>
  6.           <And>
  7.             <BeginsWith>
  8.               <FieldRef Name="ContentTypeId" />
  9.               <Value Type="ContentTypeId">0x010078B0DD38574940478CF9E129FCD65E9B</Value>
  10.             </BeginsWith>
  11.             <Eq><FieldRef Name="Manager" LookupId="TRUE" /><Value Type="Lookup">2</Value></Eq>
  12.           </And>
  13.           <Or>
  14.             <Or>
  15.               <Or>
  16.                 <Or>
  17.                   <Or>
  18.                     <Or>
  19.                       <Or>
  20.                         <Eq><FieldRef Name="ID" /><Value Type="Counter">1</Value></Eq>
  21.                         <Eq><FieldRef Name="ID" /><Value Type="Counter">3</Value></Eq>
  22.                       </Or>
  23.                       <Eq><FieldRef Name="ID" /><Value Type="Counter">5</Value></Eq>
  24.                     </Or>
  25.                     <Eq><FieldRef Name="ID" /><Value Type="Counter">7</Value></Eq>
  26.                   </Or>
  27.                   <Eq><FieldRef Name="ID" /><Value Type="Counter">9</Value></Eq>
  28.                 </Or>
  29.                 <Eq><FieldRef Name="ID" /><Value Type="Counter">11</Value></Eq>
  30.               </Or>
  31.               <Eq><FieldRef Name="ID" /><Value Type="Counter">13</Value></Eq>
  32.             </Or>
  33.             <Eq><FieldRef Name="ID" /><Value Type="Counter">15</Value></Eq>
  34.           </Or>
  35.         </And>
  36.         <Gt><FieldRef Name="AccessLevel" /><Value Type="Integer">2</Value></Gt>
  37.       </And>
  38.     </Where>
  39.     <OrderBy Override="TRUE"><FieldRef Name="Title" /></OrderBy>
  40.   </Query>
  41.   <ViewFields>
  42.     <FieldRef Name="CellPhone" />
  43.     <FieldRef Name="AccessLevel" />
  44.     <FieldRef Name="Manager" />
  45.     <FieldRef Name="Department" />
  46.     <FieldRef Name="ID" />
  47.     <FieldRef Name="owshiddenversion" />
  48.     <FieldRef Name="FileDirRef" />
  49.     <FieldRef Name="Title" />
  50.     <FieldRef Name="Author" />
  51.     <FieldRef Name="Editor" />
  52.   </ViewFields>
  53.   <RowLimit Paged="TRUE">5</RowLimit>
  54. </View>

Осталось, пожалуй, только описать работу с оптимистическими блокировками в Linq to SharePoint. В ближайшее время напишу.


Поделиться

Коментарии