-
Notifications
You must be signed in to change notification settings - Fork 5
Smart Enums Customization
Smart Enums are highly customizable thanks to Roslyn Source Generators.
- Key Member Generation
- Constructor Argument Validation
- Custom Equality Comparer
- Custom Comparer
- Implementation of
IComparable/IComparable<T> - Implementation of comparison operators
- Conversion Operators
- Implementation of
IParsable<T>andISpanParsable<T> - Implementation of
IFormattable - Implementation of ToString
- Conversion from/to non-key type
- Hide fields and properties from Source Generator and Analyzer
- Switch/Map State Parameter Name
- Setting Dependencies
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;
...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
}
}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>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>andIComparer<T>. Don't confuse theIComparer<T>withIEqualityComparer<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>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
{Use SmartEnumAttribute<T> to set ComparisonOperators to control the generation of comparison operators (<, <=, >, >=).
Set ComparisonOperators to OperatorsGeneration.None to disable the implementation of comparison operators entirely:
[SmartEnum<int>(ComparisonOperators = OperatorsGeneration.None)]
public partial class Priority
{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)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.
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 = trueimplicitly disables bothIParsable<T>andISpanParsable<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
Use SmartEnumAttribute<T> to set SkipIFormattable to true to disable the implementation of IFormattable.
[SmartEnum<int>(SkipIFormattable = true)]
public partial class Priority
{Use SmartEnumAttribute<T> to set SkipToString to true to disable the implementation of the method ToString().
[SmartEnum<int>(SkipToString = true)]
public partial class Priority
{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.
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;
}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
{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 |
- Home
- Smart Enums
- Value Objects
- Discriminated Unions
- Object Factories
- Analyzer Diagnostics
- Source Generator Configuration
- Convenience methods and classes
- Migrations
- Version 7
- Version 8