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();
}
}