Інтеграція двох тенантов Dynamics CRM Online за допомогою Online Service Bus і Azure Cloud Service

У даній статті мені хотілося б поділитися досвідом використання Microsoft Azure для інтеграції двох хмарних CRM систем. В рамках задачі необхідно побудувати просте хмарне додаток, здійснює обмін повідомленнями між двома имплементациями Dynamics CRM Online, що знаходяться в різних підписках Office 365. Ми розглянемо специфіку використання Azure Service Bus в контексті Dynamics CRM Online, не багато поговоримо про підтримуваних механізми взаємодії і скористаємося хмарної робочої роллю для здійснення процесу аналізу і обробки повідомлень.

При реалізації інтеграції, що забезпечує дві філії компанії важливими та актуальними даними, нам необхідно забезпечити доставку повідомлень, навіть у тому випадку, якщо одна із систем не відповідає на запити. Природно, що в такому разі ми вдаємося до послуг систем обміну повідомленнями, які можуть забезпечити зберігання та відкладену доставку у разі відсутності підключення або будь-яких інших проблем.

Говорячи про хмара Microsoft, мені хотілося б зауважити, що сьогодні корпорація приділяє достатньо уваги інтеграції різних продуктів в одну однорідну систему, що дозволяє спростити і прискорити процес побудови і розгортання рішень, а також уникнути деяких помилок.

Якщо говорити про Dynamics CRM, то цей продукт з коробки підтримує роботу з Azure Service Bus, що дозволяє без єдиної рядки коду надсилати Ваші дані в чергу або розділ.

1. Налаштування Azure Service Bus для роботи з Dynamics CRM Online.

Тут є своя специфіка. Для того, щоб налаштувати інтеграцію між цими двома системами, Service Bus повинен щось знати про CRM, а CRM повинна коректно аутентифицироваться, використовуючи відповідні сервіси хмарної шини. На сьогоднішній день, Dynamics CRM підтримує аутентифікацію через ACS (Microsoft Active Directory Access Control). Детальніше почитати про те, що таке ACS, можна в наступні статті: Що таке ACS?
Отже, перше що нам потрібно зробити, це власне створити Service Bus, який ми будемо використовувати для роботи з нашої чергою повідомлень, але створити його простим способом через портал, на жаль, не вийде, так як в цьому випадку Service Bus не буде підтримувати аутентифікацію через ACS. Для того, щоб створити Service Bus з підтримкою ACS, скористаємося Microsoft Power Shell. Детальніше про те, що таке Microsoft Power Shell і як ним користуватися можна почитати в статті: Що таке Azure PowerShell?

[CmdletBinding(PositionalBinding=$True)]
Param(
# [Parameter(Mandatory = $true)]
# [ValidatePattern("^[a-z0-9]*$")]
[String]$Path = "q4depa2depb", # required needs to be alphanumeric 
[Bool]$EnableDeadLetteringOnMessageExpiration = $True , # optional default to false
[Int]$LockDuration = 30, # optional default to 30
[Int]$MaxDeliveryCount = 10, # optional default to 10
[Int]$MaxSizeInMegabytes = 1024, # optional default to 1024
[Bool]$SupportOrdering = $True, # optional default to true
# [Parameter(Mandatory = $true)]
# [ValidatePattern("^[a-z0-9]*$")]
[String]$Namespace = "sb4crm2crm", # required needs to be alphanumeric
[Bool]$CreateACSNamespace = $True, # optional default to $false
[String]$Location = "West Europe" # optional default to "West Europe"
)


# Create Azure Service Bus namespace
$CurrentNamespace = Get-AzureSBNamespace -Name $Namespace

if ($CurrentNamespace)
{
Write-Output "The namespace [$Namespace] already exists in the [$($CurrentNamespace.Region)] region." 
}
else
{
Write-Host "The [$Namespace] namespace does not exist."
Write-Output "Creating the [$Namespace] namespace in the [$Location] region..."
New-AzureSBNamespace -Name $Namespace -Location $Location -CreateACSNamespace $CreateACSNamespace -NamespaceType Messaging
$CurrentNamespace = Get-AzureSBNamespace -Name $Namespace
Write-Host "The [$Namespace] namespace in the [$Location] region has been successfully created."
}

$NamespaceManager = [Microsoft.ServiceBus.NamespaceManager]::CreateFromConnectionString($CurrentNamespace.ConnectionString);


if ($NamespaceManager.QueueExists($Path))
{
Write-Output "The [$Path] queue already exists in the [$Namespace] namespace." 
}
else
{
Write-Output "Creating the [$Path] in the queue [$Namespace] namespace..."
$QueueDescription = New-Object -TypeName Microsoft.ServiceBus.Messaging.QueueDescription -ArgumentList $Path

$QueueDescription.EnableDeadLetteringOnMessageExpiration = $EnableDeadLetteringOnMessageExpiration
if ($LockDuration -gt 0)
{
$QueueDescription.LockDuration = [System.TimeSpan]::FromSeconds($LockDuration)
}
$QueueDescription.MaxDeliveryCount = $MaxDeliveryCount
$QueueDescription.MaxSizeInMegabytes = $MaxSizeInMegabytes
$QueueDescription.SupportOrdering = $SupportOrdering
$NamespaceManager.CreateQueue($QueueDescription);
Write-Host "The [$Path] in the queue [$Namespace] namespace has been successfully created."


} 


Повний варіант використаного мною скрипта доступний тут.
Скрипт досить простий, параметри, які можуть використовуватися для створення черги докладно описані в статті: Online Service Bus – As I Understand It: Part II (Queues & Messages). Додам, що для коректної роботи нашої інтеграції необхідно мати чітку послідовність повідомлень, так як ми будемо обробляти повідомлення як створення, так і на зміну записів, і не хотілося б обробляти повідомлення про зміну запису до її створення. У слідстві чого не забуваємо проставити поле SupportOrdering відповідне значення, в цьому випадку чергу буде працювати за принципом FIFO (First In First Out).
Після того як скрипт успішно відпрацював на своєму екрані Ви повинні отримати щось подібне



Тепер, після того як все готово ми можемо переконатися, що черга і шина коректно створилися і доступні на порталі.



2. Підключення Dynamics CRM Online до Azure Service Bus.

Отже, для того що б підключити до Azure Dynamics CRM Service Bus необхідно відкрити Plugin Registration Tool і встановити з'єднання з CRM-системою. Після того, як відкриється список Plugins, вибираємо пункт Register та Register New Service Endpoint.



Далі, у вікні, заповнюємо параметри підключення.



Name – це назва нашого події. Як приклад: ContactIntegration.
Description – опис виклику.
Solution Namespace – назва нашої сервісної шини. В моєму випадку: sb4crm2crm
Path – назва черги, яка буде приймати повідомлення. В моєму випадку: q4depa2depb
Contract – контракт передачі повідомлень. Тут є кілька варіантів: Queue, Topic, One — way, two – way, REST. Ми будемо розглядати Queue та Topic. Докладніше про кожен з цих контрактів можна прочитати в наступній статті: Write a listener for a Microsoft Azure solution. Для нашої інтеграції вибираємо Persistent Queue.
Claim – у якості додаткової інформації в контексті повідомлення можна відправити ID користувача.
ID – унікальний ідентифікатор створеної конфігурації.

Після того, як всі поля заповнені, можна переходити до конфігурування ACS. Для цього нажимам на кнопку Save & Configure ACS.


Key Management – цей ключ можна отримати з порталу Azure. Для цього потрібно перейти в розділ сервісних шин.

Вибрати створену нами шину і клацнути по кнопці Connection Information.

Відкриється вікно, в якому Ви зможете знайти всю необхідну інформацію.

Нам потрібен Default Key з розділу ACS.
Certificate File – публічний сертифікат, який використовувався при конфігуруванні Dynamics CRM для інтеграції з Microsoft.
Issuer Name – Найменування емітента. Ім'я повинно бути те саме, яке використовувалося при конфігуруванні Dynamics CRM для інтеграції c Azure.
Certificate File і Issuer Name можна знайти у Dynamics CRM в розділі Settings -> Customizations -> Developer Resources. Виглядає наступним чином.



Завантажуємо сертифікат, заповнюємо всі необхідні поля і натискаємо на кнопку Configure ACS. Якщо все вказали коректно, то через короткий інтервал часу, Ви побачите таке повідомлення:



Після чого вікно можна закрити, натиснувши на кнопку Close.
Далі натискаємо на кнопку Save & Verify Authentication. Отримуємо повідомлення наступного вигляду:

Verifying Authentication: Success

Закриваємо вікно, натискаємо кнопку Save і готово.
Тепер залишається тільки зареєструвати, які конкретно події ми хочемо обробляти і відсилати в нашу сервісну шину.
Для цього необхідно зареєструвати Plugin Step, як Ви звичайно це робите для Ваших плагінів. Я реєструю Create і Update повідомлення для сутності Contact. Для цього досить викликати контекстне меню, тільки що створеному Service Endpoint, і вибрати Register New Step. Заповнення інтуїтивно зрозуміло.


Тепер наші створені контакти будуть відправлятися в Service Bus.

Для того що б відстежити успішність або не успішність відправки повідомлень з Dynamics CRM, досить відкрити систему і перейти в розділ Settings -> System Jobs. Вибрати цікаву сутність і завантажити подання.

Нижче наводжу скріншот з потенційною помилкою:


3. Розробка Worker Role для обробки повідомлень.

Справа залишається за малим, розробити код, який буде обробляти наші повідомлення, заливати їх в іншу систему і коректно реагувати на потенційні помилки.
Будь-який робочий процес повинен десь виконується і в нашому випадку це Azure Cloud Service.
Давайте створимо новий Azure Cloud Service в Visual Studio.


Далі вказуємо, що в контексті нашого Azure Cloud Service, ми хочемо створити Worker Role.



Тепер, коли у нас є Azure Cloud Service і Azure Worker Role, можна реалізувати код, який зможе отримувати повідомлення з нашої черги. Найпростіший варіант отримання повідомлень наведено нижче.
Будь-яка Worker Role містить три обов'язкові методу — це OnStart, Run і OnStop. Давайте розглянемо їх реалізацію в самому загальному вигляді. У методі OnStart визначаємо параметри підключення до нашої шині, тут так само можна ініціювати підключення до системи, у яку планується заливка даних.

public override bool OnStart()
{
Trace.WriteLine("Creating Queue");
string connectionString = "*** provide your connection string here***";
var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);
// Ініціалізація підключення до службової шині
Client = QueueClient.CreateFromConnectionString(connectionString, QueueName);
return base.OnStart();
}


Метод Run, найбільш цікавий, так як тут ми погоджуємося на отримання повідомлень з нашої черги і виробляємо налаштування методу отримання даних.

public override void Run()
{
OnMessageOptions options = new OnMessageOptions();
options.AutoComplete = true; // Повідомлення буде автоматично позначатися як відпрацьований і видалятися з черги після завершення виконання методу receivedMessage
options.MaxConcurrentCalls = 1; // Вказує максимальну кількість одночасних викликів функції зворотного виклику 
options.ExceptionReceived += LogErrors; // Обробник помилок
// Start receiveing messages
Client.OnMessage((receivedMessage) => // Даний метод буде викликатися для кожного отриманого повідомлення
{
try
{
// Виконуємо обробку повідомлення
Trace.WriteLine("Processing Service Bus message: " + receivedMessage.SequenceNumber.ToString());
}
catch
{
// Перехватываем помилки виникли в процесі обробки повідомлення
}
}, options);
CompletedEvent.WaitOne();
}


Код забезпечений достатньо докладними коментарями, так що додатково тут коментувати нічого не буду.
Ну, і на останок, подивимося як виглядає метод OnStop.

public override void OnStop()
{
Client.Close();
CompletedEvent.Set(); //Завершуємо виконання Run функції
base.OnStop();
}

Тут ми закриваємо всі можливі підключення і закінчуємо виконання функції Run. Детальніше про Azure Cloud Service, Ви можете прочитати в наступній статті: Докладний опис можливостей розробки Microsoft Azure Cloud Services
Так само варто зауважити, що публікацію робочої ролі можна виконати в двох варіаціях: Staging і Production. Якщо Ви публікуєте Вашу роль з типом складання Debug, то в результаті отримаєте — Staging розгортання, якщо використовуєте тип складання Release, то Production. Навіть за умови, що роль опублікована і знаходиться в хмарі, її все одно можна налагоджувати. Для того, щоб детальніше дізнатися про можливості публікації і налагодження робочої ролі в хмари, я пропоную звернутися до наступної статті: Debugging an Azure cloud service or virtual machine in Visual Studio

4. Архітектура інтеграції двох CRM систем.

В цілому опишу, як влаштований процес обробки повідомлень в черзі, який ми розробили та застосували в інтеграції двох систем. Робота починається з класу CRMQueueProcessor, в його обов'язки входить ініціалізація зв'язків, створення та конфігурування класу процесора повідомлень «CrmMessageProcessor », а також передплата на отримання повідомлень від шини. Як тільки весь процес ініціалізації завершено, і від шини отримано повідомлення, яке потрібно обробити, в роботу вступає CrmMessageProcessor.
CrmMessageProcessor є реалізацією патерну «Спостерігач». Його завдання-спостерігати за змінами, які відбуваються в системі і повідомляти про ці зміни своїх передплатників. Передплатників може бути скільки завгодно багато, кожен передплатник вирішує сам обробляти йому повідомлення чи ні. Всі передплатники успадковані від базового класу CrmBaseIntegrationHandler. CrmBaseIntegrationHandler будучи абстрактним класом, пропонує до реалізації кілька методів:

getProcessingEntityName() – повинен бути перевизначений, повертає ім'я сутності, наприклад, contact.

getProcessingAction() – повинен бути перевизначений, повертає дія або сукупність дій, на які має реагувати обробник. Наприклад: це створення запису.

HandleCrmMessage(string entityLogicalNameValue, string requestNameValue, Entity entity) – приймає саме повідомлення, а так само сутність і тип дії, викликає перевизначено обробник події якщо подія має місце бути

Entity OnProcessCreateEntity(Entity sourceEntity) – Обробник створення запису, він приймає сутність прийшла з черги і формує сутність, яка буде створена.

Entity OnProcessUpdateEntity(Entity sourceEntity) – Обробник зміни запису, він приймає сутність прийшла з черги і формує сутність, яка буде змінена.

public class ContactIntegrationHandler : CrmBaseIntegrationHandler
{
public override string getProcessingEntityName()
{
return "contact";
}

public override CrmMessageType getProcessingAction()
{
return CrmMessageType.Create | CrmMessageType.Update;
}

public override Entity OnProcessCreateEntity(Entity sourceEntity)
{
Entity output = new Entity("contact");
output["new_integrationid"] = sourceEntity.Id.ToString();
output["firstname"] = sourceEntity.GetAttributeValue<string>("firstname");
output["lastname"] = sourceEntity.GetAttributeValue<string>("lastname");
output["jobtitle"] = sourceEntity.GetAttributeValue<string>("jobtitle");

return output;
}

public override Entity OnProcessUpdateEntity(Entity sourceEntity)
{
Entity output = new Entity("contact");
output.Id = sourceEntity.Id;
if (sourceEntity.Contains("firstname"))
{
output["firstname"] = sourceEntity.GetAttributeValue<string>("firstname");
}
if (sourceEntity.Contains("lastname"))
{
output["lastname"] = sourceEntity.GetAttributeValue<string>("lastname");
}
if (sourceEntity.Contains("jobtitle"))
{
output["jobtitle"] = sourceEntity.GetAttributeValue<string>("jobtitle");
}

return output;
}

}

Клас CrmMessageProcessor виглядає наступним чином:
public class CrmMessageProcessor
{
List<CrmBaseIntegrationHandler> integrationSubscribers;

public CrmMessageProcessor(List<CrmBaseIntegrationHandler> subscribers)
{
this.integrationSubscribers = subscribers;
}

public void Subscribe(CrmBaseIntegrationHandler observer)
{
integrationSubscribers.Add(observer);
}

public void Unsubscribe(CrmBaseIntegrationHandler observer)
{
integrationSubscribers.Remove(observer);
}

public bool ProcessMessage(BrokeredMessage receivedMessage)
{
object entityLogicalNameValue, requestNameValue;
ExtractCrmProperties(receivedMessage, out entityLogicalNameValue, out requestNameValue);
if (entityLogicalNameValue == null || requestNameValue == null)
{
return false;
}

var context = receivedMessage.GetBody<RemoteExecutionContext>();
Entity entity = (Entity)context.InputParameters["Target"];

foreach (var handler in integrationSubscribers)
{
var status = handler.HandleCrmMessage((string)entityLogicalNameValue, (string)requestNameValue, entity);
if (status.ProcessMessgae)
{
switch (status.MessageType)
{

case CrmMessageType.Create:
{
CrmConnector.Instance.CreateEntity(status.EntityToProcess);
return true;
}

case CrmMessageType.Update:
{
var guid = CrmConnector.Instance.checkEntityForExistance(status.EntityToProcess);
if (guid != Guid.Empty)
{
status.EntityToProcess.Id = guid;
CrmConnector.Instance.UpdateEntity(status.EntityToProcess);
return true;
}
break;
}

default:
{
break;
}
}

}
}

return false;
}

/// <summary>
/// Витягує специфічні для CRM параметри повідомлення
/ / / < /summary>
/ / / < param name="receivedMessage">Повідомлення прийшло з шини</param>
/ / / < param name="entityLogicalNameValue">out: Назва сутності</param>
/ / / < param name="requestNameValue">out: Тип дії</param>
private void ExtractCrmProperties(BrokeredMessage receivedMessage, 
out object entityLogicalNameValue, out object requestNameValue)
{
string keyRoot = "http://schemas.microsoft.com/xrm/2011/Claims/";
string entityLogicalNameKey = "EntityLogicalName";
string requestNameKey = "RequestName";
receivedMessage.Properties.TryGetValue(keyRoot + entityLogicalNameKey, out entityLogicalNameValue);
receivedMessage.Properties.TryGetValue(keyRoot + requestNameKey, out requestNameValue);
}

}

Якщо жоден з обробників не обробив повідомлення, то воно поміщається в чергу необроблених повідомлень з відповідною позначкою. Якщо під час обробки повідомлення сталася помилка, то ми розблокуємо повідомлення в черзі і намагаємося обробити його повторно, і так поки не буде досягнуто ліміт спроб, після чого повідомлення потрапляє в чергу необроблених повідомлень. Далі витяг з класу CrmQueueProcessor.
public void OnMessageRecieved(BrokeredMessage receivedMessage)
{
try
{
if (processor.ProcessMessage(receivedMessage))
receivedMessage.Complete();
else
receivedMessage.DeadLetter("Canceled", "No event handler found");
}
catch (Exception ex)
{
receivedMessage.Abandon();
logger.LogCrmMessageException(receivedMessage, ex);
}
}

Для того щоб отримати шлях до повідомлень з черги необроблених повідомлень потрібно викликати метод FormatDeadLetterPath на поточному екземплярі об'єкта QueueClient і передати назву робочої черзі в якості аргументу:
QueueClient.FormatDeadLetterPath(queueName)
Ця рядок сформує відповідний шлях, потім можна сміливо підписатись на отримання повідомлень та обробляти їх.

5. Висновок

У прикладі, який ми розібрали, використовується чергу і всі повідомлення обробляються одним робочим процесом. В якості альтернативи використання черзі, Ви так само можете використовувати розділи (топіки), їх можна настроїти таким чином, щоб повідомлення від різних сутностей оброблялися різними потоками в рамках однієї робочої ролі чи в різних. Для кожного передплатника буде працювати своя черга, а коректно налаштований фільтр буде отримувати тільки ті повідомлення, які повинні оброблятися даним екземпляром. Якщо при цьому необхідно синхронізувати роботу декількох робочих ролей, то для цього Ви можете використовувати Blob leasing, більш докладно про синхронізацію робочих ролей в Azure Ви можете прочитати в наступній статті: Preventing Jobs Running From Simultaneously on Multiple Role Instances.

Список статей на які робив виноски:
Що таке ACS?
Що таке Azure PowerShell?
How to create service bus queues, topics and subscriptions using a powershell script .
Online Service Bus – As I Understand It: Part II (Queues & Messages)
Докладний опис можливостей розробки Microsoft Azure Cloud Services
Debugging an Azure cloud service or virtual machine in Visual Studio
Preventing Jobs Running From Simultaneously on Multiple Role Instances.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.