How can I detect reference loops in an EF-generated many-to-many self-reference relationship?

Viewed 162

I have a collection of objects like the example Foo below:

public class Foo
{
    public long Id { get; set; }

    public IEnumerable<Foo> FooParents { get; set; }
    public IEnumerable<Foo> FooChildren { get; set; }
}

These objects are related to each other by the properties FooParents and FooChildren.

How can I effectively detect a circular dependency for a specific Foo?

I know, what to do in case of one-to-many, but in case of many-to-many, I am a little bit confused.

:(

Thank you!

1 Answers

Here is an implementation with some tests.

This is a generic solution that performs a Depth-First Search with behaviour specialised to your problem domain. It keeps a history of visited nodes in the soFar hash set, completing when either a duplicate is encountered or the paths along the specified branches are exhausted.

A duplicate indicates a circular relationship.

This extension method must be supplied with a number delegates. First a key selector that yields a unique identifier for each item. Then any number of axis selectors that represent the branches, upon which, the object graph extends.

The interesting extension method is here,

public static bool IsCircular<T, K>(
    this T item,
    Func<T, K> keySelector, 
    params Func<T, IEnumerable<T>>[] axes)
{
    var soFar = new HashSet<K>();
    soFar.Add(keySelector(item));
    return axes.Any(axis =>
        axis(item).Any(next => IsCircularOnAxis(
            next,
            keySelector,
            axis,
            soFar)));
}

private static bool IsCircularOnAxis<T, K>(
    T item,
    Func<T, K> keySelector,
    Func<T, IEnumerable<T>> axisSelector, 
    HashSet<K> soFar)
{
    var key = keySelector(item);
    if(soFar.Contains(key))
    {
        return true;
    }

    soFar.Add(key);
    return axisSelector(item).Any(next => 
        IsCircularOnAxis(
            next,
            keySelector,
            axisSelector,
            soFar));
}

In this case of Foo, the extension method would be called like this,

someFoo.IsCircular(
    foo => foo.Id,           /*The key selector*/
    foo => foo.FooParents,   /*The parent axis selector*/
    foo => foo.FooChildren); /*The child axis selector*/

The full working example is here, with some tests for Foo.

using System;
using System.Collections.Generic;
using System.Linq;
                    
public class Program
{
    public static void Main()
    {
        var soloFoo = new Foo
        {
            Id = 1,
            FooParents = Enumerable.Empty<Foo>(),
            FooChildren = Enumerable.Empty<Foo>(),
        };
        Console.WriteLine($"soloFoo isCircular:{soloFoo.IsCircular(foo => foo.Id, foo => foo.FooParents, foo => foo.FooChildren)}");
        
        var selfParent = new Foo
        {
            Id = 1,
            FooChildren = Enumerable.Empty<Foo>(),
        };
        selfParent.FooParents = new[] { selfParent };
        Console.WriteLine($"selfParent isCircular:{selfParent.IsCircular(foo => foo.Id, foo => foo.FooParents, foo => foo.FooChildren)}");
        
        
        var selfChild = new Foo
        {
            Id = 1,
            FooParents = Enumerable.Empty<Foo>(),
        };
        selfChild.FooChildren = new[] { selfChild };
        Console.WriteLine($"selfChild isCircular:{selfChild.IsCircular(foo => foo.Id, foo => foo.FooParents, foo => foo.FooChildren)}");
        
        var parentFoo = new Foo
        {
            Id = 1,
            FooParents = Enumerable.Empty<Foo>(),
            FooChildren = Enumerable.Empty<Foo>(),
        };
        var childFoo = new Foo
        {
            Id = 2,
            FooParents = Enumerable.Empty<Foo>(),
            FooChildren = Enumerable.Empty<Foo>(),
        };
        var middleFoo = new Foo
        {
            Id = 3,
            FooParents = new[] { parentFoo },
            FooChildren = new[] { childFoo },
        };
        Console.WriteLine($"middleFoo isCircular:{middleFoo.IsCircular(foo => foo.Id, foo => foo.FooParents, foo => foo.FooChildren)}");
        
        var ringFooA = new Foo
        {
            Id = 1,
        };
        var ringFooB = new Foo
        {
            Id = 2,
            FooParents = new[] { ringFooA },
        };
        var ringFooC = new Foo
        {
            Id = 3,
            FooParents = new[] { ringFooB },
            FooChildren = new[] { ringFooA },
        };
        ringFooA.FooParents = new[] { ringFooC };
        ringFooA.FooChildren = new[] { ringFooB };
        ringFooB.FooChildren = new[] { ringFooC };
        Console.WriteLine($"ringFooA isCircular:{ringFooA.IsCircular(foo => foo.Id, foo => foo.FooParents, foo => foo.FooChildren)}");
        Console.WriteLine($"ringFooB isCircular:{ringFooB.IsCircular(foo => foo.Id, foo => foo.FooParents, foo => foo.FooChildren)}");
        Console.WriteLine($"ringFooC isCircular:{ringFooC.IsCircular(foo => foo.Id, foo => foo.FooParents, foo => foo.FooChildren)}");
    }
}

public static class Extensions
{
    public static bool IsCircular<T, K>(
        this T item,
        Func<T, K> keySelector, 
        params Func<T, IEnumerable<T>>[] axes)
    {
        var soFar = new HashSet<K>();
        soFar.Add(keySelector(item));
        return axes.Any(axis =>
            axis(item).Any(next => IsCircularOnAxis(
                next,
                keySelector,
                axis,
                soFar)));
    }

    private static bool IsCircularOnAxis<T, K>(
        T item,
        Func<T, K> keySelector,
        Func<T, IEnumerable<T>> axisSelector, 
        HashSet<K> soFar)
    {
        var key = keySelector(item);
        if(soFar.Contains(key))
        {
            return true;
        }

        soFar.Add(key);
        return axisSelector(item).Any(next => 
            IsCircularOnAxis(
                next,
                keySelector,
                axisSelector,
                soFar));
    }
}

public class Foo
{
    public long Id { get; set; }

    public IEnumerable<Foo> FooParents { get; set; }
    public IEnumerable<Foo> FooChildren { get; set; }
}
Related