When working on the bot-zero-sharp project, the need arose for better handling of enums decorated with a [Flags]
attribute. In a previous article we explored how to use Handlebars.NET to generate JSON strings. In this article we'll build further on that JSON generator to add support for enums. We will also move away from the static JsonHandlebarsDotNet
to an injectable version.
Through this article I'll be working with the following enum:
[Flags]
enum MenuItems
{
None = 0,
Pizza = 1,
Fries = 2,
Pancakes = 4,
Meatballs = 8,
Pasta = 16,
StuffWithP = Pizza | Pancakes | Pasta,
All = Pizza | Fries | Pancakes | Meatballs | Pasta | StuffWithP
};
You can see that it has all the nice features a flagged enum comes with: multiple combinations and a None value. It is going to be fun; fun with flags!

Deconstructing a [Flags] enum
When we want to deconstruct our enum to the unique flag values, we see some special use cases:
- The
None
has value0
, so it can't be used with theHasFlag
operation, as it will always returntrue
. SoMenuItems.Pasta.HasFlag(MenuItems.None)
will return true. We will need to skip this value. - The
StuffWithP
andAll
values are combined values, so I want their individual values to appear in a deconstructed array.
The following method will deconstruct a flagged enum to a string array of unique values:
public static string[] DeconstructFlags(Enum items)
{
if (items.GetType().GetCustomAttribute<FlagsAttribute>() == null)
{
throw new ArgumentException("Enum has no [Flags] attribute.", nameof(items));
}
// no value, no list
var itemsValue = (int)(object)items;
if (itemsValue == 0) return Array.Empty<string>();
var result = new List<string>();
foreach (var item in Enum.GetValues(items.GetType()))
{
if(item == null) continue;
var value = (int)item;
// skip combined flags
if (!BitOperations.IsPow2(value))
{
continue;
}
if (items.HasFlag((Enum)item))
{
result.Add(item.ToString() ?? "");
}
}
return result.ToArray();
}
This will return the following arrays:
void Echo(MenuItems items)
{
Console.WriteLine($"{items}: {String.Join(" + ", DeconstructFlags(items))}");
}
Echo(MenuItems.None);
// outputs: None: None
Echo(MenuItems.Pasta);
// outputs: Pasta: Pasta
Echo(MenuItems.Fries | MenuItems.Pizza);
// outputs: Pizza, Fries: Pizza + Fries
Echo(MenuItems.StuffWithP);
// outputs: StuffWithP: Pizza + Pancakes + Pasta
Note how it is skipping the StuffWithP
value and deconstructs properly.
Make Handlebars.NET respect the [Flags]
Now, we would love to iterate over our flagged enums using an {{#each }}
statement. By default this is not possible and Handlebars.NET will just return the integer value of your enum. We'll make a FlaggedEnumObjectDescriptorProvider
that fixes this:
var order = new
{
items = MenuItems.Pizza | MenuItems.Pancakes
};
string source = @"
{
""order"": [
{{#each items}}""{{this}}""{{#unless @last}},
{{/unless}}{{/each}}
],
""orders"": ""{{items}}""
}";
var hbs = new JsonTemplateGenerator().Handlebars;
hbs.Configuration.ObjectDescriptorProviders.Add(new FlaggedEnumObjectDescriptorProvider());
var template = hbs.Compile(source);
var json = template(order);
Console.WriteLine(json);
This will output:
{
"order": [
"Pizza",
"Pancakes"
],
"orders": "Pizza, Pancakes"
}
The trick is to register a FlaggedEnumObjectDescriptorProvider
that will handle the flagged enums.
Implementing an IObjectDescriptorProvider
I did some research on how to implement an IObjectDescriptorProvider
and took the Handlebars.Net.Extension.Json array iterator as inspiration. The class checks if the type
is an enum with the [Flags]
attribute and returns an iterator that uses the DeconstructFlags
method we've seen before.
using HandlebarsDotNet.Compiler;
using HandlebarsDotNet.Iterators;
using HandlebarsDotNet.ObjectDescriptors;
using HandlebarsDotNet.PathStructure;
using HandlebarsDotNet.Runtime;
using HandlebarsDotNet.ValueProviders;
using System.Numerics;
using System.Reflection;
public class FlaggedEnumObjectDescriptorProvider : IObjectDescriptorProvider
{
public bool TryGetDescriptor(Type type, out ObjectDescriptor value)
{
if (!type.IsEnum || type.GetCustomAttribute<FlagsAttribute>() == null)
{
value = ObjectDescriptor.Empty;
return false;
}
value = new ObjectDescriptor(
type,
null,
null,
self => new FlagEnumInterator()
);
return true;
}
public static string[] DeconstructFlags(Enum items)
{
if (items.GetType().GetCustomAttribute<FlagsAttribute>() == null)
{
throw new ArgumentException("Enum has no [Flags] attribute.", nameof(items));
}
// no value, no list
var itemsValue = (int)(object)items;
if (itemsValue == 0) return Array.Empty<string>();
var result = new List<string>();
foreach (var item in Enum.GetValues(items.GetType()))
{
if(item == null) continue;
var value = (int)item;
// skip combined flags
if (!BitOperations.IsPow2(value))
{
continue;
}
if (items.HasFlag((Enum)item))
{
result.Add(item.ToString() ?? "");
}
}
return result.ToArray();
}
internal class FlagEnumInterator : IIterator
{
public void Iterate(in HandlebarsDotNet.EncodedTextWriter writer, HandlebarsDotNet.BindingContext context, ChainSegment[] blockParamsVariables, object input, TemplateDelegate template, TemplateDelegate ifEmpty)
{
using var innerContext = context.CreateFrame();
var iterator = new IteratorValues(innerContext);
var blockParamsValues = new BlockParamsValues(innerContext, blockParamsVariables);
blockParamsValues.CreateProperty(0, out var _0);
blockParamsValues.CreateProperty(1, out var _1);
iterator.First = BoxedValues.True;
iterator.Last = BoxedValues.False;
var target = DeconstructFlags((Enum)input);
var count = target.Length;
var enumerator = target.GetEnumerator();
var index = 0;
var lastIndex = count - 1;
while (enumerator.MoveNext())
{
var value = enumerator.Current;
var objectIndex = BoxedValues.Int(index);
if (index == 1) iterator.First = BoxedValues.False;
if (index == lastIndex) iterator.Last = BoxedValues.True;
iterator.Index = objectIndex;
object resolvedValue = value;
blockParamsValues[_0] = resolvedValue;
blockParamsValues[_1] = objectIndex;
iterator.Value = resolvedValue;
innerContext.Value = resolvedValue;
template(writer, innerContext);
++index;
}
if (index == 0)
{
innerContext.Value = context.Value;
ifEmpty(writer, innerContext);
}
}
}
}
Notice how @first
and @last
are also implemented by this class.
Improve the JsonTemplateGenerator
We can now add the FlaggedEnumObjectDescriptorProvider
to the ObjectDescriptorProviders
of our JsonTemplateGenerator
constructor:
public JsonTemplateGenerator()
{
Handlebars = HandlebarsDotNet.Handlebars.Create();
Handlebars.Configuration.TextEncoder = new JsonTextEncoder();
Handlebars.Configuration.ObjectDescriptorProviders.Add(new FlaggedEnumObjectDescriptorProvider());
}
One might also consider adding the helpers to it: HandlebarsHelpers.Register(Handlebars);
bot-zero-sharp
I've created this code for bot-zero-sharp, so it can use Handlebar templates for its Slack dialogs. Here are references to the classes used by this article:
Final thoughts
I just love Handlebars and the fact that the template engine is so versatile. It is super easy to handle enum flags.
Further reading
When writing this article, I stumbled upon some resources that might interest you:
- StackOverflow answer: What does the [Flags] Enum Attribute mean in C#? - I didn't know that it did nothing with values, but only with the
.ToString()
. - StackOverflow question: Why are flag enums usually defined with hexadecimal values - I personally don't define them that way, but this article has some nice insights to how you can define them. I like the
1 << 0, 1 << 1, 1 << 2
notation, suggested by the answer.
Changelog
- 2022-09-24: Moved the dependency injection section to Handlebars.Net & JSON templates: Inject a JSON Template Generator.
- 2022-09-18: Initial article.