Предыстория
Началось всё со вполне (как это всегда в Dynamics бывает) тривиальной задачи. Необходимо при закрытии возможной сделки также автоматом закрывать все ассоциированные с ней задачи. Сделать это можно только программно, поэтому пишем плагин. Как обычно встаёт вопрос: а на какое стандартное сообщение его регистрировать? Гугл выдаёт два приемлемых варианта: на создание промежуточной сущности и на событие выигрыша и проигрыша (в нашей задаче не существенно, как именно закрыли). Здоровая лень и жадность говорит, зачем регистрировать два шага, если можно зарегистрировать один? Пишем, проверяем – не работает! Очень интересно не работает: все открытые задачи успешно находятся и закрываются, но после закрытия возникает две-три новых открытых задачи. Сели с аналитиком, начали копать… Откопали чудную цепочку бизнес-процессов: при закрытии задачи связанной со стадией бизнес-процесса, автоматом проставляются галочки стадии. При закрытии всех галочек стадии сделка автоматом двигается на новую стадию. При передвижении на новую стадию автоматически ставятся задачи под новые галочки новой стадии. Без аналитика не разберёшься, потому что синхронные бизнес-процессы, завершившиеся успешно, как хорошие киллеры, следов не оставляют. Ура бага налицо, поправили БП, вклинили условие не закрытой сделки, всё равно не работает. Придётся разбираться…
Закрытие возможной сделки – это внутренний механизм плотно встроенный в кишочки Dynamics CRM и влиять на него до сих пор достаточно проблематично. Каждый второй наш клиент очень хочет добавить что-то своё в момент закрытия сделки, например выбрать из своей базы – какое из подразделений сделку успешно закрыло или кто из конкурентов привёл к её провалу. Буквально каждый клиент хочет в процесс закрытия вмешаться: самое распространённое требование – валидация вводимых при закрытии данных, например, чтобы не выигрывались сделки с нулевым доходом и не проигрывались сделки без объяснительной – что и как. Давайте разбираться со встроенными механизмами и степенью возможности вмешаться.
Механизм закрытия после недолгого гугляжа выглядит в общем-то незамысловато: создаётся экземпляр новой сущности opportunityclose со всеми теми полями, что пользователь МОЖЕТ заполнить при закрытии. И при выигрыше и при проигрыше окошко в сущности одно и то же, меняется только список вариантов «причина состояния» - в первом случае они берутся из состояния Won во втором – из Lost. (Вот тут как раз ньюанс: поле «Фактический доход» является обязательным к заполнению, но подставляемое по умолчанию 0.00 вполне устраивает встроенный валидатор.) Далее в сделку перекочёвывают обязательные параметры из opportunityclose, состояние меняется на «закрытая» и всё, дело в шляпе. Это из очевидных шагов, а что там ещё делается под ковром проприетарной защиты знают только программисты MS.
Итак, мы имеем целых 3 точки воздействия (ну на самом деле 8, об этом далее), на которые можно подвесить плагин и тем или иным способом вмешаться в процесс:
Создание новой сущности Opportunityclose
Изменение полей сущности Opportunity (в том числе – изменение статуса)
Собственно, закрытие сделки
Давайте напишем простейший плагин для того, чтобы посмотреть, в каком порядке все эти шаги идут и что мы можем получить в качестве параметров сообщения. Заглушка плагина генерируется моим любимым легковесным инструментом CRM Developer Extensions. Всё просто, печатаем в трейс всё ценное, что мы можем вытащить из контекста.
public class TraceStepPlugin : IPlugin
{
//Just trace the step
public void Execute(IServiceProvider serviceProvider)
{
ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
IOrganizationService service = factory.CreateOrganizationService(context.UserId);
try
{
tracer.Trace("Plugin was called for {0} message on {1} entity, stage {2}",
context.MessageName,
context.PrimaryEntityName,
context.Stage.ToString());
foreach(var inputParameterName in context.InputParameters.Keys)
{
tracer.Trace("Input parameter " + inputParameterName);
tracer.Trace("of type: " + context.InputParameters[inputParameterName]?.GetType().ToString());
}
}
catch (Exception e)
{
throw new InvalidPluginExecutionException(e.Message);
}
}
}
Затем регистрируем на основе этого плагина 4 шага. Почему 4, если ранее пунктов было только 3? Потому что событие закрытия сделки оформлено двумя отдельными сообщениями: win и lost. Удобно – создать дополнительный шаг при необходимости одинаково обрабатывать оба события легче и нагляднее, чем усложнять логику проверками. На каждое сообщение можно зарегистрировать шаги до и после непосредственно операции, умножаем количество точек на 2. При регистрации плагина на обновление полей возможной сделки, как всегда при плагине на update, обязательно указываем, изменение каких именно полей повлечёт за собой вызов плагина:
Вот тут у меня огромные претензии к переводчикам. Какой духовно и физически одарённой личностью надо быть, чтобы перевести STATUScode как состояние, а statecode как статус? Я до сих пор эти поля страшно путаю.
Итого у нас получилось 10 шагов – точек изменения процесса. При регистрации взгляд зацепился за сообщение SetState, на него тоже можно подвесить плагин и посмотреть, отработает ли. Давайте теперь всё это дело запустим, например, проиграв сделку. Заглядываем в журнал трассировки:
Как видим, отработало 6 из 10 шагов. Почему не отработали before и after win очевидно. А вот с SetState – не так очевидно, хотя причина ровно та же самая. Генерированное сообщение, на которое вешается обработка было lose, а не SetState. Для генерации этого сообщения надо послать SetStateRequest, оно использовалось для инактивации сущностей. (Список можно посмотреть в документации https://docs.microsoft.com/en-us/dotnet/api/microsoft.crm.sdk.messages.setstaterequest?view=dynamics-general-ce-9). К стати, SetStateRequest нынче deprecated, вместо него нужно использовать Update с изменением полей statecode и statuscode, например, вот так:
contract.statecode = contractState.Inactive;
contract.statuscode = new OptionSetValue(2);
contract.EntityState = EntityState.Changed;
organizationService.Update(contract);
По последовательности шагов. На первый взгляд – всё красиво, но нельзя забывать, что трейсинг выполняется в фоновом режиме, что означает, что наши коротенькие шажочки могли попадать в очередь на асинхронную обработку конкурентно. А как это в Dynamics реализовано – никто вам не расскажет и есть вероятность, что оно тут совсем даже не в том порядке, в каком выполнялось. Для чистоты эксперимента поставим задержку в полторы минуты (я работаю в облачной версии – здесь все плагины можно регистрировать только в песочницу, что автоматически ведёт двухминутное ограничение на время работы плагина). Теперь можно быть чуть более уверенным, что мы получим в трейсе шаги в том порядке, в каком они действительно выполнялись. Мы видим, что гипотеза, основанная на здравом смысле подтвердилась. Действительно, триггером на выполнение всей цепочки служит сообщение lose (или win) далее вторым уровнем вложенности выполняются создание сущности opportunityclose и обновление opportunity.
Давайте глянем на параметры всех отработавших плагинов. Никакой разницы в параметрах между before и after нет. Поэтому приведу только трассировку before шагов.
Plugin was called for Lose message on opportunity entity, stage 20
Input parameter OpportunityClose
of type: Microsoft.Xrm.Sdk.Entity
Input parameter Status
of type: Microsoft.Xrm.Sdk.OptionSetValue
Input parameter Caller
of type:
Plugin was called for Create message on opportunityclose entity, stage 20
Input parameter Target
of type: Microsoft.Xrm.Sdk.Entity
Plugin was called for Update message on opportunity entity, stage 10
Input parameter Target
of type: Microsoft.Xrm.Sdk.Entity
Input parameter ConcurrencyBehavior
of type: Microsoft.Xrm.Sdk.ConcurrencyBehavior
Исходная задача была решена перерегистрацией шагов на PostOperation Win+Lose Opportunity и в момент срабатывания плагина и закрытия задач сделка была уже закрыта и никаких side effects больше не возникает. Пока не возникает...