- PVSM.RU - https://www.pvsm.ru -
При разработке одного из проектов, мне понадобилась интеграция с MS CRM… Посмотрев на стандартные механизмы запросов в msdn я понял, что это немного неудобно, а так как проект обещает быть длинным, и даже бесконечным (внутренняя автоматизация), я решил что использование QueryExpression в чистом виде приведет к серьезному увеличению трудозатрат и станет рассадником ошибок по невнимательности (так как разработчики по проекту будут частенько меняться — у кого есть время, тот и занимается).
Итак, было принято решение написать обертку над QueryExpression и добавить возможность построения fluent запросов как в EF. Сразу оговорюсь, что при написании этой обертки (где-то в середине) я нашел библиотеку из sdk которая предоставляет такую возможность — sdk crm client [1] но посмотрев на нее повнимательнее я понял что там нет документации(!!!) и нескольких полезных возможностей, например: использование in в where, добавление условий к join и еще несколько помельче. Сравнительную таблицу приведу позже.
Так как проект обещает быть долгим, я все же решил дописать свою реализацию…
Проект представляет из себя сборку, всего с одной дополнительной зависимостью — 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
Для работы с сущностями CRM нужно их замапить на классы (определить data contract). Для этого есть 2 атрибута (стандартные атрибуты из сборки microsoft.xrm.sdk.dll)
Если атрибуты не заданы, то в качестве имя сущности/поля в 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, нужно определить класс, унаследовать его от 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
Это простые операции, все должно быть понятно из примера:
[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 сразу же. Транзакционности нет (не разбирался с этим еще)
Для получения перечислений у клиента есть специальный метод
public T OptionsSet<T>()
Где T — data contract перечисления. Пример (data contarct описан выше):
var optionSet = _client.OptionsSet<ConnectionRoleCategoryEnum>();
В namespace CrmClient.Linq определены следующие методы-расширения, предназначенные для формирования fluent запросов к CRM:
Методы из этого namespace начинаются с Crm, чтобы сразу было видно где формируется запрос к CRM а где идет работа с уже выгруженными объектами.
Стартовый метод формирования запроса к CRM — Query:
public ICrmQueryable<T> Query<T>()
после него могут использоваться остальные методы формирования запроса.
Сам запрос к CRM выполняется. при вызове метода GetEnumerator(), т.е при попытке перечисления данных (как в EF).
Анонимный тип
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();
//простой
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 [2]):
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();
var users = _client.Query<CrmUser>()
.CrmOrderBy(i => i.Name)
.CrmOrderByDescending(i => i.Id)
.ToList();
Результат будет отсортирован по Name asc, потом по Id desc
var users = _client.Query<CrmUser>()
.CrmDistinct()
.ToList();
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.
Этот метод нужен для того, чтобы запросы на стороне CRM выполнялись с опцией with(nolock) полезно для получения данных для отчета за прошедший период. Пример:
var users = _client.Query<CrmUser>()
.CrmNoLock()
.ToList();
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 практически одинаковы (разница в пределах сетевых задержек при выполнении теста). Из-за небольшого объема данных на тестовой 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 [3]. Solution состоит из 3х проектов:
На codeplex я писал документацию на английском, русская версия документации — эта статья.
Все ошибки, которые я найду в процессе использования этого клиента, я буду сразу править и выкладывать обновление на codeplex. Если Вы решите использовать этот клиент в своих проектах, и обнаружите ошибку, создайте issue на странице проекта. Я подписался на получение уведомлений. Все ошибки я буду стараться исправлять в кратчайшие сроки (это в моих интересах хотя бы потому, что я тоже использую этот клиент в своих проектах).
Автор: setsergey
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/33296
Ссылки в тексте:
[1] sdk crm client: http://rostacik.net/2011/08/11/how-to-read-create-update-and-detele-objects-in-microsoft-crm-2011-on-premise-via-wcf-service/
[2] Условие «WHERE» по составным ключам в Entity Framework: http://habrahabr.ru/post/152417/
[3] mscrmclient: https://mscrmclient.codeplex.com/
[4] Источник: http://habrahabr.ru/post/177273/
Нажмите здесь для печати.