Skip to content

Creating Custom Diffing Strategies

Egil Hansen edited this page Dec 27, 2019 · 10 revisions

The built-in options/strategies covers a lot of ground and will be enough for many scenarios. However, the diffing engine is pluggable with new strategies to enable custom scenarios. This page show you have to create your own.

To learn when and how the different types of strategies are used, see the Difference Engine Internals page.

Contents:

Understanding the strategy pipeline

The library comes with a single implementation of the IDiffingStrategy interface: DiffingStrategyPipeline.

The strategy pipeline type allows us to register filters, matchers, and comparers that it will call, one at the time, when its Filter, Match, and Compare methods are called by the diffing engine.

When you register strategies, you have to decide what its category is, i.e. if it is a general or specialized strategy. This affects the order which the pipeline invokes the strategies in. The invocation rules are as follows:

  1. Strategies within the same category are invoked in the order they are added to the pipeline in.
  2. Specialized filters always execute after any generalized filters in the pipeline. That enables them to correct for the generic filters decision.
  3. Specialized matchers always execute before any generalized matchers in the pipeline. This enables the special matchers to handle special matching cases before the more simple generalized matchers process the rest.
  4. Specialized comparers always execute after any generalized comparers in the pipeline. That enables them to correct for the generic comparers decision.

Creating filters strategies

A filter strategy are used during the filtering phase where nodes and attributes are either included or excluded from the comparison.

A filter has to have the signature that is compatible with the FilterStrategy delegate shown below:

delegate FilterDecision FilterStrategy<TSource>(in TSource source, FilterDecision currentDecision);

The strategy you can create must return a FilterDecision, which can be either Keep or Exclude. The recommended structure of a filter looks like this (this is the IgnoreCommentFilter):

public static FilterDecision Filter(in ComparisonSource source, FilterDecision currentDecision)
{
    // Step 1: Check if the current decision already has the outcome the filter would set
    //         if its test passes.
    if (currentDecision.IsExclude()) return currentDecision;

    // Step 2: Inspect source and decide if the current decision should be changed.
    //         If it should be changed, just return the desired decision.
    if (source.Node.NodeType == NodeType.Comment) return FilterDecision.Exclude;

    // Step 3: If the inspection did not change the current decision, just return it.
    return currentDecision;
}

This is an example of the filter that filters out "diff:"-attributes, i.e. attributes used in control markup to change the behaviour of the diffing engine:

private const string DiffAttributePrefix = "diff:";

public static FilterDecision Filter(in AttributeComparisonSource source, FilterDecision currentDecision)
{
    if (currentDecision.IsExclude()) return currentDecision;

    if (source.Attribute.Name.StartsWith(DiffAttributePrefix, StringComparison.OrdinalIgnoreCase))
        return FilterDecision.Exclude;

    return currentDecision;
}

For a more complex example, see the TextNodeFilter. It removes empty text nodes depending on a custom diff:whitespace attribute and a general option set inside it during construction.

Creating matching strategies

A matching strategy is used to match control and test nodes/attributes with each other for comparison. If an control/test attribute or node is not match during the matching phase, it is marked as either missing or unexpected, depending on whether it is a control or test attribute/node.

Creating comparer strategies

A compare strategy is used to evaluation a matched set of nodes or attributes and decide if the are semantically the same or different.

Creating strategy libraries/collections

If you are creating a set of strategies and want to make it easy for users to use them during a diffing operation, e.g. via the DiffBuilder type, it is recommended to include an extension method with your library, that handles the registration correctly (e.g. is it a specialized or general strategy).

For example, this extension method adds the standard attribute comparison logic and ensures that all the needed strategies are added in the correct order with the right category:

public static class DiffingStrategyCollectionExtensions
{
    public static IDiffingStrategyCollection AddAttributeComparer(this IDiffingStrategyCollection builder)
    {
        builder.AddMatcher(PostfixedAttributeMatcher.Match, StrategyType.Specialized);
        builder.AddComparer(AttributeComparer.Compare, StrategyType.Generalized);
        builder.AddComparer(IgnoreAttributeComparer.Compare, StrategyType.Specialized);
        return builder;
    }
}
Clone this wiki locally