Linq to SharePoint. Паттерн Repository

Linq to SharePoint - это провайдер от Microsoft, который позволяет транслировать LINQ-выражения в CAML-запросы для работы с данными списков и библиотек документов SharePoint. Сегодня я покажу, как можно реализовать паттерн репозитория для работы с данными SharePoint 2010.

Репозиторий

Так как работа с данными SharePoint имеет свою специфику, для начала определим требования к будущему репозиторию:

  • Поддержка анонимного доступа - потребуется, например, при создании интернет-сайта на базе SharePoint 2010;
  • Режим "только для чтения" - при работе с Linq to SharePoint отключение отслеживания изменений объектов это позволит обеспечить лучшую производительность. Подробнее об этом здесь;
  • Доступ к объектной модели SharePoint - наш репозиторий должен обеспечить некую инкапсуляцию для простого доступа к объектной модели SharePoint 2010.

Исходя из этих требований будем реализовывать паттерн репозитория. А модель данных для этого мы будем использовать описанную мною в февральском посте, посвященном Linq to SharePoint.

Entity, DataContext

Базовым классом для всех других классов, описанных в модели данных будет класс ZhukDataItem, привязанный к базовому типу содержимого SharePoint - элемент (Id = 0x01).

Контекст для работы с данными здесь свой не понадобится. Вполне хватит стандартного Microsoft.SharePoint.Linq.DataContext. При желании можно создать и свой, реализовав в нем дополнительные методы, например получение списка по его URL'у и прочее.

Итак, вышеописанное в виде диаграммы классов:

Диаграмма классов репозитория SharePoint 2010

При инициализации репозитория мы будем создавать контекст для работы с данными, определять является ли пользователь анонимным и инициализировать загрузку информации о списке (EntityList<TEntity>):

  1. /// <summary>
  2. /// Базовый класс репозитория
  3. /// </summary>
  4. /// <typeparam name="TEntity">Тип сущности</typeparam>
  5. /// <typeparam name="TContext">Тип контекста данных</typeparam>
  6. public abstract class BaseRepository<TEntity, TContext>
  7.     where TEntity : ZhukDataItem, new()
  8.     where TContext : DataContext
  9. {
  10.     protected readonly string WebUrl;
  11.     protected readonly string ListName;
  12.     protected readonly bool ReadOnly;
  13.     public readonly bool IsAnonymous;
  14.  
  15.     /// <summary>
  16.     /// Инициализация репозитория
  17.     /// </summary>
  18.     /// <param name="listName">Название списка</param>
  19.     /// <param name="webUrl">Url сайта</param>
  20.     /// <param name="readOnly">Режим "Только для чтения"</param>
  21.     protected BaseRepository(string listName, string webUrl, bool readOnly)
  22.     {
  23.         ReadOnly = readOnly;
  24.         ListName = listName;
  25.         WebUrl = webUrl;
  26.  
  27.         var ctx = SPContext.Current;
  28.         IsAnonymous = ctx != null && SPContext.Current.Web.CurrentUser == null;
  29.  
  30.         InitializeParameters();
  31.     }
  32.  
  33.     /// <summary>
  34.     /// Инициализация репозитория для текущего сайта
  35.     /// </summary>
  36.     /// <param name="listName">Название списка</param>
  37.     /// <param name="readOnly">Режим "Только для чтения"</param>
  38.     protected BaseRepository(string listName, bool readOnly)
  39.         : this(listName,
  40.             SPContext.Current.Web.Url, readOnly)
  41.     { }
  42.  
  43.     /// <summary>
  44.     /// Инициализация репозитория для текущего сайта в режиме "Только для чтения"
  45.     /// </summary>
  46.     /// <param name="listName">Название списка</param>
  47.     protected BaseRepository(string listName)
  48.         : this(listName, true)
  49.     { }
  50.  
  51.     /// <summary>
  52.     /// Инициализация репозитория в режиме "Только для чтения"
  53.     /// </summary>
  54.     /// <param name="listName">Название списка</param>
  55.     /// <param name="webUrl">Url сайта</param>
  56.     protected BaseRepository(string listName, string webUrl)
  57.         : this(listName, webUrl, true)
  58.     { }
  59.  
  60.     /// <summary>
  61.     /// Инициализация параметров репозитория
  62.     /// </summary>
  63.     private void InitializeParameters()
  64.     {
  65.         if (IsAnonymous)
  66.         {
  67.             RunAsAdmin(() =>
  68.             {
  69.                 CurrentContext =
  70.                     (TContext)Activator.CreateInstance(typeof(TContext),
  71.                     new object[] { WebUrl });
  72.                 CurrentContext.ObjectTrackingEnabled = !ReadOnly;
  73.                 CurrentList = CurrentContext.GetList<TEntity>(ListName);
  74.             });
  75.         }
  76.         else
  77.         {
  78.             CurrentContext =
  79.                 (TContext)Activator.CreateInstance(typeof(TContext),
  80.                 new object[] { WebUrl });
  81.             CurrentContext.ObjectTrackingEnabled = !ReadOnly;
  82.             CurrentList = CurrentContext.GetList<TEntity>(ListName);
  83.         }
  84.     }
  85.  
  86.     /// <summary>
  87.     /// Контекст данных
  88.     /// </summary>
  89.     private TContext CurrentContext { getset; }
  90.  
  91.     /// <summary>
  92.     /// Список/библиотека элементов
  93.     /// </summary>
  94.     private EntityList<TEntity> CurrentList { getset; }
  95.     
  96.     //... Прочие методы
  97. }

Если пользователь анонимен, то контекст данных будет создан с правами учетной записи, от имени которой работает пул приложений в IIS. Здесь надо не забывать это и учитывать при реализации.

CRUD операции

Теперь очередь для операций с данными Create, Read, Update, Delete (CRUD).

Создание/Сохранение элемента

Создание и сохранение элементов в репозитории будет основано на присоединении сущности к контексту.

  1. /// <summary>
  2. /// Сохранение элемента списка/библиотеки
  3. /// </summary>
  4. /// <param name="entity">Элемент списка/библиотеки</param>
  5. public TEntity SaveEntity(TEntity entity)
  6. {
  7.     if (!entity.Id.HasValue)
  8.         entity.EntityState = EntityState.ToBeInserted;
  9.     CurrentList.Attach(entity);
  10.     CurrentContext.SubmitChanges();
  11.     return entity;
  12. }

Здесь, в случае создания новой записи (если у объекта отсутствует идентификатор (поле Id)) надо указать его состояние равным EntityState.ToBeInserted.

Удаление элемента

Удаление элементов списков/библиотек документов в Linq to SharePoint достаточно просто, для этого необходимо просто передать методу DataContext.DeleteOnSubmit объект или коллекцию объектов, подлежащих удалению.

  1. /// <summary>
  2. /// Удаление элемента списка/библиотеки
  3. /// </summary>
  4. /// <param name="id">Id элемента</param>
  5. public void DeleteEntity(int id)
  6. {
  7.     var query = CurrentList
  8.         .ScopeToFolder(string.Empty, true)
  9.         .Where(entry => entry.Id == id);
  10.     var entity = query.FirstOrDefault();
  11.     if (entity != null)
  12.     {
  13.         CurrentList.DeleteOnSubmit(entity);
  14.     }
  15.     CurrentContext.SubmitChanges();
  16. }

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

Чтение данных

Это самое простое в Linq to SharePoint. В случае чтения одной записи, выбирать её достаточно по полю Id, т.к. оно уникально в пределах одного списка:

  1. /// <summary>
  2. /// Получение элемента списка/библиотеки
  3. /// </summary>
  4. /// <param name="id">Id элемента</param>
  5. public TEntity GetEntity(int id)
  6. {
  7.     var query = CurrentList
  8.         .ScopeToFolder(string.Empty, true)
  9.         .Where(entry => entry.Id == id);
  10.     return query.FirstOrDefault();
  11. }

В случае чтения коллекции объектов методов будет несколько больше:

  1. /// <summary>
  2. /// Получение коллекции объектов из всех папок списка
  3. /// </summary>
  4. public IQueryable<TEntity> GetEntityCollection()
  5. {
  6.     return GetEntityCollection(entry => true);
  7. }
  8.  
  9. /// <summary>
  10. /// Получение коллекции объектов из всех папок списка
  11. /// </summary>
  12. /// <param name="expression">Предикат</param>
  13. public IQueryable<TEntity> GetEntityCollection(
  14.     Expression<Func<TEntity, bool>> expression)
  15. {
  16.     return GetEntityCollection(expression, string.Empty, true, 0);
  17. }
  18.  
  19. /// <summary>
  20. /// Получение коллекции объектов из указанной папки её и дочерних папок
  21. /// </summary>
  22. /// <param name="expression">Предикат</param>
  23. /// <param name="path">Папка</param>
  24. public IQueryable<TEntity> GetEntityCollection(
  25.     Expression<Func<TEntity, bool>> expression, string path)
  26. {
  27.     return GetEntityCollection(expression, path, true, 0);
  28. }
  29.  
  30. /// <summary>
  31. /// Получение коллекции объектов из указанной папки
  32. /// </summary>
  33. /// <param name="expression">Предикат</param>
  34. /// <param name="path">Папка</param>
  35. /// <param name="recursive">Выбор из дорчерних папок</param>
  36. public IQueryable<TEntity> GetEntityCollection(
  37.     Expression<Func<TEntity, bool>> expression,
  38.     string path, bool recursive)
  39. {
  40.     return GetEntityCollection(expression, path, recursive, 0);
  41. }
  42.  
  43. /// <summary>
  44. /// Получение коллекции объектов из указанной папки
  45. /// </summary>
  46. /// <param name="expression">Предикат</param>
  47. /// <param name="path">Папка</param>
  48. /// <param name="recursive">Выбор из дорчерних папок</param>
  49. /// <param name="maxRows">Максимальное кол-во строк</param>
  50. public IQueryable<TEntity> GetEntityCollection(
  51.     Expression<Func<TEntity, bool>> expression,
  52.     string path, bool recursive, int maxRows)
  53. {
  54.     var query = CurrentList
  55.         .ScopeToFolder(path, recursive)
  56.         .Where(expression);
  57.     if (maxRows > 0)
  58.         query = query.Take(maxRows);
  59.     return query;
  60. }

Здесь важно то, что методы в качестве предиката принимают параметр типа Expression, т.к. он хранит все дерево запросов и потому поддается анализу Linq to SharePoint. Если же передать запрос в виде Func, Linq to SharePoint неявно запросит все данные их списка. Подробнее можно почитать в посте о реализации аналоги T-SQL'ного оператора IN.

Доступ к объектной модели SharePoint

Механизм доступа к объектной модели я писал в посте о получении мета-данных списка. Класс представляющий мета-данные списка содержат свойство List, возвращающее тот самый SPList:

  1. /// <summary>
  2. /// Мета-данные списка/библиотеки
  3. /// </summary>
  4. public EntityListMetaData MetaData
  5. {
  6.     get
  7.     {
  8.         return EntityListMetaData.GetMetaData(CurrentList);
  9.     }
  10. }

Частный случай репозитория

Теперь, используя базовый класс репозитория можно с легкостью создавать свои репозитории для работы со списками/библиотеками документов SharePoint. Вот, для примера, репозиторий для работы с элементами списка Employees:

  1. public sealed class EmployeeRepository 
  2.     : BaseRepository<Employee, ZhukDataContext>
  3. {
  4.     /// <summary>
  5.     /// Название списка
  6.     /// </summary>
  7.     private const string EmployeeListName = "Employees";
  8.  
  9.     public EmployeeRepository() 
  10.         : base(EmployeeListName) { }
  11.     public EmployeeRepository(bool readOnly) 
  12.         : base(EmployeeListName, readOnly) { }
  13.     public EmployeeRepository(string webUrl) 
  14.         : base(EmployeeListName, webUrl) { }
  15.     public EmployeeRepository(string webUrl, bool readOnly) 
  16.         : base(EmployeeListName, webUrl, readOnly) { }
  17.  
  18.     // Дополнительный метод
  19.     /// <summary>
  20.     /// Получение сотрудников
  21.     /// </summary>
  22.     /// <param name="managerId">Id сотрудника</param>
  23.     public IEnumerable<Employee> GetEmployees(int managerId)
  24.     {
  25.         return GetEntityCollection(emp => emp.ManagerId == managerId);
  26.     }
  27. }

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

  1. var repository = new EmployeeRepository("http://sharepointserver");
  2.  
  3. // Получение сотрудника
  4. var employee = repository.GetEntity(1);
  5.             
  6. // Создание и сохранение сотрудника 
  7. var employeeNew = new Employee {Title = "Иванов Иван Иванович"};
  8. repository.SaveEntity(employeeNew);
  9.  
  10. // Удаление сотрудника
  11. repository.DeleteEntity(2);
  12.  
  13. // Вызов дополнительного метода
  14. var empList = repository.GetEmployees(1);
  15.  
  16. // Получение мета-данных списка без инициализации SPWeb и прочего
  17. var md = repository.MetaData;
  18.  
  19. // Получение списка полей
  20. var fields = md.Fields.Where(f => f.Hidden == false);
Виталий Жуков

Виталий Жуков

SharePoint архитектор, разработчик, тренер, Microsoft MVP (Office Development). Более 15 лет опыта работы с SharePoint, Dynamics CRM, Office 365, и другими продуктами и сервисами Microsoft.

Смотрите также

EntityFramework. Оптимистические блокировки

EntityFramework. Оптимистические блокировки

Linq to Sharepoint. Особенности

Linq to Sharepoint. Особенности

SharePoint 2010. Настройка входящей почты для кастомного списка

SharePoint 2010. Настройка входящей почты для кастомного списка

SharePoint 2010. PeopleEditor. Установка значения

SharePoint 2010. PeopleEditor. Установка значения

Linq to SharePoint. Особенности. Часть 2

Linq to SharePoint. Особенности. Часть 2