Enums and Literals

One of the things I love about C# is reflection. Because of assemblies and metadata, and an excellent supporting framework, it is very easy to take advantage of this paradigm.

This week I threw myself into building an XMPP component framework from scratch. The first objective is to get the plumbing and architecture in place. To get it up and running with highly cohesive parts loosely integrated into the whole. I’ll upload it soon and post an update.

The next step is to refactor the XMPP layer to standards and increase the scope and functionality.

For anyone interested, this brief post describes a technique I use to simplify working with enums.

XMPP standards specify XML elements which support very specific options, or domain values if you will. This is an example of a stream error type (unsupported-version):

<stream:error>
    <unsupported-version xmlns='urn:ietf:params:xml:ns:xmpp-streams'/>
</stream:error>

In my code there is a string literal for this element name.

// The version attribute provided by the initiating entity in the header
// specifies a version of XMPP that is not supported by the server.
public const string UnsupportedVersion = "unsupported-version";

When coding I prefer to work with a set of known types, rather than string literals. I represent the various XMPP values as enums types. You’ll notice that unsupported-version is an invalid name, so I can’t go from name to value as per normal. I need to decouple the name and literal value.

This is where attributes come in handy:

/// <summary>
/// The various XMPP stream error type elements.
/// </summary>
[Enum(Xl.Unknown)]
public enum StreamErrorType
{
    [Text(Xl.UnsupportedVersion)]
    UnsupportedVersion,

The first attribute identifies the enum type, and its default text (literal) value:

/// <summary>
/// Identifies enums to be cached in the system.
/// </summary>
public class EnumAttribute : Attribute
{   
    public EnumAttribute(string defaultText) 
        => Default = defaultText;     
    
    public string Default { get; }
}

The second identifies the enum values and their associated text values:

[AttributeUsage(AttributeTargets.Field)]
public class TextAttribute : Attribute
{
    public TextAttribute(string text) 
        => Text = text;
    
    public string Text { get; }
}   

Using reflection, I scan all these enums and build up two dictionaries for each enum type:

static readonly Dictionary<Type, Dictionary<string, object>> TextToEnum = new();

static readonly Dictionary<Type, Dictionary<object, string>> EnumToText = new();

And I use an extension method to resolve enums and their associated literal values:

public static class EnumExtension
{    
    // Converts a registered enum value to its literal. 

    public static string AsText(this Enum value) 
        => Enums.AsText(value);
    

    /// Converts a literal to its registered enum value. 

    public static T As<T>(this string text) where T : Enum 
        => Enums.AsEnum<T>(text);
}

Which facilitates code like this:

// call to add a stream error type to the StreamError XML packet:

stream.AddError(StreamErrorType.UnsupportedVersion)

// which converts the type to text:

var name = type.AsText()

// here we test if an XML element is a specific type

if (name.As<StreamErrorType>() == StreamErrorType.UnsupportedVersion)...

Here is another example enum representing various authentication types:

[Enum(Xl.Unknown)]
public enum AuthType
{
    [Text(Xl.Unknown)]
    Unknown,
    
    [Text(Xl.External)]
    External,
    
    [Text(Xl.ScramSha1Plus)]
    ScramSha1Plus,
    
    [Text(Xl.ScramSha1)]
    ScramSha1,
    
    [Text(Xl.Plain)]
    Plain,
    
    [Text(Xl.Digest)]
    Digest
}

And a corresponding unit:

[TestFixture]
public class AuthTypeTest : TestBase
{
    [Test]
    public void TestEnumToString()
    {
        Assert.AreEqual(Xl.Unknown, AuthType.Unknown.AsText());
        Assert.AreEqual(Xl.External, AuthType.External.AsText());
        Assert.AreEqual(Xl.ScramSha1, AuthType.ScramSha1.AsText());
        Assert.AreEqual(Xl.ScramSha1Plus, AuthType.ScramSha1Plus.AsText());
        Assert.AreEqual(Xl.Plain, AuthType.Plain.AsText());
        Assert.AreEqual(Xl.Digest, AuthType.Digest.AsText());
    }

    [Test]
    public void TestStringToEnum()
    {
        Assert.AreEqual(Xl.Unknown.As<AuthType>(), AuthType.Unknown);
        Assert.AreEqual(Xl.External.As<AuthType>(), AuthType.External);
        Assert.AreEqual(Xl.ScramSha1.As<AuthType>(), AuthType.ScramSha1);
        Assert.AreEqual(Xl.Plain.As<AuthType>(), AuthType.Plain);
        Assert.AreEqual(Xl.Digest.As<AuthType>(), AuthType.Digest);
        Assert.AreEqual("no type".As<AuthType>(), AuthType.Unknown);
    }
}

I hope you may have found this post useful.

Advertisement

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s