Skip to content

Smart Enums Customization

Pawel Gerr edited this page Feb 9, 2026 · 1 revision

Smart Enums are highly customizable thanks to Roslyn Source Generators.

Key member generation

The key member is generated by the source generator. Use KeyMemberName, KeyMemberAccessModifier and KeyMemberKind to change the generation of the key member.

Example: Have the source generator generate a field private readonly string _name; instead of property public string Key { get; } (Default).

[SmartEnum<string>(KeyMemberName = "_name",
                   KeyMemberAccessModifier = AccessModifier.Private,
                   KeyMemberKind = MemberKind.Field)]
public partial class ProductType
{
   public static readonly ProductType Electronics = new("Electronics");
   public static readonly ProductType Clothing = new("Clothing");
}

// --- Generated code ---

partial class ProductType
{
  ...

  private readonly string _name;

  ...

Validation of the constructor arguments

Although the constructor is implemented by the source generator, the arguments can still be validated in the partial method ValidateConstructorArguments. Please note that the key must never be null.

[SmartEnum<string>]
public partial class ProductType
{
   public static readonly ProductType Electronics = new("Electronics");
   public static readonly ProductType Clothing = new("Clothing");

   static partial void ValidateConstructorArguments(ref string key)
   {
      if (String.IsNullOrWhiteSpace(key))
         throw new Exception("Key cannot be empty.");

      key = key.Trim();
   }
}

Additional fields and properties are passed to the method as well (see DisplayName below):

[SmartEnum<string>]
public partial class ProductType
{
   public static readonly ProductType Electronics = new("Electronics", "Display name for electronics");
   public static readonly ProductType Clothing = new("Clothing", "Display name for clothing");

   public string DisplayName { get; }

   static partial void ValidateConstructorArguments(ref string key, ref string displayName)
   {
      // validate
   }
}

Custom equality comparer

By default, the source generator uses the default implementation of Equals and GetHashCode, except for string. If the key member is a string, then the source generator uses StringComparer.OrdinalIgnoreCase. Additionally, the analyzer will warn you if you don't provide an equality comparer for a string-based Smart Enum.

The reason strings are not using the default implementation is because there are very few use cases where the comparison must be performed case-sensitive. Most often, case-sensitive string comparisons are bugs because developers have forgotten to pass an appropriate (case-insensitive) comparer.

Use KeyMemberEqualityComparerAttribute<TComparerAccessor, TMember> to define an equality comparer for comparison of key members and for computation of the hash code. Use one of the predefined ComparerAccessors or implement a new one (see below).

The example below changes the comparer from OrdinalIgnoreCase to Ordinal.

[SmartEnum<string>]
[KeyMemberEqualityComparer<ComparerAccessors.StringOrdinal, string>]
public partial class ProductType
{
   public static readonly ProductType Electronics = new("Electronics");
   public static readonly ProductType Clothing = new("Clothing");
}

Implement the interface IEqualityComparerAccessor<T> to create a new custom accessor. The accessor has 1 static property that returns an instance of IEqualityComparer<T>. The generic type T is the type of the member to compare.

public interface IEqualityComparerAccessor<in T>
{
   static abstract IEqualityComparer<T> EqualityComparer { get; }
}

Implementation of an accessor for members of type string.

public class StringOrdinal : IEqualityComparerAccessor<string>
{
  public static IEqualityComparer<string> EqualityComparer => StringComparer.Ordinal;
}

Predefined accessors in static class ComparerAccessors:

// Predefined:
ComparerAccessors.StringOrdinal
ComparerAccessors.StringOrdinalIgnoreCase
ComparerAccessors.CurrentCulture
ComparerAccessors.CurrentCultureIgnoreCase
ComparerAccessors.InvariantCulture
ComparerAccessors.InvariantCultureIgnoreCase
ComparerAccessors.Default<T>; // e.g. ComparerAccessors.Default<string> or ComparerAccessors.Default<int>

Custom Comparer

Use KeyMemberComparerAttribute<TComparerAccessor, TMember> to specify a comparer. Use one of the predefined ComparerAccessors or implement a new one (see below).

Please note that this section is about implementation of IComparable<T> and IComparer<T>. Don't confuse the IComparer<T> with IEqualityComparer<T> which is being used for equality comparison and the computation of the hash code.

[SmartEnum<string>]
[KeyMemberComparer<ComparerAccessors.StringOrdinal, string>]
public partial class ProductType
{
   public static readonly ProductType Electronics = new("Electronics");
   public static readonly ProductType Clothing = new("Clothing");
}

Implement the interface IComparerAccessor<T> to create a new custom accessor. The accessor has 1 static property that returns an instance of IComparer<T>. The generic type T is the type of the member to compare.

public interface IComparerAccessor<in T>
{
   static abstract IComparer<T> Comparer { get; }
}

Implementation of an accessor for members of type string.

public class StringOrdinal : IComparerAccessor<string>
{
    public static IComparer<string> Comparer => StringComparer.Ordinal;
}

Predefined accessors in static class ComparerAccessors:

// Predefined:
ComparerAccessors.StringOrdinal
ComparerAccessors.StringOrdinalIgnoreCase
ComparerAccessors.CurrentCulture
ComparerAccessors.CurrentCultureIgnoreCase
ComparerAccessors.InvariantCulture
ComparerAccessors.InvariantCultureIgnoreCase
ComparerAccessors.Default<T>; // e.g. ComparerAccessors.Default<string> or ComparerAccessors.Default<int>

Implementation of IComparable/IComparable<T>

Use SmartEnumAttribute<T> to set SkipIComparable to true to disable the implementation of IComparable and IComparable<T>.

[SmartEnum<int>(SkipIComparable = true)]
public partial class Priority
{

Implementation of comparison operators

Use SmartEnumAttribute<T> to set ComparisonOperators to control the generation of comparison operators (<, <=, >, >=).

Disabling comparison operators

Set ComparisonOperators to OperatorsGeneration.None to disable the implementation of comparison operators entirely:

[SmartEnum<int>(ComparisonOperators = OperatorsGeneration.None)]
public partial class Priority
{

Generating key type overloads

Set ComparisonOperators to OperatorsGeneration.DefaultWithKeyTypeOverloads to generate additional operators that compare a Smart Enum directly with its key type. The Default mode generates operators comparing two Smart Enum instances (e.g. Priority > Priority), while DefaultWithKeyTypeOverloads additionally generates overloads for comparing a Smart Enum with a value of the key type (e.g. Priority > int and int > Priority):

[SmartEnum<int>(ComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads)]
public partial class Priority
{
   public static readonly Priority Low = new(1);
   public static readonly Priority Medium = new(2);
   public static readonly Priority High = new(3);
}

Usage:

// Default: compare two Smart Enum instances
bool result = Priority.High > Priority.Low;   // true

// DefaultWithKeyTypeOverloads: compare directly with key type
bool isAbove = Priority.High > 1;             // true  (compares Key 3 > 1)
bool isBelow = 1 < Priority.Medium;           // true  (compares 1 < Key 2)

Conversion Operators

The source generator creates implicit and explicit conversion operators between the Smart Enum and its key type. Use ConversionToKeyMemberType and ConversionFromKeyMemberType to customize this behavior:

  • ConversionToKeyMemberType: Controls the conversion operator from Smart Enum to key type. Default: ConversionOperatorsGeneration.Implicit.
  • ConversionFromKeyMemberType: Controls the conversion operator from key type to Smart Enum. Default: ConversionOperatorsGeneration.Explicit.
[SmartEnum<string>(
    ConversionToKeyMemberType = ConversionOperatorsGeneration.Explicit,
    ConversionFromKeyMemberType = ConversionOperatorsGeneration.None)]
public partial class ProductType
{

Available values: ConversionOperatorsGeneration.Implicit, ConversionOperatorsGeneration.Explicit, ConversionOperatorsGeneration.None.

Implementation of IParsable<T> and ISpanParsable<T>

Smart Enums automatically implement parsing interfaces based on their key type:

IParsable: Implemented when the key type implements IParsable<TKey> or is string. Provides Parse and TryParse methods for string-based parsing.

ISpanParsable: Implemented when the key type implements ISpanParsable<TKey>. Provides zero-allocation parsing using ReadOnlySpan<char>:

[SmartEnum<int>]
public partial class Priority
{
    public static readonly Priority Low = new(1);
    public static readonly Priority Medium = new(2);
    public static readonly Priority High = new(3);
}

// ISpanParsable<T> implementation
// Zero-allocation parsing for high-performance scenarios
ReadOnlySpan<char> span = "1".AsSpan();
bool success = Priority.TryParse(span, null, out Priority? priority);  // priority == Low

// Works with numeric types (int, long, decimal, etc.)
[SmartEnum<decimal>]
public partial class PriceCategory
{
    public static readonly PriceCategory Budget = new(9.99m);
    public static readonly PriceCategory Premium = new(99.99m);
}

ReadOnlySpan<char> priceSpan = "9.99".AsSpan();
PriceCategory? category = PriceCategory.Parse(priceSpan, null);  // Budget

// Works with DateTime, Guid, and other ISpanParsable types
[SmartEnum<DateTime>]
public partial class SpecialDate
{
    public static readonly SpecialDate NewYear = new(new DateTime(2024, 1, 1));
}

[SmartEnum<Guid>]
public partial class EntityType
{
    public static readonly EntityType Customer = new(Guid.Parse("11111111-1111-1111-1111-111111111111"));
}

Disabling parsing interfaces: Use SkipIParsable = true to disable both IParsable<T> and ISpanParsable<T> implementations:

[SmartEnum<int>(SkipIParsable = true)]
public partial class Priority
{

To disable only ISpanParsable<T> while keeping IParsable<T>, use SkipISpanParsable = true:

[SmartEnum<int>(SkipISpanParsable = true)]
public partial class Priority
{

Note: Setting SkipIParsable = true implicitly disables both IParsable<T> and ISpanParsable<T>.

Supported key types for ISpanParsable: All built-in .NET types that implement ISpanParsable<T>, including:

  • Numeric types: int, long, short, byte, sbyte, uint, ulong, ushort, float, double, decimal
  • Date/Time types: DateTime, DateTimeOffset, TimeSpan, DateOnly, TimeOnly
  • Other types: Guid, Version, IPAddress, and more

Implementation of IFormattable

Use SmartEnumAttribute<T> to set SkipIFormattable to true to disable the implementation of IFormattable.

[SmartEnum<int>(SkipIFormattable = true)]
public partial class Priority
{

Implementation of ToString

Use SmartEnumAttribute<T> to set SkipToString to true to disable the implementation of the method ToString().

[SmartEnum<int>(SkipToString = true)]
public partial class Priority
{

Conversion from/to non-key type

With ObjectFactoryAttribute<T> you can implement additional methods to convert an item from/to type T. This conversion can be one-way (T -> Smart Enum) or two-way (T <-> Smart Enum). Conversion from a string allows ASP.NET Model Binding to bind Smart Enums with any key-member type.

By applying [ObjectFactory<string>], the source generator adds the interface IObjectFactory<MyEnum, string>, requiring you to implement a Validate method for one-way conversion:

[SmartEnum<int>]
[ObjectFactory<string>]
public partial class MyEnum
{
   public static readonly MyEnum Item1 = new(1);

   public static ValidationError? Validate(string? value, IFormatProvider? provider, out MyEnum? item)
   {
      switch (value)
      {
         case "=1=":
            item = Item1;
            return null;
         default:
            item = null;
            return new ValidationError($"Unknown value '{value}'");
      }
   }
}

Note: HasCorrespondingConstructor = true is not allowed for Smart Enums (TTRESG060).

For full details on two-way conversion (UseForSerialization), framework integration properties (UseForModelBinding, UseWithEntityFramework), multiple object factories, and zero-allocation JSON with ReadOnlySpan<char>, see the Object Factories page.

Hide fields and properties from Source Generator and Analyzer

Use this feature with caution!

A Smart Enum must be immutable. Hiding a member from the Generator and Analyzer means that there is no validation of this member anymore.

Use IgnoreMemberAttribute to hide a member.

[SmartEnum<string>]
public partial class ProductType
{
   public static readonly ProductType Electronics = new("Electronics");
   public static readonly ProductType Clothing = new("Clothing");

   // With IgnoreMemberAttribute the Analyzer doesn't emit a compiler error that the member is not read-only.
   [IgnoreMember]
   private string _someValue;
}

Switch/Map State Parameter Name

By default, the state parameter in generated Switch/Map methods is named state. Use SwitchMapStateParameterName to customize this:

[SmartEnum<string>(SwitchMapStateParameterName = "context")]
public partial class ProductType
{

Setting Dependencies

Several attribute settings have cascading effects on other settings. Understanding these dependencies is important for correct configuration:

Setting Cascading Effect
SkipIParsable = true Forces SkipISpanParsable = true (ISpanParsable<T> inherits from IParsable<T>)
EqualityComparisonOperators = None Forces ComparisonOperators to None in generated code (comparison operators require equality operators)
ComparisonOperators set higher than EqualityComparisonOperators EqualityComparisonOperators is coerced upward to match ComparisonOperators
DisableSpanBasedJsonConversion Only applies to string-keyed Smart Enums; has no effect on non-string key types

Clone this wiki locally