AXForum  
Вернуться   AXForum > Блоги > CRM, SharePoint и Черная Магия
NAV
Забыли пароль?
Зарегистрироваться Правила Справка Пользователи Сообщения за день Поиск Все разделы прочитаны

Добро пожаловать в мой блог! Изначально он не задумывался как блог CRM разработчика, но жизнь сама внесла нужные коррективы. Тут я публикою все свои наблюдения относительно обозначенных в заголовке систем. Если Вы найдете в нем что-то интересное для Вас, как для заказчика, то буду рад сотрудничать с Вами! В моей компетенции 100% задач по MS CRM 3.0/4.0/2011:
  • Консалтинг
  • Проектирование
  • Разработка
  • Обучение


MVP 2010, 2011
Оценить эту запись

Кастомные служебные задания для CRM

Запись от Артем Enot Грунин размещена 17.07.2012 в 17:11
Обновил(-а) Артем Enot Грунин 17.07.2012 в 23:27
Теги .net, clr, sql

Начиная с версии 4.0 в CRM есть подсистема Служебных заданий - System Job. В SDK служебные задания представлены объектом AsyncOperation и, как можно понять из названия, за их выполнение отвечает Асинхронный сервис системы.

Служебные задания - это запускаемые асинхронно плагины, рабочие процессы и различные служебные системные задания, такие как обновления индексов базы данных. В более ранних версиях системы эти задания выполнялись SQL Agent.

И хотя со Служебными заданиями можно взаимодействовать через SDK, с ними нельзя сделать одно самое главное - создать свой тип Служебного задания. Это прискорбно, так как достаточно часто возникает задача выполнять какие-либо операции на периодической основе. Хороший пример - это системные задания автоматического сведения Целей и закрытия истекших сервисных Контрактов. Даже набившая оскомину проблема "Поздравлялки с днем рождения" могла бы получить изысканное решение, будь у нас возможность выполнять некие действия на периодической основе.

Вариантов решения множество:
  • Самопал. Самостоятельная реализация "долгоиграющего" приложения.
  • Винсервис. Стандартный сервис Windows.
  • Консоль + планировщик. Настройка планировщика Windows для вызова некоторого приложения по расписанию
  • Прочие менее популярные подходы.
У всех них есть как сильные так и слабые стороны. Требование предъявляемое к идеальному решению:
  • Корректная обработка выключения и перезагрузки сервера
  • Корректная работа на ферме серверов
  • Балансировка нагрузки при работе в ферме
Реализовать подобное с нуля не так просто, но можно попробовать использовать какие-то части готовых решений. Например, вышеупомянутый SQL Agent. Задания SQL Agent, или SQL Job, имеют достаточно развитый планировщик и удовлетворяют требованиям приведенным выше. Дело за малым - научить его взаимодействовать с CRM.

Для реализации этой задачи я выбрал следующий подход:
  1. Создается стандартный рабочий процесс с запуском вручную. Этот процесс будет выполнять необходимые нам действия - обновлять запись, слать письма и пр. для каждой записи в выборке.
  2. Создается хранимая CLR процедура. Процедура в качестве входных параметров принимает строку SQL запроса и идентификатор рабочего процесса. Она выполняет запрос и для каждой найденной записи осуществляет запуск указанного бизнес процесса.
  3. Создается задание планировщика SQL, который будет выполнять запуск процедуры.
Почему сборка выполняет процесс, а не выполняет всю логику самостоятельно? На мой взгляд, это позволяет создать решение, которое проще и быстрее масштабируется - операции выполняются ассинхронным сервисом, а не сервером СУБД, а так же позволяет реализовать парадигму "настройка вместо разработки". При данном подходе новые "служебные задания" реализуются проще и быстрее. Так же, это позволяет хранить в Решении CRM большее количество компонент, что упрощает развертывание обновлений.

В теории все просто, но на практике возникают сложности. Первая проблема, которую нам придется преодолеть - это врожденное неумение CRL сборок для SQL работать с веб-сервисами. Дело в том, что текущие версии SQL поддерживают работу только с определенным, очень ограниченным набором сборок из состава .NET: Supported .NET Framework Libraries. Все прочие (и их зависимости) придется регистрировать в базе отдельно. Но, обо всем по порядку!


1. Поддержка CLR.

Необходимо включить поддержку CLR типов для базы в которой мы планируем разворачивать нашу процедуру. Для этого необходимо выполнить следующий скрипт:
X++:
sp_configure 'show advanced options', 1;
GO

RECONFIGURE;
GO

sp_configure 'clr enabled', 1;
GO

RECONFIGURE;
GO
Пожалуйста, убедитесь что вы понимаете что делаете, прежде чем выполнять данный скрипт! Рекомендую ознакомиться с официальным источником: Common Language Runtime (CLR) Integration.


2. Поддержка WCF

Следующим шагом, необходимо зарегистрировать сборки (и их зависимости), которые нам потребуются для взаимодействия с веб-сервисами Windows Communication Foundation. Для этого выполним следующий скрипт:
X++:
ALTER DATABASE [FixRM_MSCRM] SET Trustworthy ON
GO

CREATE ASSEMBLY
[System.Web] from 'C:\Windows\Microsoft.NET\Framework64\v2.0.50727\System.Web.dll'
with permission_set = UNSAFE
GO

CREATE ASSEMBLY
SMDiagnostics from 'C:\Windows\Microsoft.NET\Framework64\v3.0\Windows Communication Foundation\SMDiagnostics.dll'
with permission_set = UNSAFE
GO

CREATE ASSEMBLY 
[System.Runtime.Serialization] from 'C:\Windows\Microsoft.NET\Framework64\v3.0\Windows Communication Foundation\System.Runtime.Serialization.dll'
with permission_set = UNSAFE
GO

CREATE ASSEMBLY
[System.IdentityModel] from 'C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\System.IdentityModel.dll'
with permission_set = UNSAFE
GO

CREATE ASSEMBLY
[System.IdentityModel.Selectors] from 'C:\Program Files\Reference Assemblies\Microsoft\Framework\v3.0\System.IdentityModel.Selectors.dll'
with permission_set = UNSAFE
GO

CREATE ASSEMBLY
[System.Messaging] from 'C:\Windows\Microsoft.NET\Framework64\v2.0.50727\System.Messaging.dll'  
with permission_set = UNSAFE
GO

CREATE ASSEMBLY
[Microsoft.Transactions.Bridge] from
'C:\Windows\Microsoft.NET\Framework\v3.0\Windows Communication Foundation\Microsoft.Transactions.Bridge.dll'
with permission_set = UNSAFE
GO

CREATE ASSEMBLY 
[System.ServiceModel] from 
'C:\Windows\Microsoft.NET\Framework64\v3.0\Windows Communication Foundation\System.ServiceModel.dll'
with permission_set = UNSAFE
GO
Так как наши сборки "UNSAFE" мы вынуждены сделать нашу базу Trustworthy в первой инструкции.


3. Проект CLR процедуры для SQL Server.


Visual Studio имеет готовые шаблоны проектов CLR типов для SQL Server. Воспользуемся этим:

Нажмите на изображение для увеличения
Название: project.png
Просмотров: 3346
Размер:	57.3 Кб
ID:	252

Далее необходимо указать к какой базе будет подключено решение. Нужно указать ту базу, для которой вы выполняли инструкции выше:

Название: add connection.png
Просмотров: 468

Размер: 8.2 Кб

Следующий шаг - создание самой процедуры. Для этого выбираем пункт "Создать новый элемент" (Add new item) из контекстного меню проекта:

Нажмите на изображение для увеличения
Название: add proc.png
Просмотров: 3348
Размер:	33.6 Кб
ID:	254

Далее добавим в созданный файл следующий код:
X++:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using SqlServerCrmClrIntegration.CrmService;
using System.Collections.Generic;
using System.ServiceModel.Channels;
using System.ServiceModel.Security.Tokens;
using System.ServiceModel;

public partial class StoredProcedures
{
    [Microsoft.SqlServer.Server.SqlProcedure]
    public static void ExecuteWorkflow(SqlString sql, SqlGuid workflowId)
    {
        using (SqlConnection connection = new SqlConnection("context connection=true"))
        {
            connection.Open();
            SqlCommand command = new SqlCommand(sql.Value, connection);
            SqlDataReader reader = command.ExecuteReader();

            using (reader)
            {
                String serviceUrl = "http://localhost/FixRM/XRMServices/2011/Organization.svc";

                SymmetricSecurityBindingElement security = new SymmetricSecurityBindingElement(new SspiSecurityTokenParameters());
                HttpTransportBindingElement http = new HttpTransportBindingElement();

                CustomBinding binding = new CustomBinding();
                binding.Elements.Add(security);
                binding.Elements.Add(http);

                OrganizationServiceClient client = new OrganizationServiceClient(binding, new EndpointAddress(serviceUrl));

                while (reader.Read())
                {
                    OrganizationRequest executeWorkflow = new OrganizationRequest();
                    executeWorkflow.RequestName = "ExecuteWorkflow";
                    executeWorkflow.Parameters = new ParameterCollection();
                    executeWorkflow.Parameters.Add(new KeyValuePair<string, object>("WorkflowId", workflowId.Value));
                    executeWorkflow.Parameters.Add(new KeyValuePair<string, object>("EntityId", reader.GetSqlGuid(0).Value));

                    client.Execute(executeWorkflow);
                }

                client.Close();
            }
        }

    }
};
Так же следует добавить Service Reference для CRM. В моем примере она называется CrmService. Не забудьте указать корректный адрес веб-сервиса в переменной serviceUrl! Я не сторонник хардкодинга подобных вещей - данный код - это лишь пример. В своем решении, вы можете реализовать получение адреса из таблицы настроек или параметра процедуры - на свой вкус.


4. Развертывание и тестирование созданного CLR типа.

Visual Studio умеет автоматически развертывать сборки и их содержимое на SQL Server. К сожалению, при этом студия зависает на неопределенное время. Возможно это специфика моего развертывания - не знаю. В любом случае, если вы столкнулись с такой проблемой вы можете воспользоваться следующим скриптом для выполнения той же самой операции:
X++:
IF EXISTS (SELECT name FROM sysobjects WHERE name = 'ExecuteWorkflow')
DROP PROCEDURE ExecuteWorkflow

IF  EXISTS (SELECT * FROM sys.assemblies asms WHERE asms.name = N'SqlServerCrmClrIntegration' and is_user_defined = 1)
DROP ASSEMBLY [SqlServerCrmClrIntegration]

CREATE ASSEMBLY
[SqlServerCrmClrIntegration] from 'С:\Путь к каталогу проекта\SqlServerCrmClrIntegration\bin\Debug\SqlServerCrmClrIntegration.dll'
WITH PERMISSION_SET = UNSAFE
GO

CREATE PROCEDURE ExecuteWorkflow (@SQL NVARCHAR(MAX), @WORKFLOW UNIQUEIDENTIFIER)
AS EXTERNAL NAME SqlServerCrmClrIntegration.StoredProcedures.ExecuteWorkflow; 
GO
Напоминаю, что в скрипте необходимо указать корректный путь к сборке решения и корректные имена типов.

Теперь остается убедиться что все работает. Для этого вы можете воспользоваться отладчиком VS или просто запустить процедуру:
X++:
DECLARE @SQL AS NVARCHAR(MAX) = 
'SELECT 
    account.accountid
FROM FilteredAccount account
WHERE account.accountid = ''1BDFF7A2-D599-E111-A0F2-0800271D883E'''

DECLARE @WORKFLOW AS UNIQUEIDENTIFIER = '53304737-2B84-4C09-8969-351218500BD1'

EXECUTE ExecuteWorkflow @SQL, @WORKFLOW
GO
В данном примере, запрос выбирает идентификатор тестовой записи объекта Клиент и вызывает предварительно настроенный тестовый процесс. Процесс должен быть доступен для запуска в ручную!

В реализации приведенной выше, я разворачивал сборки непосредственно в базе CRM, что, конечно же, не поддерживается. В реальном проекте следует использовать для этих целей отдельную базу. Второй момент: учетная запись от имени которой выполняется код - это учетная запись SQL сервера. Если база данных CRM обслуживается той же инстанцией SQL, то ее не нужно (даже запрещено) добавлять как пользовательскую учетную запись CRM. Все действия вашей сборки будут выполняться от привилегированной учетной записи SYSTEM:

Нажмите на изображение для увеличения
Название: system.png
Просмотров: 3294
Размер:	10.8 Кб
ID:	255

4. Настройка SQL Job

Последний шаг - это настройка самого задания SQL Agent Job. Это наиболее простая операция из всех здесь приведенных. Нужно лишь указать операцию которую необходимо выполнить:

Нажмите на изображение для увеличения
Название: new step.png
Просмотров: 3488
Размер:	70.4 Кб
ID:	256

и расписание запуска:

Нажмите на изображение для увеличения
Название: schedule.png
Просмотров: 3306
Размер:	77.2 Кб
ID:	257

Разумеется в настройках необходимо задать какую-то разумную выборку и правильный процесс-обработчик. Процесс должен:
  • Подходить типу записи, которую возвращает запрос
  • Быть настроен на запуск вручную или как дочерний процесс
  • Быть настроен на автоматическое удаление журнала (иначе вы быстро забьете логи)

5. Грязная магия

А теперь немного о грустном... При вызове вашей процедуры вы с большой долей вероятности получите ошибку:
X++:
Server: Msg 6522, Level 16, State 2, Line 1
A .NET Framework error occurred during execution of user defined routine or aggregate XYZ:

System.IO.FileLoadException: Could not load file or assembly 'System.ServiceModel, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. Assembly in host           store has a different signature than assembly in GAC. (Exception from HRESULT:           0x80131050)

System.IO.FileLoadException:
Что это и почему возникает написано тут: http://support.microsoft.com/kb/949080/en-us и тут: Supported .NET Framework Libraries. Последнюю ссылку я уже приводил в этой статье.

Суть проблемы в следующем: при инстанцировании сборки, SQL Server по какой-то неведомой мне причине проверяет, нет ли более новой версии в GAC. Если более новая версия найдена, то запуск валится с ошибкой. Чтобы все работало корректно, необходимо обновить сборки в базе более свежими версиями из GAC.

Почему это могло случиться? На ваш сервер установились обновления .NET Framework. Надо было думать прежде чем ставить что попало на производственный сервер! Если вы думаете, что для обновления сборок в базе достаточно повторно запустить скрипт регистрации, то нет. Почему-то обновления не затрагивают сборки в каталоге инсталляции .NET Framework! Свежие версии содержаться только в GAC. Выцарапать их оттуда можно, например, командой меню RUN:
X++:
C:\Windows\assembly\gac_msil
Теперь осталось только найти и положить в какой-либо каталог 17 сборок и повторно их зарегистрировать. Прошлые версии предварительно нужно удалить. Будьте бдительны! Некоторые сборки имеют кросс-зависимости, так что их нужно удалять в одной транзакции (возможно только через SQL, через интерфейс не получится). Возможно более простым и правильным будет обновить зарегистрированные сборки при помощи инструкции ALTER.

Вторая проблема настолько загадочна, что я даже не стану вдаваться в суть проблемы. Приеду только ссылку на ее решение: What is Microsoft.VisualStudio.Diagnostics.ServiceModelSink.dll? Если упомянутая библиотека вылазит в сообщении об ошибке - вам сюда.


Итоги.

Задача решаема, но о цене судите сами. Я бы не назвал это "Enterprise Ready" решением, как любит говорить мой коллега Андрей Слепицкий, но это как минимум "быстрое решение".

Перед проведением своих зловещих экспериментов рекомендую ознакомиться с первоисточником: Common Language Runtime (CLR) Integration Programming Concepts. Так же хорошее практическое руководство есть тут: Invoking a WCF Service from a CLR Trigger.
Размещено в CRM
Просмотров 53391 Комментарии 1
Всего комментариев 1

Комментарии

  1. Старый комментарий
    Аватар для Артем Enot Грунин
    Мою инициативу поддержали на коннекте и обещали включить в дорожную карту. Возможно, в следующей версии у нас будет родная поддержка асинхронных служебных заданий.
    Запись от Артем Enot Грунин размещена 14.03.2014 в 10:55 Артем Enot Грунин is offline
 


Рейтинг@Mail.ru
Часовой пояс GMT +3, время: 21:45.