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);

Поделиться

Коментарии