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