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'у и прочее.
Итак, вышеописанное в виде диаграммы классов:
При инициализации репозитория мы будем создавать контекст для работы с данными, определять является ли пользователь анонимным и инициализировать загрузку информации о списке (EntityList<TEntity>):
- /// <summary>
- /// Базовый класс репозитория
- /// </summary>
- /// <typeparam name="TEntity">Тип сущности</typeparam>
- /// <typeparam name="TContext">Тип контекста данных</typeparam>
- public abstract class BaseRepository<TEntity, TContext>
- where TEntity : ZhukDataItem, new()
- where TContext : DataContext
- {
- protected readonly string WebUrl;
- protected readonly string ListName;
- protected readonly bool ReadOnly;
- public readonly bool IsAnonymous;
-
- /// <summary>
- /// Инициализация репозитория
- /// </summary>
- /// <param name="listName">Название списка</param>
- /// <param name="webUrl">Url сайта</param>
- /// <param name="readOnly">Режим "Только для чтения"</param>
- protected BaseRepository(string listName, string webUrl, bool readOnly)
- {
- ReadOnly = readOnly;
- ListName = listName;
- WebUrl = webUrl;
-
- var ctx = SPContext.Current;
- IsAnonymous = ctx != null && SPContext.Current.Web.CurrentUser == null;
-
- InitializeParameters();
- }
-
- /// <summary>
- /// Инициализация репозитория для текущего сайта
- /// </summary>
- /// <param name="listName">Название списка</param>
- /// <param name="readOnly">Режим "Только для чтения"</param>
- protected BaseRepository(string listName, bool readOnly)
- : this(listName,
- SPContext.Current.Web.Url, readOnly)
- { }
-
- /// <summary>
- /// Инициализация репозитория для текущего сайта в режиме "Только для чтения"
- /// </summary>
- /// <param name="listName">Название списка</param>
- protected BaseRepository(string listName)
- : this(listName, true)
- { }
-
- /// <summary>
- /// Инициализация репозитория в режиме "Только для чтения"
- /// </summary>
- /// <param name="listName">Название списка</param>
- /// <param name="webUrl">Url сайта</param>
- protected BaseRepository(string listName, string webUrl)
- : this(listName, webUrl, true)
- { }
-
- /// <summary>
- /// Инициализация параметров репозитория
- /// </summary>
- private void InitializeParameters()
- {
- if (IsAnonymous)
- {
- RunAsAdmin(() =>
- {
- CurrentContext =
- (TContext)Activator.CreateInstance(typeof(TContext),
- new object[] { WebUrl });
- CurrentContext.ObjectTrackingEnabled = !ReadOnly;
- CurrentList = CurrentContext.GetList<TEntity>(ListName);
- });
- }
- else
- {
- CurrentContext =
- (TContext)Activator.CreateInstance(typeof(TContext),
- new object[] { WebUrl });
- CurrentContext.ObjectTrackingEnabled = !ReadOnly;
- CurrentList = CurrentContext.GetList<TEntity>(ListName);
- }
- }
-
- /// <summary>
- /// Контекст данных
- /// </summary>
- private TContext CurrentContext { get; set; }
-
- /// <summary>
- /// Список/библиотека элементов
- /// </summary>
- private EntityList<TEntity> CurrentList { get; set; }
-
- //... Прочие методы
- }
Если пользователь анонимен, то контекст данных будет создан с правами учетной записи, от имени которой работает пул приложений в IIS. Здесь надо не забывать это и учитывать при реализации.
CRUD операции
Теперь очередь для операций с данными Create, Read, Update, Delete (CRUD).
Создание/Сохранение элемента
Создание и сохранение элементов в репозитории будет основано на присоединении сущности к контексту.
- /// <summary>
- /// Сохранение элемента списка/библиотеки
- /// </summary>
- /// <param name="entity">Элемент списка/библиотеки</param>
- public TEntity SaveEntity(TEntity entity)
- {
- if (!entity.Id.HasValue)
- entity.EntityState = EntityState.ToBeInserted;
- CurrentList.Attach(entity);
- CurrentContext.SubmitChanges();
- return entity;
- }
Здесь, в случае создания новой записи (если у объекта отсутствует идентификатор (поле Id)) надо указать его состояние равным EntityState.ToBeInserted.
Удаление элемента
Удаление элементов списков/библиотек документов в Linq to SharePoint достаточно просто, для этого необходимо просто передать методу DataContext.DeleteOnSubmit объект или коллекцию объектов, подлежащих удалению.
- /// <summary>
- /// Удаление элемента списка/библиотеки
- /// </summary>
- /// <param name="id">Id элемента</param>
- public void DeleteEntity(int id)
- {
- var query = CurrentList
- .ScopeToFolder(string.Empty, true)
- .Where(entry => entry.Id == id);
- var entity = query.FirstOrDefault();
- if (entity != null)
- {
- CurrentList.DeleteOnSubmit(entity);
- }
- CurrentContext.SubmitChanges();
- }
В случае удаления элемента из списка, достаточно получить его, использую "минимальный" тип содержимого и затем удалить.
Чтение данных
Это самое простое в Linq to SharePoint. В случае чтения одной записи, выбирать её достаточно по полю Id, т.к. оно уникально в пределах одного списка:
- /// <summary>
- /// Получение элемента списка/библиотеки
- /// </summary>
- /// <param name="id">Id элемента</param>
- public TEntity GetEntity(int id)
- {
- var query = CurrentList
- .ScopeToFolder(string.Empty, true)
- .Where(entry => entry.Id == id);
- return query.FirstOrDefault();
- }
В случае чтения коллекции объектов методов будет несколько больше:
- /// <summary>
- /// Получение коллекции объектов из всех папок списка
- /// </summary>
- public IQueryable<TEntity> GetEntityCollection()
- {
- return GetEntityCollection(entry => true);
- }
-
- /// <summary>
- /// Получение коллекции объектов из всех папок списка
- /// </summary>
- /// <param name="expression">Предикат</param>
- public IQueryable<TEntity> GetEntityCollection(
- Expression<Func<TEntity, bool>> expression)
- {
- return GetEntityCollection(expression, string.Empty, true, 0);
- }
-
- /// <summary>
- /// Получение коллекции объектов из указанной папки её и дочерних папок
- /// </summary>
- /// <param name="expression">Предикат</param>
- /// <param name="path">Папка</param>
- public IQueryable<TEntity> GetEntityCollection(
- Expression<Func<TEntity, bool>> expression, string path)
- {
- return GetEntityCollection(expression, path, true, 0);
- }
-
- /// <summary>
- /// Получение коллекции объектов из указанной папки
- /// </summary>
- /// <param name="expression">Предикат</param>
- /// <param name="path">Папка</param>
- /// <param name="recursive">Выбор из дорчерних папок</param>
- public IQueryable<TEntity> GetEntityCollection(
- Expression<Func<TEntity, bool>> expression,
- string path, bool recursive)
- {
- return GetEntityCollection(expression, path, recursive, 0);
- }
-
- /// <summary>
- /// Получение коллекции объектов из указанной папки
- /// </summary>
- /// <param name="expression">Предикат</param>
- /// <param name="path">Папка</param>
- /// <param name="recursive">Выбор из дорчерних папок</param>
- /// <param name="maxRows">Максимальное кол-во строк</param>
- public IQueryable<TEntity> GetEntityCollection(
- Expression<Func<TEntity, bool>> expression,
- string path, bool recursive, int maxRows)
- {
- var query = CurrentList
- .ScopeToFolder(path, recursive)
- .Where(expression);
- if (maxRows > 0)
- query = query.Take(maxRows);
- return query;
- }
Здесь важно то, что методы в качестве предиката принимают параметр типа Expression, т.к. он хранит все дерево запросов и потому поддается анализу Linq to SharePoint. Если же передать запрос в виде Func, Linq to SharePoint неявно запросит все данные их списка. Подробнее можно почитать в посте о реализации аналоги T-SQL'ного оператора IN.
Доступ к объектной модели SharePoint
Механизм доступа к объектной модели я писал в посте о получении мета-данных списка. Класс представляющий мета-данные списка содержат свойство List, возвращающее тот самый SPList:
- /// <summary>
- /// Мета-данные списка/библиотеки
- /// </summary>
- public EntityListMetaData MetaData
- {
- get
- {
- return EntityListMetaData.GetMetaData(CurrentList);
- }
- }
Частный случай репозитория
Теперь, используя базовый класс репозитория можно с легкостью создавать свои репозитории для работы со списками/библиотеками документов SharePoint. Вот, для примера, репозиторий для работы с элементами списка Employees:
- public sealed class EmployeeRepository
- : BaseRepository<Employee, ZhukDataContext>
- {
- /// <summary>
- /// Название списка
- /// </summary>
- private const string EmployeeListName = "Employees";
-
- public EmployeeRepository()
- : base(EmployeeListName) { }
- public EmployeeRepository(bool readOnly)
- : base(EmployeeListName, readOnly) { }
- public EmployeeRepository(string webUrl)
- : base(EmployeeListName, webUrl) { }
- public EmployeeRepository(string webUrl, bool readOnly)
- : base(EmployeeListName, webUrl, readOnly) { }
-
- // Дополнительный метод
- /// <summary>
- /// Получение сотрудников
- /// </summary>
- /// <param name="managerId">Id сотрудника</param>
- public IEnumerable<Employee> GetEmployees(int managerId)
- {
- return GetEntityCollection(emp => emp.ManagerId == managerId);
- }
- }
Минимум дополнительного кода и максимум функциональности. Напоследок несколько строк кода, демонстрирующих использования репозитория для работы с элементами списка:
- var repository = new EmployeeRepository("http://sharepointserver");
-
- // Получение сотрудника
- var employee = repository.GetEntity(1);
-
- // Создание и сохранение сотрудника
- var employeeNew = new Employee {Title = "Иванов Иван Иванович"};
- repository.SaveEntity(employeeNew);
-
- // Удаление сотрудника
- repository.DeleteEntity(2);
-
- // Вызов дополнительного метода
- var empList = repository.GetEmployees(1);
-
- // Получение мета-данных списка без инициализации SPWeb и прочего
- var md = repository.MetaData;
-
- // Получение списка полей
- var fields = md.Fields.Where(f => f.Hidden == false);