Background
I'm using Contentful to drive UIs, allowing non-technical stakeholders to change what customers see. Nothing groundbreaking here. However, there is an interesting problem to solve which Contentful introduces by having a highly flexible workflow.
Contentful works by allowing pieces of 'content' to link to other content 'by reference'. In the normal case, you end up with a few content models pointing to each other, A->B->C->D, etc. But I found myself with an interesting conundrum where we have a business case that legitimately sees the definition of content models that are essentially circular.
A can reference B, B can reference C, and C can reference A or B.
This means if one were to sit there and actively click through the UI and link all these pieces of content up, I need to be able to resolve these as the user expected.
Let's consider defining:
public class Foo
{
public required Bar Bar { get; init; }
}
public class Bar
{
public required Foo Foo { get; init; }
}
How about trying to create these?
var foo = new Foo { Bar = new Bar { Foo = new Foo() } };
if you try this for yourself you will get an error
Required member 'Foo.Bar' must be set in the object initializer or attribute constructor.
You can mess around with it and make them nullable so this error goes away as assign by reference and not value.
public class Foo
{
public Bar? Bar { get; set; }
}
public class Bar
{
public Foo? Foo { get; set; }
}
var foo = new Foo();
var bar = new Bar();
foo.Bar = bar;
bar.Foo = foo;
However, if you ever try to serialize this, you can say bye-bye to your memory. Best case, you are using Newtonsoft and it will throw, telling you about it. Worst case, your task dies after a glorious and expensive few seconds.
Cue Recursion
In C# at least, there are two things which make developers' eyes twitch: reflection and recursion. Recently, I have become a firm believer that in the proper place, these two things are very powerful and even elegant. You just have to have a warped enough mind to be able to think recursively. When we encounter these problems as challenges, they are often overly complex or abstract, but hopefully, you'll now see that thinking recursively can be easy when you understand the problem.
We already know that a human configures this system, so it's going to stop at some point. We already know that we start with a single content model and the references fan out from that point. We already know that we can reference ourselves.
So what does this sound like? Well, it's a tree! So let's walk it.
Conceptually, you take the first piece of content and resolve it, leaving references untouched.
- A is a piece of content with data and a reference to B. So now resolve B.
- B is a piece of content with data and a reference to C. So now resolve C.
- C is a piece of content with data and a reference to A. So now resolve A.
Crucially, at this point, these are not classes; these are instances of an object, so there is nothing circular going on. What does this practically look like?
Walk(IEnumerable<INodes> nodes)
{
foreach(var node in nodes){
switch(node){
case A a:
Walk(a.nodes);
// Business logic...
break;
case B b:
Walk(b.nodes);
// Business logic...
break;
case C c:
Walk(c.nodes);
// Business logic...
break;
}
}
return "something";
}
Eventually, any of the nodes will be empty, and the stack will unwind and resolve the concrete values.
Conclusion
Is it neat and an elegant solution? Yes. Is it for everyone? No.
Could I have used a while(true)? Yes, probably, but I found that hard to reason about.
I know many developers who would avoid recursion at all costs, but a lesson learned here is that it really does have a time and place. I'm fully aware that it is simply one of those things in life that you have to experience to believe.