Smart enums / Type-safe enums in .NET

Enumeration in .NET are very useful to avoid magic strings/numbers. .NET provides useful methods such as GetValues, Parse, TryParse, ToString.

public enum Role
{
    [DisplayName("Viewer")]
    [Description("read and comment on posts and pages")]
    Guest = 0,
    [DisplayName("Editor")]
    [Description("has access to all posts, pages, comments, categories, tags, and links")]
    Editor = 1,
    [DisplayName("Author")]
    [Description("can write, upload photos to, edit, and publish their own posts")]
    Author = 2,
    [DisplayName("Contributor")]
    [Description("has no publishing or uploading capability, but can write and edit their own posts until they are published")]
    Contributor = 3,
    [DisplayName("Admin")]
    [Description("has full power over the site and can do everything related to site administration")]
    Administrator = 4,
}

However, it doesn't provide more than a mapping between a name and a number. You can extend it by adding attributes, but it can become clumpy and hard to use. With the time, you may have lots of attributes and the enumeration is very hard to use. To get the value of these attributes you may want to use extension methods. This mean you have one extension method per attribute. And your code becomes less easy to use.

An alternative to enumeration is the smart enumeration or type-safe enumeration pattern. The idea is to use a class with read only properties to define the enumeration members. What's interesting is that you can use as many properties as needed to replace the attributes you add on enumeration members. To mimic the enumeration, you may want to prevent user from creating new instances by using a private constructor and sealing the class.

public sealed class Role
{
    public static Role Guest { get; } = new Role(0, "Viewer", "read and comment on posts and pages");
    public static Role Editor { get; } = new Role(1, "Editor", "has access to all posts, pages, comments, categories, tags, and links");
    public static Role Author { get; } = new Role(2, "Author", "can write, upload photos to, edit, and publish their own posts");
    public static Role Contributor { get; } = new Role(3, "Contributor", "has no publishing or uploading capability, but can write and edit their own posts until they are published");
    public static Role Administrator { get; } = new Role(4, "Admin", "has full power over the site and can do everything related to site administration");

    private Role(int id, string name, string description)
    {
        Id = id;
        Name = name;
        Description = description;
    }

    public int Id { get; }
    public string Name { get; }
    public string Description { get; }
}

Of course, you'll want to have the same functionalities as the enumeration, so let's add some methods:

public sealed class Role
{
    public override string ToString() => Name;
    public static IEnumerable<string> GetNames() => GetValues().Select(role => role.Name);
    public static Role GetValue(int id) => GetValues().First(role => role.Id == id);
    public static Role GetValue(string name) => GetValues().First(role => role.Name == name);

    public static IReadOnlyList<Role> GetValues()
    {
        // There are other ways to do that such as filling a collection in the constructor
        return typeof(Role).GetProperties(BindingFlags.Public | BindingFlags.Static)
            .Select(property => (Role)property.GetValue(null))
            .ToList();
    }

    public static explicit operator int(Role role) => role.Id; // int value = (int)Role.Author;
    public static explicit operator Role(int id) => GetValue(id); // Role role = (Role)1;
}

Enumeration are very easy to use with a switch statement. The type-safe enum is not as easy to use but you can get something a little more verbose but still readable thank to the new features of C# 7:

var value = Role.Editor;
switch (value)
{
    case var _ when value == Role.Guest:
        break;

    case var _ when value == Role.Editor:
        break;
}

There are still some drawbacks such as the serialization or UI integration. Enumerations are serialized using the number or string value, which is not the case of the type-safe enum. You can still implement IXmlSerializable, IBinarySerialize, JsonConverter or whatever mechanism provided by your serializer to customize the serialization. Also, the enumerations are well supported by some controls such as PropertyGrid. You can achieve the same result by implementing a UITypeEditor for the smart enum.

Conclusion

Do not replace all enumerations with this kind of enumerations. Instead, use them when the standard enumerations do not fit your needs. For instance, when you decorate enumeration member with many attributes to add behaviors, or when you want to map the values of a database that contains constant values such as a Role table.

Comments

Joe -

Thanks for the article.

Note: Typo in, Admininistrator => Administrator

Id is probably not 2 but 4. In: public static Role Admininistrator { get; } = new Role(2, "Admin", "has full power over the site and can do everything related to site administration");

Meziantou -

Thanks Joe for pointing me out these mistakes!

mohsen -

Thanks for the article.

Armin -

Thanks a lot, Gerald! Very useful indeed. Learned some cool C# features here. I'm definitely going to refactor my enums and use these TypeSafe Enums in my active project. Cheers!

Leave a reply