Клиент с поддержкой Linq для Microsoft Dynamics CRM (альтернатива клиенту из sdk)

от автора

При разработке одного из проектов, мне понадобилась интеграция с MS CRM… Посмотрев на стандартные механизмы запросов в msdn я понял, что это немного неудобно, а так как проект обещает быть длинным, и даже бесконечным (внутренняя автоматизация), я решил что использование QueryExpression в чистом виде приведет к серьезному увеличению трудозатрат и станет рассадником ошибок по невнимательности (так как разработчики по проекту будут частенько меняться — у кого есть время, тот и занимается).

Итак, было принято решение написать обертку над 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *