Алгоритм поиска, слияния и обновления ссылок совпадающих сущностей в Microsoft Dynamics 365

Иногда возникает необходимость выполнить объединение совпадающих записей в одну. Это можно сделать выполнив слияние сущностей, с дальнейшим обновлением ссылок слитой сущности на основную сущность.

Предположим, что у нас есть сущность «Физическое лицо» или «Клиент» или «Контакт» (contact), с дополнительным внешним идентификатором (new_id) и связанной сущностью «Документ клиента» (new_client_document). Нам необходимо создать плагин, который будет при создании новой записи Документа клиента искать клиентов с совпадающими данными документа и пустым дополнительным идентификатором, и объединять их в одну запись клиента.

Пример

Клиент №1. Иванов Иван, Документ 1234 123456, идентификатор отсутствует.

Клиент №2. Петров Петр, Документ 1234 123456, идентификатор отсутствует.

Создаем нового клиента:

Клиент №3. Сидоров Сидор, Документ 1234 123456, идентификатор 123.

В результате в системе должна остаться одна активная запись Клиента №3, на которую ссылаются все связанные записи от Клиента №1 и Клиента №2.

Код

/// <summary> 
/// Плагин для автоматического поиска "Физических лиц" с совпадающими данными и их слияния. 
/// </summary> 
/// <remarks> 
/// Данный плагин регистрируется на сообщение Create, сущности "Документ клиента" (new_client_document),  
/// состояние post-operation в асинхронном режиме. 
/// </remarks>
public class Plugin : IPlugin
{
       public void Execute(IServiceProvider serviceProvider)
       {
             // Получить контекст выполнения и сервис от поставщика услуг.
             IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
             var serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
             var service = serviceFactory.CreateOrganizationService(context.UserId);

             // Коллекция InputParameters содержит все данные, переданные в запросе.
             if (context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
             {
                    // Получить целевой объект из входных параметров.
                    var clientDocument = (Entity)context.InputParameters["Target"];

                    // Ищем контакты с совпадающими данными документа.
                    var contacts = GetContacts(service, clientDocument);
                    if (contacts.Length == 0)
                    {
                           return;
                    }

                    // Связанные со сливаемым клиентом записи.
                    var entityNames = GetOneToManyRelationshipEntityAttributePair(service);

                    // Сливаем совпадающие контакты и обновляем ссылки.
                    foreach (var contact in contacts)
                    {
                           var client = clientDocument.GetAttributeValue<EntityReference>("new_clientid");
                           MergeContact(service, client, contact);
                           foreach (var entity in entityNames)
                           {
                                  UpdateLookup(service, entity.Item1, entity.Item2, contact.Id, client.Id);
                           }
                    }
             }
       }

       /// <summary>
       /// Выполняет получение массива "Физических лиц", удовлетворяющих условиям.
       /// </summary>
       /// <remarks>
       /// Совпадает серия и номер "Документа клиента" "Физического лица" с обрабатываемым "Документом клиента".
       /// Отсутствует идентификатор у "Физического лица.
       /// </remarks>
       /// <param name="service">Провайдер предоставляющий доступ к данным и метаданным организации.</param>
       /// <param name="clientDocument">Документ клиента.</param>
       /// <returns>Массив "Физических лиц".</returns>
       private EntityCollection GetContacts(IOrganizationService service, Entity clientDocument)
       {
             var series = clientDocument.GetAttributeValue<string>("new_series");
             var number = clientDocument.GetAttributeValue<string>("new_number");

             var query = new QueryExpression()
             {
                    EntityName = "contact",
                    ColumnSet = new ColumnSet(false),
                    Distinct = true,
                    LinkEntities =
                    {
                           new LinkEntity()
                           {
                                  LinkFromEntityName = "contact",
                                  LinkFromAttributeName = "contactid",
                                  LinkToEntityName = "new_client_document",
                                  LinkToAttributeName = "new_clientid",
                                  Columns = new ColumnSet(false),
                                  LinkCriteria = new FilterExpression
                                  {
                                        FilterOperator = LogicalOperator.And,
                                        Conditions =
                                        {
                                               new ConditionExpression("new_series", ConditionOperator.Equal, series),
                                               new ConditionExpression("new_number", ConditionOperator.Equal, number)
                                        }
                                  }
                           },
                    },
                    Criteria =
                    {
                           Conditions =
                           {
                                  new ConditionExpression("new_id", ConditionOperator.Null)
                           }
                    }
             };

             var contacts = service.RetrieveMultiple(query);
             return contacts;
       }

       /// <summary>
       /// Получить список имен зависимых сущностей и атрибутов.
       /// </summary>
       /// <param name="service">Провайдер предоставляющий доступ к данным и метаданным организации.</param>
       /// <returns>Список пар "Имя сущности" - "Атрибут сущности".</returns>
       private List<(string, string)> GetOneToManyRelationshipEntityAttributePair(IOrganizationService service)
       {
             // Не все сущности поддерживают возможность выполнения поисковых запросов.
             // Обычно это внутренние служебные сущности CRM.
             // Поэтому мы пропускаем эти сущности.
             var excludeEntitys = new string[] { "postrole", "postregarding", "mailboxtrackingfolder", "customeraddress" };

             var contactMetadata = RetrieveEntityMetadata(service, "contact");

             var result = contactMetadata.OneToManyRelationships
                    .Select(c => new ValueTuple<string, string>
                    {
                           Item1 = c.ReferencingEntity,
                           Item2 = c.ReferencingAttribute
                    })
                    .Where(c => !excludeEntitys.Contains(c.Item1)).ToList();
             return result;
       }

       /// <summary>
       /// Получает метаданные заданной сущности.
       /// </summary>
       /// <param name="service">Провайдер предоставляющий доступ к данным и метаданным организации.</param>
       /// <param name="logicalName">Логическое имя сущности.</param>
       /// <returns>Метаданные сущности.</returns>
       public static EntityMetadata RetrieveEntityMetadata(IOrganizationService service, string logicalName)
       {
             var request = new RetrieveEntityRequest
             {
                    LogicalName = logicalName,
                    EntityFilters = EntityFilters.All,
                    RetrieveAsIfPublished = false
             };
             var result = (RetrieveEntityResponse)service.Execute(request);
             return result.EntityMetadata;
       }
}

Подробнее про регистрацию плагинов можно прочитать в статье Создание Plug-in для Microsoft Dynamic 365