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, Antão Almada, and Nagasudhir Pulla for help with this project!
The framework is provided as a set of NuGet packages. In many cases you'll only need the base package, but if you need serialization and/or ORM support there are many implementation-specific packages available to assist.
To install the minimum requirements:
Install-Package Ardalis.SmartEnum
To install support for serialization, AutoFixture, EF Core, Model Binding, or Dapper select the lines that apply:
Install-Package Ardalis.SmartEnum.AutoFixture
Install-Package Ardalis.SmartEnum.JsonNet
Install-Package Ardalis.SmartEnum.SystemTextJson
Install-Package Ardalis.SmartEnum.Utf8Json
Install-Package Ardalis.SmartEnum.MessagePack
Install-Package Ardalis.SmartEnum.ProtoBufNet
Install-Package Ardalis.SmartEnum.EFCore
Install-Package Ardalis.SmartEnum.ModelBinding
Install-Package Ardalis.SmartEnum.Dapper
The latest version of the package supports .NET 8 and NetStandard 2.0.
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
typically 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;
}
}
You can take this a step further and use the ManagerType
and associated BonusSize
property in a parent class like so:
public class Manager
{
private ManagerType _managerType { get; set; }
public string Type
{
get => _managerType.Name;
set
{
if (!ManagerType.TryFromName(value, true, out var parsed))
{
throw new Exception($"Invalid manager type of '{value}'");
}
_managerType = parsed;
}
}
public string BonusSize
{
get => _managerType.BonusSize();
set => _bonusSize_ = value;
}
}
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 of 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;
}
Because of the limitations of pattern matching SmartEnum also provides a fluent interface to help create clean code:
testEnumVar
.When(TestEnum.One).Then(() => ... )
.When(TestEnum.Two).Then(() => ... )
.When(TestEnum.Three).Then(() => ... )
.Default( ... );
N.B. For performance critical code the fluent interface carries some overhead that you may wish to avoid. See the available benchmarks code for your use case.
Support has been added for a Flag
functionality.
This feature is similar to the behaviour seen when applying the [Flag]
attribute to Enums in the .NET Framework
All methods available on the SmartFlagEnum
class return an IEnumerable<SmartFlagEnum>
with one or more values depending on the value provided/method called.
Some Functionality is shared with the original SmartEnum class, listed below are the variations.
When setting the values for a SmartFlagEnum
It is imperative to provide values as powers of two. If at least one value is not set as power of two or two or more power of two values are provided inconsecutively (eg: 1, 2, no four!, 8) a SmartFlagEnumDoesNotContainPowerOfTwoValuesException
will be thrown.
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum Bpay = new SmartFlagTestEnum(nameof(Bpay), 4);
public static readonly SmartFlagTestEnum Paypal = new SmartFlagTestEnum(nameof(Paypal), 8);
public static readonly SmartFlagTestEnum BankTransfer = new SmartFlagTestEnum(nameof(BankTransfer), 16);
public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
This behaviour can be disabled by applying the AllowUnsafeFlagEnumValuesAttribute
to the smart enum class. Note: If power of two values are not provided the SmarFlagEnum will not behave as expected!
[AllowUnsafeFlagEnumValues]
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum Bpay = new SmartFlagTestEnum(nameof(Bpay), 4);
public static readonly SmartFlagTestEnum Paypal = new SmartFlagTestEnum(nameof(Paypal), 8);
public static readonly SmartFlagTestEnum BankTransfer = new SmartFlagTestEnum(nameof(BankTransfer), 16);
public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
Combination
values can be provided explicitly and will be returned in place of the multiple flag values that would have been returned from the FromValue()
method.
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum CardAndCash = new SmartFlagTestEnum(nameof(CardAndCash), 3); -- Explicit `Combination` value
public static readonly SmartFlagTestEnum Bpay = new SmartFlagTestEnum(nameof(Bpay), 4);
public static readonly SmartFlagTestEnum Paypal = new SmartFlagTestEnum(nameof(Paypal), 8);
public static readonly SmartFlagTestEnum BankTransfer = new SmartFlagTestEnum(nameof(BankTransfer), 16);
public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
These explicit values can be provided above the highest allowable flag value without consequence, however attempting to access a value that is higher than the maximum flag value that has not explicitly been provided (for example 4) will cause a SmartEnumNotFoundException
to be thrown.
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum AfterPay = new SmartFlagTestEnum(nameof(AfterPay), 5);
public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
var myFlagEnums = FromValue(3) -- Works!
-and-
var myFlagEnums = FromValue(5) -- Works!
-but-
Var myFlagEnums = FromValue(4) -- will throw an exception :(
A Negative One (-1) value may be provided as an All
value. When a value of -1 is passed into any of the FromValue()
methods an IEnumerable containing all values (excluding 0) will be returned.
If an explicit Combination
value exists with a value of -1 this will be returned instead.
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum All = new SmartFlagTestEnum(nameof(All), -1);
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public static readonly SmartFlagTestEnum Cash = new SmartFlagTestEnum(nameof(Cash), 2);
public static readonly SmartFlagTestEnum Bpay = new SmartFlagTestEnum(nameof(Bpay), 4);
public static readonly SmartFlagTestEnum Paypal = new SmartFlagTestEnum(nameof(Paypal), 8);
public static readonly SmartFlagTestEnum BankTransfer = new SmartFlagTestEnum(nameof(BankTransfer), 16);
public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
public abstract class EmployeeType : SmartFlagEnum<EmployeeType>
{
public static readonly EmployeeType Director = new DirectorType();
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 DirectorType : EmployeeType
{
public DirectorType() : base("Director", 1) { }
public override decimal BonusSize => 100_000m;
}
private sealed class ManagerType : EmployeeType
{
public ManagerType() : base("Manager", 2) { }
public override decimal BonusSize => 10_000m;
}
private sealed class AssistantType : EmployeeType
{
public AssistantType() : base("Assistant", 4) { }
public override decimal BonusSize => 1_000m;
}
}
public class SmartFlagEnumUsageExample
{
public void UseSmartFlagEnumOne()
{
var result = EmployeeType.FromValue(3).ToList();
var outputString = "";
foreach (var employeeType in result)
{
outputString += $"{employeeType.Name} earns ${employeeType.BonusSize} bonus this year.\n";
}
=> "Director earns $100000 bonus this year.\n"
"Manager earns $10000 bonus this year.\n"
}
public void UseSmartFlagEnumTwo()
{
EmployeeType.FromValueToString(-1)
=> "Director, Manager, Assistant"
}
public void UseSmartFlagEnumTwo()
{
EmployeeType.FromValueToString(EmployeeType.Assistant | EmployeeType.Director)
=> "Director, Assistant"
}
}
Access an IEnumerable
of enum instances by matching a string containing one or more enum names seperated by commas to its Names
property:
var myFlagEnums = TestFlagEnum.FromName("One, Two");
Exception SmartEnumNotFoundException
is thrown when no names are found. Alternatively, you can use TryFromName
that returns false
when no names are found:
if (TestFlagEnum.TryFromName("One, Two", out var myFlagEnums))
{
// use myFlagEnums here
}
Both methods have a ignoreCase
parameter (the default is case sensitive).
Access an IEnumerable
of enum instances by matching a value:
var myFlagEnums = TestFlagEnum.FromValue(3);
Exception SmartEnumNotFoundException
is thrown when no values are found. Alternatively, you can use TryFromValue
that returns false
when values are not found:
if (TestFlagEnum.TryFromValue(3, out var myFlagEnums))
{
// use myFlagEnums here
}
Note: Negative values other than (-1) passed into this method will cause a NegativeValueArgumentException
to be thrown, this behaviour can be disabled by applying the AllowNegativeInput
attribute to the desired SmartFlagEnum
class.
[AllowNegativeInput]
public class SmartFlagTestEnum : SmartFlagEnum<SmartFlagTestEnum>
{
public static readonly SmartFlagTestEnum None = new SmartFlagTestEnum(nameof(None), 0);
public static readonly SmartFlagTestEnum Card = new SmartFlagTestEnum(nameof(Card), 1);
public SmartFlagTestEnum(string name, int value) : base(name, value)
{
}
}
Note: FromValue()
will accept any input that can be succesfully parsed as an integer. If an invalid value is supplied it will throw an InvalidFlagEnumValueParseException
.
Return a string representation of a series of enum instances name's:
var myFlagEnumString = TestFlagEnum.FromValueToString(3);
Exception SmartEnumNotFoundException
is thrown when no values are found. Alternatively, you can use TryFromValueToString
that returns false
when values are not found:
if (TestFlagEnum.TryFromValueToString(3, out var myFlagEnumsAsString))
{
// use myFlagEnumsAsString here
}
Note: Negative values other than (-1) passed into this method will cause a NegativeValueArgumentException
to be thrown, this behaviour can be disabled by applying the AllowNegativeInput
attribute to the desired SmartFlagEnum
class.
The FromValue()
methods allow the Or ( | ) operator to be used to add
enum values together and provide multiple values at once.
var myFlagEnums = TestFlagEnum.FromValue(TestFlagEnum.One | TestFlagEnum.Two);
This will only work where the type of the SmartFlagEnum
has been specified as Int32
or else can be explicitly cast as an Int32
.
var myFlagEnums = TestFlagEnumDecimal.FromValue((int)TestFlagEnum.One | (int)TestFlagEnum.Two);
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));
}
Remember, you need to implement your own parameterless constructor to make it works with db context. See #103 issue.
EF Core 6 introduced pre-convention model configuration which allows value conversions to be configured for specific types within a model. If you have installed Ardalis.SmartEnum.EFCore
it is sufficient to add the following line at the beginning of the ConfigureConventions
method:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.ConfigureSmartEnum();
...
}
For previous versions of EF Core, the following line can be added at the end of the OnModelCreating
method:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.ConfigureSmartEnum();
}
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:
System.Text.Json by default doesn't know how to do this. The Ardalis.SmartEnum.SystemTextJson
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<TestEnum,int>))]
public TestEnum Property { get; set; }
}
uses the Name
:
{
"Property": "One"
}
While this:
public class TestClass
{
[JsonConverter(typeof(SmartEnumValueConverter<TestEnum,int>))]
public TestEnum Property { get; set; }
}
uses the Value
:
{
"Property": 1
}
Note: The SmartFlagEnum works identically to the SmartEnum when being Serialized and Deserialized.
To enable Dapper support for SmartEnum
values, add a SmartEnumTypeHandler
to SqlMapper
for the
given SmartEnum
type. There are two inheritors of SmartEnumTypeHandler
:
SmartEnumByNameTypeHandler
, which maps the Name of a SmartEnum
to a database column, and
SmartEnumByValueTypeHandler
, which maps the Value of a SmartEnum
to a database column.
// Maps the name of TestEnum objects (e.g. "One", "Two", or "Three") to a database column.
SqlMapper.AddTypeHandler(typeof(TestEnum), new SmartEnumByNameTypeHandler<TestEnum>());
// Maps the value of TestEnum objects (e.g. 1, 2, or 3) to a database column.
SqlMapper.AddTypeHandler(typeof(TestEnum), new SmartEnumByValueTypeHandler<TestEnum>());
To avoid needing to explicitly register a SmartEnum
type with Dapper, it can be done automatically
by inheriting from DapperSmartEnumByName
or DapperSmartEnumByValue
instead of from SmartEnum
.
public class TestEnumByName : DapperSmartEnumByName<TestEnumByName>
{
public static readonly TestEnumByName One = new TestEnumByName(1);
public static readonly TestEnumByName Two = new TestEnumByName(2);
public static readonly TestEnumByName Three = new TestEnumByName(3);
protected TestEnumByName(int value, [CallerMemberName] string name = null) : base(name, value)
{
}
}
public class TestEnumByValue : DapperSmartEnumByValue<TestEnumByValue>
{
public static readonly TestEnumByValue One = new TestEnumByValue(1);
public static readonly TestEnumByValue Two = new TestEnumByValue(2);
public static readonly TestEnumByValue Three = new TestEnumByValue(3);
protected TestEnumByValue(int value, [CallerMemberName] string name = null) : base(name, value)
{
}
}
Inheritors of DapperSmartEnum
can be decorated with custom attributes in order to configure
its type handler. Use DbTypeAttribute
(e.g. [DbType(DbType.String)]
) to specify that parameters
should have their DbType
property set to the specified value. Use DoNotSetDbTypeAttribute
(e.g.
[DoNotSetDbType]
) to specify that parameters should not have their DbType
property set. Use
IgnoreCaseAttribute
(e.g. [IgnoreCase]
) when inheriting from DapperSmartEnumByName
to specify
that database values do not need to match the case of a SmartEnum Name.
When creating enums of strings, the default behaviour of SmartEnum is to compare the strings with a case sensitive comparer. It is possible to specify a different equality comparer for the enum values, for example a case insensitive one:
[SmartEnumStringComparer(StringComparison.InvariantCultureIgnoreCase)]
public class CaseInsensitiveEnum : SmartEnum<CaseInsensitiveEnum, string>
{
protected CaseInsensitiveEnum(string name, string value) : base(name, value) { }
public static CaseInsensitiveEnum One = new CaseInsensitiveEnum("One", "one");
public static CaseInsensitiveEnum Two = new CaseInsensitiveEnum("Two", "two");
}
var e1 = CaseInsensitiveEnum.FromValue("ONE");
var e2 = CaseInsensitiveEnum.FromValue("one");
//e1 is equal to e2
The DataAnnotations ValidationAttribute SmartEnumNameAttribute
allows you to validate your models, mandating that when provided a value it must be matching the name of a given SmartEnum
. This attribute allows null
values (use [Required]
to disallow nulls).
In addition to specifying the SmartEnum
to match, you may also pass additional parameters:
allowCaseInsensitiveMatch
(default false
)errorMessage
(default "{0} must be one of: {1}"
): A format string to customize the error
{0}
is the name of the property being validated{1}
is the comma-separated list of valid SmartEnum
namespublic sealed class ExampleSmartEnum : SmartEnum<ExampleSmartEnum>
{
public static readonly ExampleSmartEnum Foo = new ExampleSmartEnum(nameof(Foo), 1);
public static readonly ExampleSmartEnum Bar = new ExampleSmartEnum(nameof(Bar), 2);
private ExampleSmartEnum(string name, int value) : base(name, value) { }
}
public class ExampleModel
{
[Required]
[SmartEnumName(typeof(ExampleSmartEnum)]
public string MyExample { get; set; } // must be "Foo" or "Bar"
[SmartEnumName(typeof(ExampleSmartEnum), allowCaseInsensitiveMatch: true)]
public string CaseInsensitiveExample { get; set; } // "Foo", "foo", etc. allowed; null also allowed here
}