Enumerations in .NET are very useful for avoiding magic strings and numbers. .NET provides helpful methods such as GetValues, Parse, TryParse, and ToString.
C#
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, enumerations provide little more than a mapping between a name and a number. You can extend them by adding attributes, but this approach can become unwieldy over time. As attributes accumulate, the enumeration grows harder to work with. Retrieving attribute values typically requires extension methods, one per attribute, making the code more verbose and less intuitive.
An alternative is the smart enum, also known as the type-safe enum pattern. The idea is to use a class with read-only static properties to define the enum members. This lets you add as many properties as needed, eliminating the need for attributes entirely. To mimic a standard enum, you can prevent external instantiation by using a private constructor and sealing the class.
C#
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 will want the same functionality as a standard enum, so let's add some methods:
C#
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;
}
Enumerations work naturally with switch statements. The type-safe enum requires a slightly more verbose syntax, but it is still readable thanks to the pattern matching features introduced in C# 7:
C#
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, particularly around serialization and UI integration. Standard enumerations serialize using the number or string value by default, which is not the case for type-safe enums. You can work around this by implementing IXmlSerializable, IBinarySerialize, JsonConverter, or whatever customization mechanism your serializer provides. Similarly, standard enumerations are well supported by controls such as PropertyGrid, but you can achieve the same result by implementing a UITypeEditor for your smart enum.
#Conclusion
Do not replace all enumerations with smart enums. Instead, use them when standard enumerations do not meet your needs, such as when you need to decorate enum members with many attributes to encode behavior, or when you want to map values from a database table containing constant values like a Role table.
Do you have a question or a suggestion about this post? Contact me!