Skip to content

Flexible Enums

At Majority we use a custom flexible enumeration system through two specialized base classes: FlexibleStringEnum<TEnum> and FlexibleIntegerEnum<TEnum>. These flexible enumerations address a limitation of the traditional C# enums by providing graceful handling of unknown values, which is useful in our distributed microservice architecture where not all services are updated and deployed simulatenously.

Core Architecture and Philosophy

Both flexible enum types follow a similar architectural pattern but differ in their underlying value types. Unlike traditional C# enums that throw exceptions when encountering unknown values during deserialization, flexible enums are designed to be resilient and adaptable. They maintain a collection of predefined enum members while supporting the creation of new instances for previously unknown values.

The key innovation lies in their ability to handle "forward compatibility" scenarios. When an API receives a JSON payload containing an enum value that doesn't exist in the current codebase, instead of failing with a deserialization exception, the flexible enum can create a new temporary instance with that unknown value. This behavior is particularly crucial in distributed systems where different services might be running different versions of the codebase.

Guideline

Using one of the flexible enums should be the default options when writing new code. Only for specific cases when you are sure the enum won't change and are internal only should the traditional enums be used (i.e. in a test).

FlexibleStringEnum: String-Based Enumeration

The FlexibleStringEnum<TEnum> uses string values as its underlying type, making it ideal for scenarios involving textual identifiers, codes, or names. The NumericCurrencyCode class serves as an excellent real-world example, implementing ISO 4217 currency codes:

public static readonly NumericCurrencyCode USD = new NumericCurrencyCode("840", "USD", 2);
public static readonly NumericCurrencyCode EUR = new NumericCurrencyCode("978", "EUR", 2);

Each currency code instance contains three pieces of information: the numeric code ("840"), the alphabetic code ("USD"), and the decimal places (2). The string-based nature allows for intuitive parsing and comparison operations. When deserializing JSON, if a currency code like "NEW" is encountered but doesn't exist in the predefined set, a new NumericCurrencyCode instance is automatically created rather than throwing an exception.

The TestStringEnum used in unit tests demonstrates the basic usage pattern:

public static TestStringEnum _01_RESERVED = new("01", "Reserved");
public static TestStringEnum _02_CUSTOM = new("02", "Custom");

String-based flexible enums excel in scenarios involving external APIs, configuration files, or any situation where enum values might be added by external systems without code updates.

FlexibleIntegerEnum: Integer-Based Enumeration

The FlexibleIntegerEnum<TEnum> uses integer values, making it suitable for scenarios requiring numeric identifiers or when performance considerations favor integer operations. The test implementation shows this pattern:

public static readonly TestIntEnum _01_RESERVED = new(1, "Reserved");
public static readonly TestIntEnum _02_CUSTOM = new(2, "Custom");

Integer-based enums are particularly useful for database scenarios where enum values are stored as integers, or when integrating with systems that use numeric codes for categorization. The integer backing provides efficient comparison operations and maintains compatibility with traditional integer-based enum patterns.

Advanced Features and Capabilities

Both enum types provide comprehensive parsing and comparison capabilities. They support parsing by both name and value, with case-sensitive and case-insensitive options. The In() method allows for elegant membership testing:

if (currencyCode.In(NumericCurrencyCode.USD, NumericCurrencyCode.EUR)) {
    // Handle USD or EUR specific logic
}

The flexible enums integrate seamlessly with JSON serialization through specialized converters that can serialize as name, value, or complete object representations. This flexibility allows the same enum to be serialized differently depending on the API contract requirements.

Extensibility and Customization

Child classes can override the Deserialize() method to implement custom behavior for unknown values. The NumericCurrencyCode returns an "Unknown" instance for invalid inputs, while TestStringEnum creates new instances dynamically. This extensibility allows each enum implementation to define its own strategy for handling unknown values based on business requirements. However, in most cases use the default!

Example Implementations

Good examples exists in many areas. A couple of examples from be-bank-platform:

JSON Serialization Options

Flexible enums support multiple serialization formats through specialized JSON converters:

  • SerializeAs.Name: Serializes as the enum name (e.g., "Active")
  • SerializeAs.Value: Serializes as the enum value (e.g., "active")
  • SerializeAs.Object: Serializes as complete object with both name and value

Example configuration:

[JsonConverter(typeof(FlexibleStringEnumJsonObjectConverter<StatusCode>), SerializeAs.Name, true)]
public StatusCode Status { get; set; }

Make sure to sync with the external party (e.g. Apps) which way is the proper way.