Итак, было принято решение написать обертку над QueryExpression и добавить возможность построения fluent запросов как в EF. Сразу оговорюсь, что при написании этой обертки (где-то в середине) я нашел библиотеку из sdk которая предоставляет такую возможность — sdk crm client но посмотрев на нее повнимательнее я понял что там нет документации(!!!) и нескольких полезных возможностей, например: использование in в where, добавление условий к join и еще несколько помельче. Сравнительную таблицу приведу позже.
Так как проект обещает быть долгим, я все же решил дописать свою реализацию…
Задачи:
- Дать возможность определить DataContract для нужных сущностей CRM, чтобы при изменении/удалении/добавлении полей нужно было учитывать это только в одном месте
- Предоставить возможность написания fluent запросов к CRM для поддержки строгой типизации, и выявления максимального количества проблем от удаления или изменения типа поля на этапе компиляции
- Предоставить возможность настраивать result set получаемый от CRM чтобы было удобно минимизировать трафик (Select, Join, Where и т.д.)
- Обеспечить безопасность и ролевой доступ к данным в CRM (ASP.NET Impersonation в моем случае)
Что в итоге получилось:
Проект представляет из себя сборку, всего с одной дополнительной зависимостью — microsoft.xrm.sdk.dll, которую просто подключить к проекту.
Клиент
Сборка предоставляет абстрактный базовый класс для создания клиента — CrmClientBase. В этом классе одно абстрактное поле, которое должно быть переопределено:
protected abstract IWcfCrmClient WcfClient { get; }
IWcfCrmClient — это интерфейс взаимодействия с добавленным к проекту WCF клиентом (Service Reference).
Как создать класс клиента лучше показать на примере (В большинстве случаев достаточно его просто скопировать в проект, подправить using и все должно заработать):
using System; using CrmClient; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; using MsCrmClientTest.MSCRM; public class OrgCrmClient : CrmClientBase { private class WcfCrmClient : IWcfCrmClient { private OrganizationServiceClient _client; public Guid Create(Entity entity) { return _client.Create(entity); } public void Update(Entity entity) { _client.Update(entity); } public void Delete(string entityName, Guid id) { _client.Delete(entityName, id); } public EntityCollection RetrieveMultiple(QueryBase query) { return _client.RetrieveMultiple(query); } public OrganizationResponse Execute(OrganizationRequest request) { return _client.Execute(request); } public void Close() { _client.Close(); } public WcfCrmClient() { _client = new OrganizationServiceClient(); } } private IWcfCrmClient _wcfClient; protected override IWcfCrmClient WcfClient { get { if (_wcfClient == null) _wcfClient = new WcfCrmClient(); return _wcfClient; } } }
OrganizationServiceClient — это клиент из Service Reference
Mapping
Для работы с сущностями CRM нужно их замапить на классы (определить data contract). Для этого есть 2 атрибута (стандартные атрибуты из сборки microsoft.xrm.sdk.dll)
- EntityLogicalNameAttribute(string name) — определяет имя сущности в CRM
- AttributeLogicalNameAttribute(string name) — определяет имя поля сущности в CRM
Если атрибуты не заданы, то в качестве имя сущности/поля в CRM используются имя класса/имя свойства.
Каждый data contract должен быть унаследован от базового класса CrmDataContractBase. Это абстрактный класс с одним абстрактным свойством
public abstract Guid Id { get; set; }
которое нужно переопределить и тоже пометить атрибутом AttributeLogicalName.
Пример data contract:
[EntityLogicalName("systemuser")] public class User : CrmDataContractBase { [AttributeLogicalName("systemuserid")] public override Guid Id { get; set; } [AttributeLogicalName("fullname")] public string Name { get; set; } [AttributeLogicalName("parentsystemuserid")] public EntityReference Сhief { get; set; } [AttributeLogicalName("caltype")] public OptionSetValue CALType }
ВАЖНО!
- Если свойство является ссылкой на другую сущность CRM (как Сhief в примере) оно должно быть типа EntityReference
- Если свойство является значением из перечисления CRM (как CALType в примере) оно должно быть типа OptionSetValue
Mapping перечислений CRM
Чтобы замапить перечисления CRM, нужно определить класс, унаследовать его от CrmOptionsSetBase и пометить его атрибутом EntityLogicalName, в котором указать имя перечисления в CRM:
[EntityLogicalName("connectionrole_category")] public class ConnectionRoleCategoryEnum : CrmOptionsSetBase { }
CrmOptionsSetBase реализует интерфейс IEnumerable типа CrmOption, т.е. им сразу можно пользоваться как источником данных для контролов.
Класс CrmOption содержит 2 свойства:
public string Label { get; private set; } public OptionSetValue Value { get; private set; }
В Label содержится отображаемое имя элемента, а Value это тот самый OptionSetValue, используемый в data contract сущностей CRM
Использование клиента
Добавление, изменение, удаление сущностей CRM
Это простые операции, все должно быть понятно из примера:
[EntityLogicalName("new_nsi")] public class NSI : ICrmDataContract { [AttributeLogicalName("new_nsiid")] public override Guid Id { get; set; } [AttributeLogicalName("new_name")] public string Name { get; set; } } //Добавление var newnsi = new NSI { Name = "Test NSI" }; _client.Add(newnsi); //Метод 'Add' проставляет свойство 'Id' для добавленной сущности, как в EF //Изменение newnsi.Name = "Test NSI 2"; _client.Update(newnsi); //Удаление _client.Delete(newnsi);
ВАЖНО! Все изменения применяются в CRM сразу же. Транзакционности нет (не разбирался с этим еще)
Получение перечислений CRM
Для получения перечислений у клиента есть специальный метод
public T OptionsSet<T>()
Где T — data contract перечисления. Пример (data contarct описан выше):
var optionSet = _client.OptionsSet<ConnectionRoleCategoryEnum>();
Запросы к CRM с использованием Linq
В namespace CrmClient.Linq определены следующие методы-расширения, предназначенные для формирования fluent запросов к CRM:
- CrmSelect
- CrmWhere
- CrmJoin
- CrmOrder
- CrmPaging
- CrmDistinct
Методы из этого namespace начинаются с Crm, чтобы сразу было видно где формируется запрос к CRM а где идет работа с уже выгруженными объектами.
Стартовый метод формирования запроса к CRM — Query:
public ICrmQueryable<T> Query<T>()
после него могут использоваться остальные методы формирования запроса.
Сам запрос к CRM выполняется. при вызове метода GetEnumerator(), т.е при попытке перечисления данных (как в EF).
Select
Анонимный тип
var users = _client.Query<CrmUser>() .CrmSelect(u => new { u.Id, u.Name, Test = 1 }) .ToList();
Класс (как и в EF, у класса должен быть конструктор без паарметров)
var users2 = _client.Query<CrmUser>() .CrmSelect(u => new TestUser() { Id = u.Id, FullName = u.Name, Test = 1 }) .ToList();
Where
//простой var user = _client.Query<CrmUser>() .CrmWhere(i => i.Id == _directorUserId) .Single(); var list = new[] { _directorUserId }; // in var filteredUsers = _client.Query<CrmUser>() .CrmWhere(i => list.Contains(i.Id)) .ToList(); // not in filteredUsers = _client.Query<CrmUser>() .CrmWhere(i => !list.Contains(i.Id)) .ToList(); //like var users = _client.Query<CrmUser>().ToList(); var firstUser = users.First(i => i.Name.Contains(" ")).Name.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); // like text% var user = _client.Query<CrmUser>() .CrmWhere(i => i.Name.StartsWith(firstUser[0])) .ToList(); // like %text user = _client.Query<CrmUser>() .CrmWhere(i => i.Name.EndsWith(firstUser[0])) .ToList(); // like %text% user = _client.Query<CrmUser>() .CrmWhere(i => i.Name.Contains(firstUser[0])) .ToList(); // not like text% user = _client.Query<CrmUser>() .CrmWhere(i => !i.Name.StartsWith(firstUser[0])) .ToList(); // not like %text user = _client.Query<CrmUser>() .CrmWhere(i => !i.Name.EndsWith(firstUser[0])) .ToList(); // not like %text% user = _client.Query<CrmUser>() .CrmWhere(i => !i.Name.Contains(firstUser[0])) .ToList();
ВАЖНО! Для конструкции ‘in’ нужно передавать только массив (array). Это ограничение я уберу чуть позже.
Так же есть поддержка составных условий (как в моем расширении для EF Условие «WHERE» по составным ключам в Entity Framework):
var users = _client.Query<CrmUser>().ToList(); var directors = users.Where(u => u.Director != null).Select(u => new { u.Director.Id, u.Director.Name }).Take(2); var users2 = _client.Query<CrmUser>() .CrmWhere(ExpressionType.Or, directors, (u, d) => u.Id == d.Id && u.Name == d.Name, (pn, o) => { switch (pn) { case "Id": return o.Id; case "Name": return o.Name; default: return null; } }) .ToList();
Order
var users = _client.Query<CrmUser>() .CrmOrderBy(i => i.Name) .CrmOrderByDescending(i => i.Id) .ToList();
Результат будет отсортирован по Name asc, потом по Id desc
Distinct
var users = _client.Query<CrmUser>() .CrmDistinct() .ToList();
Join
C Join дела обстоят чуть посложнее… Например, нужно сделать join пользователей на своих руководителей:
var users = _client.Query<CrmUser>() .CrmJoin(_client.Query<CrmUser>(), s => s.Chief.Id, d => d.Id, (s, d) => new { s.Id, s.Name, ChiefId = d.Id, ChiefFullName = d.Name }) .ToList(); or var users = _client.Query<CrmUser>() .CrmLeftJoin(_client.Query<CrmUser>(), s => s.Chief.Id, d => d.Id, (s, d) => new { s.Id, s.Name, ChiefId = d.Id, ChiefFullName = d.Name }) .ToList();
Запрос выполнится правильно. Но если присмотреться внимательнее, свойство Id у s.Chief не замаплено никуда, так как это свойство класса EntityReference… Но само свойство s.Chief замаплено на нужное нам ‘parentsystemuserid’ CRM… Клиент сам разруливает данную ситуацию, подставляя в запрос к CRM ‘parentsystemuserid’ из s.Chief, а запись s => s.Chief.Id, d => d.Id нужна для совместимости типов.
Еще одна проблема, это указание условий на join запрос. В запросе CRM это условие указывается в самом классе Link, так что для указания этого условия нужно его прописать в самом join запросе:
var users = _client.Query<CrmUser>() .CrmJoin(_client.Query<CrmUser>().CrmWhere(u => u.Id == _directorUserId), s => s.Chief.Id, d => d.Id, (s, d) => new { s.Id, s.Name, ChiefId = d.Id, ChiefFullName = d.Name }) .ToList(); or var users = _client.Query<CrmUser>() .CrmLeftJoin(_client.Query<CrmUser>().CrmWhere(u => u.Id == _directorUserId), s => s.Chief.Id, d => d.Id, (s, d) => new { s.Id, s.Name, ChiefId = d.Id, ChiefFullName = d.Name }) .ToList();
_client.Query().CrmWhere(u => u.Id == _directorUserId) — это и есть условие на join. Т.е. если это inner join то вернутся только пользователи, у которых директор с id _directorUserId. Здесь можно указать и другие условия, например Order, но это не даст никакого эффекта, учитывается только условие Where.
NoLock
Этот метод нужен для того, чтобы запросы на стороне CRM выполнялись с опцией with(nolock) полезно для получения данных для отчета за прошедший период. Пример:
var users = _client.Query<CrmUser>() .CrmNoLock() .ToList();
Paging
CRM запрос поддерживает постраничную выдачу результатов. Для выполнения постраничного запроса есть метод
List<T> CrmGetPage(int pageNumber, int pageSize, out int totalCount, out bool moreRecordsExists)
Он возвращает сразу List, это означает что его вызов сразу выполняет запрос к CRM.
Этот метод возвращает общее количество записей, и признак что еще есть данные для получения на стороне CRM. Пример:
int total; bool moreExists; var users = _client.Query<CrmUser>().CrmGetPage(1, 10, out total, out moreExists);
(Все примеры можно найти в тестовом проекте)
Особенности клиента
В клиенте используется reflection только для первоначально получения данных о mapping, и эти данные кэшируются в памяти. Из-за этого первый запрос будет выполняться медленно…
Код создания экземпляров классов компилируется динамически, так что время получение результата зависит только от времени выполнения запроса на стороне CRM и сетевых задержек. Код создания типов компилируется в памяти — одна сборка на каждый тип. Но здесь есть одна оговорка: создание анонимных типов происходит через конструктор через reflection. Это обусловлено тем, что в разных сборках анонимные типы имеют разный внутренний тип и приведение невозможно. Если кто знает как преодолеть это ограничение, напишите пожалуйста, я это поправлю.
Сравнение с клиентом из SDK
По скорости этот клиент и клиент из SDK практически одинаковы (разница в пределах сетевых задержек при выполнении теста). Из-за небольшого объема данных на тестовой CRM я так и не выяснил все ли клиент из SDK делает на стороне CRM или что-то уже на стороне приложения (например сортировка…), из кода теста этого не понять, так как он использует стандартный IQueryable и стандартные методы-расширения Linq.
Сам сравнительный тест есть в исходниках. Результаты его выполнения следующие:
Operation ThisCrmClient SdkCrmClient Query 00:00:25.6005598 00:01:01.1291123 Select 00:00:03.5173517 00:00:03.6273627 Order 00:00:08.2558255 00:00:08.2338233 Where 00:00:04.1074107 00:00:03.9203920 WhereIn 00:00:05.3745374 not supported %Like% 00:00:03.3983398 00:00:03.4093409 %Like 00:00:03.4403440 00:00:03.4163416 Like% 00:00:03.3093309 00:00:03.3033303 Join 00:00:03.4313431 00:00:03.4143414 JoinFilter 00:00:03.3833383 not supported NoLock 00:00:09.6899689 not supported Distinct 00:00:07.9847984 00:00:08.0328032
Сборки и исходники
Закачать сборку и посмотреть исходники можно на codeplex — mscrmclient. Solution состоит из 3х проектов:
- MsCrmClient — сам клиент
- MsCrmClientTest — тесты для клиента
- PerfomanceTest — консольное приложение, для сравнения производительности с кkиентом из SDK
Вместо заключения
На codeplex я писал документацию на английском, русская версия документации — эта статья.
Все ошибки, которые я найду в процессе использования этого клиента, я буду сразу править и выкладывать обновление на codeplex. Если Вы решите использовать этот клиент в своих проектах, и обнаружите ошибку, создайте issue на странице проекта. Я подписался на получение уведомлений. Все ошибки я буду стараться исправлять в кратчайшие сроки (это в моих интересах хотя бы потому, что я тоже использую этот клиент в своих проектах).
ссылка на оригинал статьи http://habrahabr.ru/post/177273/
Добавить комментарий