Перейти к содержанию

API Слияния объектов

SmartMergeAPI - это механизм, позволяющий произвести слияние двух объектов одного типа.

В типичной реализации основан на построении многоуровневых деревьев по данным объектам с последующим сопоставлением узлов дерева для первого объекта слияния со вторым.

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

Интерфейсы и классы, относящиеся к SmartMergeAPI, и их описание

Note

В Generic интерфейсах SmartMergeAPI используются следующие типы:

<TMergeObject> - тип, представляющий объекты слияния.

<TMergeOptions> - тип, представляющий параметры слияния. TMergeOptions должны быть реализациями интерфейса IMergeOptions.

ISmartMerger<TMergeObject> - Объект, содержащий основную логику, которая будет производить слияние.

SmartMergerBase<TMergeObject, TMergeOptions> : ISmartMerger<TMergeObject> - Абстрактный базовый класс для типичной реализации ISmartMerger с использованием интерфейсов IMergeTreeBuilder и IMergeMetadataBuilder.

IMergeMetadataBuilder<in TMergeOptions> - Объект, который строит метаданные для логики слияния.

IMergeMetadata - Объект метаданных для слияния.

IMergeMetadataTier - Уровень метаданных для слияния, содержащий в себе список узлов метаданных слияния IMergeMetadataNode.

IMergeMetadataNode - Узел метаданных слияния.

IMergeTreeBuilder<TMergeObject, TMergeOptions> - Объект, который cтроит дерево слияния на основе IMergeMetadata, для каждого из объектов слияния.

IMergeTree<TMergeObject, TMergeOptions> - Дерево слияния.

ITreeTier<TMergeObject> - Уровень в дереве слияния, содержит список узлов ITreeNode.

ITreeNode<TMergeObject> : IEquatable<ITreeNode<TMergeObject>> - Узел для дерева слияния.

IMergeOptions : IStorageSerializable - Параметры слияния.

IMergeResult<TMergeObject> - Объект, содержащий в себе результаты слияния (список IMergeResultItem), а также логику дальнейших действий с этими результатами в методе ApplyAsync() (например, возможность применить результаты слияния к объекту, с которым происходит слияние).

IMergeResultItem<in TMergeObject> - Отдельный результат слияния, а также логика дальнейших действий с этим результатом. Реализация полностью зависит от типов объектов слияния и их узлов слияния.

IMergeContext<out TMergeOptions> - Объект контекста для логики слияния, содержащий необходимую для этого слияния информацию.

Использование и реализация SmartMergeAPI

Реализации интерфейсов ISmartMerger<TMergeObject> (пример), IMergeMetadataBuilder<in TMergeOptions> (пример), и IMergeTreeBuilder<TMergeObject, TMergeOptions> (пример IMergeTreeBuilder и пример IMergeTreeNode) следует зарегистрировать в IoC-контейнере. Например, в реализации SmartMergeAPI для объектов типа MyObject это может выглядеть следующим образом:

unityContainer .RegisterType<ISmartMerger<MyObject>, MyObjectSmartMerger>(new ContainerControlledLifetimeManager()) .RegisterType<IMergeMetadataBuilder<MyObjectMergeOptions>, MyObjectMergeMetadataBuilder>(new ContainerControlledLifetimeManager()) .RegisterType<IMergeTreeBuilder<MyObject, MyObjectMergeOptions>, MyObjectMergeTreeBuilder>(new ContainerControlledLifetimeManager());

После регистрации интерфейсов получаем реализацию ISmartMerger через конструктор или другим удобным способом. Например:

public Constructor(ISmartMerger<MyObject> smartMerger, ...) { this.smartMerger = smartMerger; ... }

Сам объект smartMerger должен содержать основную логику, которая будет производить слияние в методе MergeAsync (см. пример реализации ISmartMerger), а также, в результате своей работы возвращать объект типа IMergeResult, который будет содержать список объектов IMergeResultItem (см. пример реализации IMergeResultItem).

В дальнейшем можно использовать smartMerger следующим образом:

var mergeResult = await smartMerger.MergeAsync( sourceObject, // Объект-источник (измененный или который объединяем с объектом-назначения) destinationObject, // Объект-назначения (текущий или с которым объединяем объект-источник) mergeOptions, // Опции слияния logger, // Можно передать кастомный логгер или будет использован логгер по умолчанию cancellationToken) .ConfigureAwait(false);

// Затем можно использовать результат слияния, например применить его к объекту-назначения (`destinationObject`):

if (mergeResult.ValidationResult.IsSuccessful) { await mergeResult.ApplyAsync(destinationObject, cancellationToken).ConfigureAwait(false); }

В типовом случае шаги по реализации ISmartMerger должны быть примерно следующие (пример реализации ISmartMerger):

Создается контекст слияния с необходимой информацией:

var mergeContext = new MergeContext<MyObjectMergeOptions>( new ValidationResultBuilder(), destinationObject, mergeOptions, logger, ComparisonLevels);

ComparisonLevels - это количество уровней при сопоставлении узлов. Зависит от реализации. Например, в текущей реализации для объектов Card этот параметр равен 2, а именно, на первом уровне сопоставления узлы будут сопоставляться по RowID, а затем, в том случае если после сопоставления на первом уровне еще остались несопоставленные узлы, на втором уровне, логика попытается сопоставить их по ключевым полям (KeyColumns), если таковые заданы в опциях слияния. В объектах типа MyObject это может быть как всего 1 уровень сопоставления, так и произвольно больше, также в опциях слияния для настройки уровней сопоставления могут существовать произвольные поля, или наоборот, опции могут совсем не влиять на поведение сопоставления. Описание и пример метода отвечающего за данную логику см. здесь.

На основании контекста строится дерево метаданных слияния:

var metadata = await this.MergeMetadataBuilder.BuildAsync( mergeContext, cancellationToken) .ConfigureAwait(false);

В результате получается объект IMergeMetadata, в котором содержится дерево метаданных, включающее в себя уровни, представленные интерфейсом IMergeMetadataTier, которые в свою очередь содержат в себе узлы, представленные интерфейсом IMergeMetadataNode соответственно.

Реализация IMergeMetadataNode полностью зависит от типов объектов слияния и действий, которые нужно совершить для получения необходимой информации по узлу (см. пример реализации IMergeMetadataNode).

Затем строятся деревья для каждого из объектов слияния:

var destinationTree = this.MergeTreeBuilder.Build(metadata, destinationObject); var sourceTree = this.MergeTreeBuilder.Build(metadata, sourceObject);

Примеры реализаций IMergeTreeBuilder и IMergeTreeNode.

Для этого могут быть использованы типовые реализации интерфейсов IMergeTree и ITreeTier, но интерфейс ITreeNode необходимо реализовать в зависимости от типов объектов слияния, возможно даже конкретных частей этих объектов, как например, в текущей реализации для объектов Card узлы, представляющие строковую секцию, строку табличной секции, запись в истории заданий и т.д. - это все различные реализации интерфейса ITreeNode (см. Особенности реализации узлов в деревьях слияния).

И, наконец, производится слияние деревьев:

var mergeResultItems = destinationTree.Merge(sourceTree, mergeContext); var mergeResult = new MergeResult(mergeResultItems, mergeContext.ValidationResult.Build());

Пример реализации IMergeResultItem.

В результате получаем объект IMergeResult, который мы можем использовать в дальнейшем, например применить результаты слияния к destinationObject:

if (mergeResult.ValidationResult.IsSuccessful) { await mergeResult.ApplyAsync(destinationObject, cancellationToken).ConfigureAwait(false); }

Особенности реализации узлов в деревьях слияния ITreeNode

Интерфейс IEquatable

При реализации узлов ITreeNode<TMergeObject> : IEquatable<ITreeNode<TMergeObject>> необходимо помнить, что от реализации методов интерфейса IEquatable будет зависеть поведение логики слияния, которая определяет, имеются ли изменения в узле по отношению с сопоставленным узлом из дерева, с которым производится слияние.

Например, в реализации узлов слияния для секций объектов Card учитываются включенные, исключенные и/или игнорируемые колонки (IncludedColumns, ExcludedColumns и/или IgnoredColumns соответственно), эти условия устанавливаются с помощью опций слияния, реализованных через CardMergeOptions : IMergeOptions.

Так, например, если в какой-либо секции необходимо учитывать для сравнения только определенные колонки, объект опций слияния будет выглядеть примерно так:

var mergeOptions = new CardMergeOptions { SectionsSettings = new[] { new SectionSettings( "SomeSection", includedColumns: new[] { "SomeColumn1", "SomeColumn2", ... } } };

Подробное описание о структуре объекта CardMergeOptions, реализованного для объектов типа Card, значениях его свойств, типах этих свойств, а также их значений по умолчанию можно прочитать в Руководстве администратора.

Метод int? GetHashCode(int compareLevel)

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

Например, в реализации логики слияния для объектов Card имеется два уровня сопоставлния. Предположим, что в опциях слияния для какой-либо табличной секции задан параметр ключевых колонок KeyColumns:

var mergeOptions = new CardMergeOptions { SectionsSettings = new[] { new SectionSettings( "SomeSection", keyColumns: new[] { "SomeColumn1", "SomeColumn2", ... } } };

Тогда логика следующая:

При compareLevel == 1, GetHashCode(int compareLevel) должен вернуть значение хеш-кода для RowID (в случае, если это строка табличной секции) и по SectionName (в случае, если узел является представлением строковой секции). Для узлов, представляющих другие части объекта Card, это может быть другая логика соответственно.

При compareLevel == 2, GetHashCode(int compareLevel) должен вернуть значение хеш-кода для комбинации колонок, заданных в параметре KeyColumns объекта CardMergeOptions. Если же параметр KeyColumns не задан, либо задан как пустой массив, то GetHashCode(int compareLevel) должен вернуть null.

При compareLevel > 2, GetHashCode(int compareLevel) должен вернуть null.

См. Пример реализации int? GetHashCode(int compareLevel)

Метод bool EqualsByKey(ITreeNode<TMergeObject> other, int compareLevel)

Данный метод должен обеспечить сравнение двух узлов по их ключам, можно реализовать на использовании метода int? GetHashCode(int compareLevel), описанного выше. В данном методе может быть разнообразная логика, все зависит от реализации и целей.

См. Пример реализации EqualsByKey()

Примеры некоторых реализаций

Пример реализации ISmartMerger

/// <summary> /// Реализация ISmartMerger для объектов MyObject /// </summary> public class MyObjectSmartMerger : SmartMergerBase<MyObject, MyObjectMergeOptions> { #region Constants

private const int ComparisonLevels = 2;

#endregion

#region Constructors

public MyObjectSmartMerger( IMergeTreeBuilder<MyObject, MyObjectMergeOptions> mergeTreeBuilder, IMergeMetadataBuilder<MyObjectMergeOptions> mergeMetadataBuilder) : base(mergeTreeBuilder, mergeMetadataBuilder) { }

#endregion

#region Methods

public override async ValueTask<IMergeResult<MyObject>> MergeAsync( MyObject sourceObject, MyObject destinationObject, IMergeOptions mergeOptions = default, ILogger logger = default, CancellationToken cancellationToken = default) { // Создание контекста слияния var mergeContext = new MergeContext<MyObjectMergeOptions>(new ValidationResultBuilder(), destinationObject, mergeOptions, logger, ComparisonLevels);

// построение мета-данных для слияния var metadata = await this.MergeMetadataBuilder.BuildAsync( mergeContext, cancellationToken).ConfigureAwait(false);

if (!mergeContext.ValidationResult.IsSuccessful()) { return ErrorValidation(mergeContext); }

// построение первго дерева var destinationTree = this.MergeTreeBuilder.Build(metadata, destinationObject); if (!mergeContext.ValidationResult.IsSuccessful()) { return ErrorValidation(mergeContext); }

// построение второго дерева var sourceTree = this.MergeTreeBuilder.Build(metadata, sourceObject); if (!mergeContext.ValidationResult.IsSuccessful()) { return ErrorValidation(mergeContext); }

// слияние деревьев var mergeResultItems = destinationTree.Merge(sourceTree, mergeContext); if (!mergeContext.ValidationResult.IsSuccessful()) { return ErrorValidation(mergeContext); }

var mergeResult = new MyObjectMergeResult(mergeResultItems, mergeContext.ValidationResult.Build());

return mergeResult; }

#endregion }

Пример реализации IMergeMetadataBuilder

public class MyObjectMergeMetadataBuilder : IMergeMetadataBuilder<MyObjectMergeOptions> { public async ValueTask<IMergeMetadata> BuildAsync( IMergeContext<MyObjectMergeOptions> mergeContext, CancellationToken cancellationToken = default) { if (mergeContext.MergeObject is not MyObject myObject) { throw new ArgumentException( $"Parameter must be typeof {nameof(MyObject)}", nameof(mergeContext.MergeObject)); }

var tiers = new List<MergeMetadataTier>();

// Логика, строящая списки узлов `IMergeMetadataNode` по уровням и определяющая узлов вида parent-child, например: var parentNodes = new List<MyObjectMetadataNode>(); foreach (var element in myObject.ParentElements) { parentNodes.Add(this.CreateNode(element)); } tiers.Add(new MergeMetadataTier(parentNodes));

var childNodes = new List<MyObjectMetadataNode>(); foreach (var element in myObject.ChildElements) { childNodes.Add(this.CreateNode(element)); } tiers.Add(new MergeMetadataTier(childNodes));

return new MergeMetadata(tiers); }

private MyObjectMetadataNode CreateNode(MyObjectElement element) { // некая логика, строящая узел метаданных на основании параметра `element` ... } }

Пример реализации MergeTreeBuilder

public class MyObjectMergeTreeBuilder : IMergeTreeBuilder<MyObject, MyObjectMergeOptions> { public IMergeTree<MyObject, MyObjectMergeOptions> Build( IMergeMetadata metadata, MyObject MyObject) { // уровни дерева слияния var treeTiers = new List<TreeTier<MyObject>>();

// проходим по каждому уровню в метаданных foreach (var metadataTier in metadata.Tiers) { var treeTier = new TreeTier<MyObject>(); // проходим по каждому узлу текущего уровня слияния в метаданных foreach (var metadataNode in metadataTier.Nodes.Cast<MyObjectMergeMetadataNode>()) { treeTier.Nodes.Add(BuildTreeNode(metadataNode)) } treeTiers.Add(treeTier); }

return new MergeTree<MyObject, MyObjectMergeOptions>(treeTiers); }

private MyObjectTreeNode BuildTreeNode(MyObjectMergeMetadataNode metadataNode) { // некая логика, строящая узел на основании параметра `metadataNode` ... } }

Пример реализации IMergeMetadataNode на примере объектов типа Card

public class CardMergeMetadataNode : IMergeMetadataNode { #region Constructors

public CardMergeMetadataNode( string sectionName, string sectionParentName, string sectionParentColumnName, IReadOnlyList<string> sectionIgnoredForEqualsColumns, IReadOnlyList<string> sectionKeyColumns, IReadOnlyList<string> sectionIgnoredForModifyColumns, bool isIgnored, bool? ignoreDuplicateRows, CardMergeMetaType metaType, Guid? cardID) { this.SectionName = sectionName; this.SectionParentName = sectionParentName; this.SectionParentColumnName = sectionParentColumnName; this.SectionIgnoredForEqualsColumns = sectionIgnoredForEqualsColumns ?? Array.Empty<string>(); this.SectionKeyColumns = sectionKeyColumns ?? Array.Empty<string>(); this.SectionIgnoredForModifyColumns = sectionIgnoredForModifyColumns ?? Array.Empty<string>(); this.IsIgnored = isIgnored; this.IgnoreDuplicateRows = ignoreDuplicateRows; this.MetaType = metaType; this.CardID = cardID; }

#endregion

#region Properties

/// <summary> /// Наименование секции карточки. /// </summary> public string SectionName { get; }

/// <summary> /// Наименование родительской секции, относительно секции указанной в `SectionName`. /// </summary> public string SectionParentName { get; }

/// <summary> /// Наименование колонки в секции, являющейся указателем на строку в родительской секции. /// </summary> public string SectionParentColumnName { get; }

/// <summary> /// Флаг, указывающий на то будет ли игнорироваться данный узел дальнейшей логикой слияния. /// </summary> public bool IsIgnored { get; }

/// <summary> /// Список колонок в секции, которые будут игнорированы при сравнении узлов по значению. /// </summary> public IReadOnlyList<string> SectionIgnoredForEqualsColumns { get; }

/// <summary> /// Список колонок в секции, которые будут игнорированы для изменения. /// </summary> public IReadOnlyList<string> SectionIgnoredForModifyColumns { get; }

/// <summary> /// Список ключевых колонок в секции, если заданы, эти колонки будут задействованы для 2-го уровня сопосталвения. /// </summary> public IReadOnlyList<string> SectionKeyColumns { get; }

/// <summary> /// Флаг, влияющий на логику при обнаружении дубликатов узлов, /// например, на втором уровне сопоставления, несколько узлов имеют одинаковые значения `SectionKeyColumns`. /// </summary> public bool? IgnoreDuplicateRows { get; }

/// <summary> /// Тип узла метаданных слияния. /// </summary> public CardMergeMetaType MetaType { get; }

/// <summary> /// ID карточки к которой относится данный узел . /// </summary> public Guid? CardID { get; }

#endregion }

Пример реализации метода int? GetHashCode(int compareLevel) для двух уровней сопоставления

public override int? GetHashCode(int compareLevel) { return compareLevel switch { 0 => this.IDHash, 1 => this.KeyColumns, _ => null }; }

Где:

IDHash - Хешкод уникального идентификатора узла объекта.

KeyColumns - Хешкод вторичных ключевых полей узла объекта.

Пример реализации метода bool EqualsByKey(ITreeNode<TMergeObject> other, int compareLevel) для двух уровней сопоставления

public class MyObjectTreeNode { ...

public override bool EqualsByKey(ITreeNode<Card> other, int compareLevel) { if (!(other is MyObjectTreeNode otherMyObjectTreeNode)) { return false; }

var hashCode = this.GetHashCode(compareLevel);

if (hashCode == 0 || hashCode != otherMyObjectTreeNode.GetHashCode(compareLevel)) { return false; }

return compareLevel switch { 0 => this.IDField == otherMyObjectTreeNode.IDField, 1 => this.KeyField1 == otherMyObjectTreeNode.KeyField1 && this.KeyField2 == otherMyObjectTreeNode.KeyField2, _ => false }; }

... }

Пример реализации ITreeNode

public class MyObjectElementTreeNode : TreeNodeBase<MyObject> { #region Properties

#endregion

private readonly MyObjectElement element;

private readonly int idHash; private readonly int valueHash;

public MyObjectElementTreeNode( MyObjectElement element) : base(null) { this.element = element;

this.idHash = HashCode.Combine(this.element.Name);

var hash = new HashCode();

foreach (var field in this.element.Fields) { hash.Add(field.Value); }

this.valueHash = hash.ToHashCode(); }

public override bool EqualsByKey(ITreeNode<MyObject> other, int compareLevel) { var otherMyObjectElementTreeNode = other as MyObjectElementTreeNode; if (otherMyObjectElementTreeNode is null) { return false; }

var hashCode = this.GetHashCode(compareLevel);

return hashCode.HasValue && (hashCode == otherMyObjectElementTreeNode.GetHashCode(compareLevel) && this.element.Name == otherMyObjectElementTreeNode.element.Name); }

public override int? GetHashCode(int compareLevel) { return compareLevel switch { 0 => this.idHash, _ => null }; }

public override bool Equals(ITreeNode<MyObject> other) { var otherMyObjectElementTreeNode = other as MyObjectElementTreeNode; if (otherMyObjectElementTreeNode is null) { throw new ArgumentException("Other node is not comparable", nameof(other)); }

return this.valueHash == otherMyObjectElementTreeNode.valueHash && SmartMergeHelper.AreEqual(this.element.Fields, otherMyObjectElementTreeNode.element.Fields); }

public override IMergeResultItem<MyObject> GetMergeResult() { switch (this.State) { case State.Modified: { return new UpdateMyObjectElementMergeResultItem(this.element.Name, this.element.Fields); } case State.Deleted: { return new DeleteMyObjectElementMergeResultItem(this.element.Name); } default: { return null; } } }

public override ITreeNode<MyObject> Clone() => this;

public override string ToString() { return this.element.Name; } }

Пример реализации IMergeResultItem

public class UpdateMyObjectElementMergeResultItem : IMergeResultItem<MyObject> { public string ElementName { get; } public IDictionary<string, object> Fields { get; }

public UpdateMyObjectElementMergeResultItem(string elementName, IDictionary<string, object> fields) { this.ElementName = elementName; this.Fields = fields; }

public ValueTask ApplyAsync(MyObject o, CancellationToken cancellationToken = default) { var updatedElement = o.Elements.First(x => x.Name == this.ElementName); foreach (var field in this.Fields) { updatedElement.Fields[field.Key] = field.Value; }

return new ValueTask(); } }

Back to top