JustNik

IEnumerable Value with value based ancestor fallback in Umbraco

Umbracologoblue05

Introduction

Have you ever wanted to get a value from an Umbraco property knowing it returns an IEnumerable<T> but also wanted it to use the ancestor fallback if all the enumerable is empty or the values selected match a specific criteria?

Well, it just so happens that I had this requirement crop up.

Background

During a recent project Contentment's Robots property editor was being used on all primary page types to allow control over the Robots meta tag on pages. When getting this value, however, the rule was it should work back up the tree to get the value if the current page doesn't have one allowing entire sections of the page to be blocked off if need be.

So imagine the following structure:

Node 1
    -> Node 1.1
        -> Node 1.1.1

If Node 1.1.1 has robots property set then it should use that for the meta value, else it looks up the tree to Node 1.1 or Node 1 to get it's value.

Conceptually all was fine, Umbraco supports this out of the box via the fallback optional parameter on the .Value method: Model.Value<IEnumerable<string>>("robots", fallback: Fallback.Ancestor). Now this worked great, until you set the value on Node 1.1.1 and then Unset it as it now doesn't fall back up the tree as it still thinks it has a value, despite it being an empty array.

The solution

This got me thinking, does this happen with all Enumerable return types for properties, the answer I think is Most Likely, so I came up with an extension method that allows you to do:

Model.EnumerableValue<string>("robots", r => !string.IsNullOrWhiteSpace(r))

How does it work?

This will work back up the tree through the parents applying the check to values from the robots property until it either hits the top of the tree or hits a valid value and it looks like this:

public static IEnumerable<T> EnumerableValue<T>(this IPublishedContent content, string alias, Func<T, bool>? predicate = null)
{
    var currentValue = content.Value<IEnumerable<T>>(alias);

    if (currentValue != null)
    {
        if (predicate == null && currentValue.Any(c => c != null)) 
            return currentValue;
        else if(predicate != null && currentValue.Any(predicate)) return currentValue.Where(predicate).ToArray();
    }

    if (content.Parent != null)
        return content.Parent.EnumerableValue(alias, predicate);

    return [];
}

This extension method allows you to specify the type of Enumerable that will be returned, e.g. is it an enumerable of strings, or int's, or custom classes but it also allows you to pass in a predicate function. This function will be applied to the Any Linq method under the hood and allows you to conditionally work back up the tree one parent at a time to get a valid value.

Notes

This is probably not the most performant approach, I get that, but for what I needed it for, it worked and it seemed to work quite well. In fact, if you had an option of "Use Parent" it works really well as your predicate can check for that option and force itself back up the tree or if you need more complex checking for some reason.

JustNik
Connect with me
Twitter GitHub LinkedIn
© JustNik 2024