NuGet: Ardalis.SmartEnum
If you like or are using this project please give it a star. Thanks!
An implementation of a type-safe object-oriented alternative to C# enum.
Thanks to Scott Depouw and Antão Almada for help with this project!
The framework is provided as a set of NuGet packages.
To install the minimum requirements:
Install-Package Ardalis.SmartEnum
To install support for serialization, select the lines that apply:
Install-Package Ardalis.SmartEnum.AutoFixture
Install-Package Ardalis.SmartEnum.JsonNet
Install-Package Ardalis.SmartEnum.Utf8Json
Install-Package Ardalis.SmartEnum.MessagePack
Install-Package Ardalis.SmartEnum.ProtoBufNet
Define your smart enum by inheriting from SmartEnum<TEnum>
where TEnum
is the type you're declaring. For example:
using Ardalis.SmartEnum;
public sealed class TestEnum : SmartEnum<TestEnum>
{
public static readonly TestEnum One = new TestEnum(nameof(One), 1);
public static readonly TestEnum Two = new TestEnum(nameof(Two), 2);
public static readonly TestEnum Three = new TestEnum(nameof(Three), 3);
private TestEnum(string name, int value) : base(name, value)
{
}
}
The default value type is int
but it can be set using the second generic argument TValue
.
The string alias can also be set explicitly, where spaces are allowed.
using Ardalis.SmartEnum;
public sealed class TestEnum : SmartEnum<TestEnum, ushort>
{
public static readonly TestEnum One = new TestEnum("A string!", 1);
public static readonly TestEnum Two = new TestEnum("Another string!", 2);
public static readonly TestEnum Three = new TestEnum("Yet another string!", 3);
private TestEnum(string name, ushort value) : base(name, value)
{
}
}
Just like regular enum
, more than one string can be assigned to the same value but only one value can be assigned to a string:
using Ardalis.SmartEnum;
public sealed class TestEnum : SmartEnum<TestEnum>
{
public static readonly TestEnum One = new TestEnum(nameof(One), 1);
public static readonly TestEnum Two = new TestEnum(nameof(Two), 2);
public static readonly TestEnum Three = new TestEnum(nameof(Three), 3);
public static readonly TestEnum AnotherThree = new TestEnum(nameof(AnotherThree), 3);
// public static TestEnum Three = new TestEnum(nameof(Three), 4); -> throws exception
private TestEnum(string name, int value) : base(name, value)
{
}
}
In this case, TestEnum.FromValue(3)
will return the first instance found, either TestEnum.Three
or TestEnum.AnotherThree
. No order should be assumed.
The Value
content is used when comparing two smart enums, while Name
is ignored:
TestEnum.One.Equals(TestEnum.One); // returns true
TestEnum.One.Equals(TestEnum.Three); // returns false
TestEnum.Three.Equals(TestEnum.AnotherThree); // returns true
Inheritance can be used to add "behavior" to a smart enum.
This example adds a BonusSize
property, avoiding the use of the switch
tipically used with regular enums:
using Ardalis.SmartEnum;
public abstract class EmployeeType : SmartEnum<EmployeeType>
{
public static readonly EmployeeType Manager = new ManagerType();
public static readonly EmployeeType Assistant = new AssistantType();
private EmployeeType(string name, int value) : base(name, value)
{
}
public abstract decimal BonusSize { get; }
private sealed class ManagerType : EmployeeType
{
public ManagerType() : base("Manager", 1) {}
public override decimal BonusSize => 10_000m;
}
private sealed class AssistantType : EmployeeType
{
public AssistantType() : base("Assistant", 2) {}
public override decimal BonusSize => 1_000m;
}
}
This other example implements a state machine. The method CanTransitionTo()
returns true
if it's allowed to transition from current state to next
; otherwise returns false
.
using Ardalis.SmartEnum;
public abstract class ReservationStatus : SmartEnum<ReservationStatus>
{
public static readonly ReservationStatus New = new NewStatus();
public static readonly ReservationStatus Accepted = new AcceptedStatus();
public static readonly ReservationStatus Paid = new PaidStatus();
public static readonly ReservationStatus Cancelled = new CancelledStatus();
private ReservationStatus(string name, int value) : base(name, value)
{
}
public abstract bool CanTransitionTo(ReservationStatus next);
private sealed class NewStatus: ReservationStatus
{
public NewStatus() : base("New", 0)
{
}
public override bool CanTransitionTo(ReservationStatus next) =>
next == ReservationStatus.Accepted || next == ReservationStatus.Cancelled;
}
private sealed class AcceptedStatus: ReservationStatus
{
public AcceptedStatus() : base("Accepted", 1)
{
}
public override bool CanTransitionTo(ReservationStatus next) =>
next == ReservationStatus.Paid || next == ReservationStatus.Cancelled;
}
private sealed class PaidStatus: ReservationStatus
{
public PaidStatus() : base("Paid", 2)
{
}
public override bool CanTransitionTo(ReservationStatus next) =>
next == ReservationStatus.Cancelled;
}
private sealed class CancelledStatus: ReservationStatus
{
public CancelledStatus() : base("Cancelled", 3)
{
}
public override bool CanTransitionTo(ReservationStatus next) =>
false;
}
}
You can list all of the available options using the enum's static List
property:
foreach (var option in TestEnum.List)
Console.WriteLine(option.Name);
List
returns an IReadOnlyCollection
so you can use the Count
property to efficiently get the number os available options.
var count = TestEnum.List.Count;
Access an instance of an enum by matching a string to its Name
property:
var myEnum = TestEnum.FromName("One");
Exception SmartEnumNotFoundException
is thrown when name is not found. Alternatively, you can use TryFromName
that returns false
when name is not found:
if (TestEnum.TryFromName("One", out var myEnum))
{
// use myEnum here
}
Both methods have a ignoreCase
parameter (the default is case sensitive).
Access an instance of an enum by matching its value:
var myEnum = TestEnum.FromValue(1);
Exception SmartEnumNotFoundException
is thrown when value is not found. Alternatively, you can use TryFromValue
that returns false
when value is not found:
if (TestEnum.TryFromValue(1, out var myEnum))
{
// use myEnum here
}
Display an enum using the ToString()
override:
Console.WriteLine(TestEnum.One); // One
Given an instance of a TestEnum, switch depending on value:
switch(testEnumVar.Name)
{
case nameof(TestEnum.One):
...
break;
case nameof(TestEnum.Two):
...
break;
case nameof(TestEnum.Three):
...
break;
default:
...
break;
}
Using pattern matching:
switch(testEnumVar)
{
case null:
...
break;
case var e when e.Equals(TestEnum.One):
...
break;
case var e when e.Equals(TestEnum.Two):
...
break;
case var e when e.Equals(TestEnum.Three):
...
break;
default:
...
break;
}
EF Core 2.1 introduced value conversions which can be used to map SmartEnum types to simple database types. For example, given an entity named Policy
with a property PolicyStatus
that is a SmartEnum, you could use the following code to persist just the value to the database:
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Policy>()
.Property(p => p.PolicyStatus)
.HasConversion(
p => p.Value,
p => PolicyStatus.FromValue(p));
}
New instance of a SmartEnum
should not be created. Instead, references to the existing ones should always be used. AutoFixture by default doesn't know how to do this. The Ardalis.SmartEnum.AutoFixture
package includes a specimen builder for SmartEnum
. Simply add the customization to the IFixture
builder:
var fixture = new Fixture()
.Customize(new SmartEnumCustomization());
var smartEnum = fixture.Create<TestEnum>();
When serializing a SmartEnum
to JSON, only one of the properties (Value
or Name
) should be used. Json.NET by default doesn't know how to do this. The Ardalis.SmartEnum.JsonNet
package includes a couple of converters to achieve this. Simply use the attribute JsonConverterAttribute to assign one of the converters to the SmartEnum
to be de/serialized:
public class TestClass
{
[JsonConverter(typeof(SmartEnumNameConverter<int>))]
public TestEnum Property { get; set; }
}
uses the Name
:
{
"Property": "One"
}
While this:
public class TestClass
{
[JsonConverter(typeof(SmartEnumValueConverter<int>))]
public TestEnum Property { get; set; }
}
uses the Value
:
{
"Property": 1
}