Home

Awesome

NameOperating SystemStatusHistory
GitHub ActionsUbuntu, Mac & WindowsBuildGitHub Actions Build History

GitHub release GitHub license GitHub issues GitHub issues-closed NuGet Badge

<p align="center"> <img src="./assets/social-preview.png" alt="The Intellenum logo"> </p>

Sparkline

Give a Star!

:star: If you like or are using this project please give it a star. Thanks! :star:

Intellenum: intelligence, for your enums!

<!-- TOC --> <!-- TOC -->

Overview

Intellenum is an open source C# project that provides a fast and efficient way to deal with enums. It uses source generation that generates backing code for extremely fast, and allocation-free, lookups, with the FromName and FromValue methods (and the equivalent Try... methods).

[Intellenum]
public partial class CustomerType
{
    public static readonly CustomerType Standard, Gold;
}

Just like normal enums, they default to integers as the underlying type, and the values are zero based.

As well as speed, it also has code analyzers for safety; safety from defaulting, e.g.:

img.png

or:

img_1.png

or:

img_2.png

Intellenum provides speed benefits over standard enums for when you need to see if an enum has a member of a particular name or value. Benchmarks are provided below, but here is a snippet showing the performance gains for using IsDefined:

MethodMeanErrorStdDevMedianGen0Allocated
StandardEnums107.4646 ns1.1617 ns1.0867 ns107.3232 ns0.005796 B
Intellenums0.0022 ns0.0031 ns0.0027 ns0.0010 ns--

Installation

Add the NuGet package to your project:

Install-Package Intellenum

Usage

To get started, add a using for the Intellenum namespace and declare an enumeration like this:

[Intellenum]
public partial class CustomerType
{
    public static readonly CustomerType Standard, Gold;
}

You can also specify different values:

[Intellenum]
public partial class CustomerType
{
    public static readonly CustomerType Standard = new(1000), Gold = new(2000);
}

Note that you don't need to repeat the member name as it is inferred from the field name at compile time. You can also supply a different name, e.g.:

public static readonly CustomerType Standard = new("STD", 1);

By default, the underlying type is int, but you can specify a different type, e.g. [Intellenum<short>].

As well as explicitly declaring members like above, there are a couple of other ways. You can use a static constructor that calls Member.

[Intellenum]
public partial class CustomerType
{
    static CustomerType()
    {
        Member("Standard", 0);
        Member("Gold", 0);
    }
}

Member is actually executed at runtime, it is used at compile time to generate field declarations.

Another way is via attributes. You can use the Member attribute for single values, or the Members for multiple attributes. Here's an example of the Member attribute:

[Intellenum]
[Member("Standard", 1)]
[Member("Gold", 2)]
public partial class CustomerType { }

Here's an example of the Members attribute. Not that this can only be applied to int based enums:

[Intellenum]
[Members("Standard, Gold, Diamond, Platinum")]
public partial class CustomerType { }

Using this attribute will generate the items in the order specified and the values will start at zero and increase. The Members attribute can be applied with other Member attributes, but only one Members attribute can be specified per type.

You can also use a mixture of all of the above methods!

[Intellenum]
[Members("Standard, Gold")]
[Member("Diamond", 2)]
public partial class CustomerType 
{
    static CustomerType()
    {
        Member("Platinum", 3);
    }
    public static readonly CustomerType Royalty = new(4);
}

... you can then treat the type just like an enum:

if(type == CustomerType.Standard) Reject();
if(type == CustomerType.Gold) Accept();

Switch

C# doesn't treat Intellenums as constants like it does with native enums. This makes it difficult to use in scenarios were a constant expression is needed, like in switch expressions. To get around this, const fields are generated which can be used in switch expressions:

string shortCode = vendorType.Value switch
{
    VendorType.StandardValue => "STD",
    VendorType.PreferredValue => "PRFRD",
    VendorType.BlockedValue => "BLCKED",
    _ => throw new InvalidOperationException("Unknown vendor type")
};

Configuration

Each Intellenum can have its own optional configuration. Configuration includes:

If any of those above are not specified, then global configuration is used. You can define global config like this:

[assembly: IntellenumDefaults(underlyingType: typeof(int), conversions: Conversions.Default)]

Those values are all optional and default to:

Underlying types

Supports underlying types such as byte, sbyte, short, ushort, int, uint, long, ulong, char, string, and bool.

Also supports other types such as Guid, DateTime, DateTimeOffset, TimeSpan, DateOnly, TimeOnly etc.

You can also specify a custom type, e.g. MyCustomType. There are some restrictions for this custom type:

Hoisting

If the underlying type implements IComparable, then the generated enum code will also implement IComparable. The code that is generated will delegate to the underlying type's implementation. This means that you can use the > and < operators on the enum type. e.g.

[Intellenum<Planet>]
public partial class PlanetEnum
{
    public static readonly PlanetEnum
        Jupiter = new(new Planet("Brown", 273_400)),
        Mars = new(new Planet("Red", 13_240)),
        Venus = new(new Planet("White", 23_622));
}

public record class Planet(string Colour, int CircumferenceInMiles) : IComparable<Planet>
{
    public int CompareTo(Planet other) => CircumferenceInMiles.CompareTo(other.CircumferenceInMiles);
}

Console.WriteLine(PlanetEnum.Mars < PlanetEnum.Jupiter); // true

Console.WriteLine(string.Join(", ", PlanetEnum.List().OrderDescending())); // Jupiter, Venus, Mars

Additionally, if the underlying type contains a static method named TryParse, then a TryParse method will be generated for the enum itself. This TryParse method is useful if you want to find an enum by an alternative representation of its value. The generated TryParse first calls the static TryParse on the underlying type, and then does a lookup with TryFromValue. The code below demonstrates this. The enum has an underlying type of Planet, which has a TryParse method that parses a string in the format 'Colour-Circumference'. Because the underlying type has a TryParsemethod, the generated enum also has aTryParsemethod which delegates to the underlying type'sTryParse` method:

[Intellenum<Planet>]
public partial class PlanetEnum
{
    public static readonly PlanetEnum
        Jupiter = new(new Planet("Brown", 273_400)),
        Mars = new(new Planet("Red", 13_240)),
        Venus = new(new Planet("White", 23_622));
}

public record class Planet(string Colour, int CircumferenceInMiles)  : IComparable<Planet>
{
    public int CompareTo(Planet other) => CircumferenceInMiles.CompareTo(other.CircumferenceInMiles);

    public static bool TryParse(string input, out Planet result)
    {
        string pattern = "^(?<colour>[a-zA-Z]+)-(?<circumference>\\d+)$";

        Match match = Regex.Match(input, pattern);

        if (!match.Success)
        {
            result = default!;
            return false;
        }

        string colour = match.Groups["colour"].Value;
        string circumference = match.Groups["circumference"].Value;

        result = new Planet(colour, Convert.ToInt32(circumference));

        return true;
    }
 }

// the following tests pass    
{
    // this is defined
    bool r = PlanetEnum.TryParse("Brown-273400", out var p);
    r.Should().BeTrue();
    p.Should().Be(PlanetEnum.Jupiter);
}

{
    // this is not defined
    bool r = PlanetEnum.TryParse("Blue-24901", out _);
    r.Should().BeFalse();
}    
}

FromName

Gets the member of the enum with the specified name. If the name is not found, then a IntellenumMatchFailedException exception is thrown.

var ct = CustomerType.FromName("Gold");
Console.WriteLine(ct.ToString()); // Gold

TryFromName

Tries to get the instance of that name. Returns true if the name is found, otherwise false. Sets the output value if found.

bool b = CustomerType.TryFromName("Gold", out var ct);
Console.WriteLine(b); // True
Console.WriteLine(ct.ToString()); // Gold

FromValue

Tries to get the instance of that value. If not found, then a IntellenumMatchFailedException exception is thrown.

var ct = CustomerType.FromValue(2);
Console.WriteLine(ct.ToString()); // Gold 

TryFromValue

Tries to get the value. Returns true or false. Sets the output value if found.

bool b = CustomerType.TryFromValue(2, out var ct);
Console.WriteLine(b); // True
Console.WriteLine(ct.ToString()); // Gold

List

Returns an IEnumerable<T> of all the members of the enum.

Deconstructing

Deconstructs an enum into it's name and value. For example:

var (name, value) = CustomerType.Gold;
Console.WriteLine(name); // Gold
Console.WriteLine(value); // 2

ToString

The ToString method returns the name of the enum member. For example:

Console.WriteLine(CustomerType.Gold); // Gold

Serialization

Intellenum supports serialization to and from JSON using System.Text.Json and Newtonsoft.Json. It also supports storing and retrieving from Dapper, EFCore and Linq2Db.

Comparison with other libraries

The bulk of Intellenum is based on the work done for Vogen which is a source generator for value objects. One of the features of Vogen is the ability to specify 'instances'. These instances are very similar to the members of an enum, but they are not enums. There were a few requests to use the same source generation and analyzers used for Vogen but to generate enums instead. This is what Intellenum is.

There are a few other libraries for dealing with enums. Some, for example, SmartEnum, declare a base class containing functionality. Others, e.g. EnumGenerators, use attributes on standard enums to generate source code.

Intellenum is a mixture of both. It uses an attribute to specify an 'enum' and then source-generates the functionality.

FAQ

How fast is it? ⚡

Very fast! Here's some comparisons of various libraries (and the default enum in C#)

MethodMeanErrorStdDevMedianGen0Allocated
StandardEnums107.4646 ns1.1617 ns1.0867 ns107.3232 ns0.005796 B
EnumGenerators0.0113 ns0.0108 ns0.0101 ns0.0095 ns--
SmartEnums13.1542 ns0.0863 ns0.0720 ns13.1441 ns--
Intellenums0.0022 ns0.0031 ns0.0027 ns0.0010 ns--
MethodMeanErrorStdDevGen0Allocated
StandardEnums11.9803 ns0.0961 ns0.0852 ns0.001424 B
EnumGenerators1.5292 ns0.0230 ns0.0215 ns--
SmartEnums0.8921 ns0.0109 ns0.0096 ns--
Intellenums0.8934 ns0.0193 ns0.0180 ns--
MethodMeanErrorStdDevAllocatedExample
StandardEnums123.937 ns0.5615 ns0.4977 ns-Enum.TryParse<CustomerType>("Standard", out _)
EnumGenerators9.067 ns0.0523 ns0.0489 ns-CustomerTypeExtensions.TryParse("Standard", out _)
SmartEnums30.719 ns0.4043 ns0.3782 ns-CustomerType.TryFromName( "Gold", out _)
Intellenums11.460 ns0.2545 ns0.2380 ns-CustomerType.TryFromName("Standard", out _)
MethodMeanErrorStdDevAllocated
StandardEnums0.0092 ns0.0082 ns0.0077 ns-
SmartEnums0.3246 ns0.0082 ns0.0069 ns-
Intellenums0.3198 ns0.0103 ns0.0096 ns-

Intellenum constantly monitors its performance. The benchrmarks are here , and are generated with a combination of excellent tools, namely Benchmark.NET and the github-action-benchmark workflow plugin.

What does ToString return?

It returns the name of the member. There is also a TypeConverter; when this is asked to convert a member to a `string', it returns the value of the member as a string.

What can the TypeConverters convert to and from?

They can convert an underlying type back to a matching enum.

Can it serialize/deserialize?

Yes, it can. There's various ways to do this, including:

Right now, Intellenum serializes using the Value property just like native enums.

I use an Intellenum as a key in a Dictionary - can I serialize that dictionary?

Yes, at least if you use System.Text.Json.

A look at the generated code

For compile-time constant (and decimal) values, a switch expression is generated for IsDefined:

public static bool IsDefined(System.Int32 value)
{
    return value switch { 
      1 => true,
      2 => true,
      3 => true,
      4 => true,
      _ => false
    };
}

For FromValue (and TryFromValue), a switch statement is used:

public static bool TryFromValue(System.Int32 value, out CustomerType member)
{
  switch (value) 
  {
      case 1:
          member = CustomerType.Unspecified; 
          return true;
      case 2:
          member = CustomerType.Normal; 
          return true;
      case 3:
          member = CustomerType.Gold; 
          return true;
      case 4:
          member = CustomerType.Diamond; 
          return true;
      default:
          member = default;
          return false;
  }
}

As an aside, we experimented with using switch expressions for this too, but they turned out to be slower than normal switch statements (about twice as slow) due to the need for having a tuple in the expression.

The generated code look like this:

public static bool TryFromValue(int value, out CustomerType member)
{
    Func<(CustomerType, bool)> f = value switch
    {
        1 => () => (CustomerType.Unspecified, true), 
        2 => () => (CustomerType.Normal, true), 
        3 => () => (CustomerType.Gold, true), 
        4 => () => (CustomerType.Diamond, true), 
        _ => () => (default, false)
    };
    
    var r = f();
    member = r.Item1;
    return r.Item2;
}

If you can think of a way of making this faster, please let us know!

A 'compile time constant' is one of the following:

For underlying types that are not one of these, then a lookup table is used. Why is a lookup table used if the underlying is not one of the above? It is because the left hand side of a switch expression must be a constant expression. A constant expression is used in the 'constant pattern' of the switch expression. The 'constant pattern' is described as:

A constant pattern is a pattern that matches a constant value. The constant value is specified by a constant expression. A constant expression is an expression that can be fully evaluated at compile time.n a constant pattern, you can use any constant expression, such as:

So, types like Guid and DateTime are not allowed on the left hand side of a switch expression (but Span<>'s are). The alternative in this case is to use a dictionary to map between names and values.

NOTE: Intellenum is in beta at the moment; I've tested it out and I think it works. The main functionality is present and the API probably won't change significantly from now on. Although it's a fairly new library, it borrows a lot of code and features from Vogen which has been in use for a while now by many projects and has lots of downloads, which should provide some confidence. Please feel free to try it and provide feedback.