We commonly use C# Enumeration types to represent a set of named constants within our application. Often the values for such properties comes from the user via an API request.
Binding directly to enums unfortunately does not give us the desired API experience, namely:
- A user can provide the integral value for the enum when, according to our API documentation, we only allow the string representation
- A user can provide non-defined integer values and they will be treated as valid
- Model binding can throw an exception if trying to bind a non-supported enum name which can be hard to handle gracefully
Our approach is to always bind the user input to string values and make use of Fluent Validation to validate the provided value. Since our JSON APIs are formatted in snake_case
we have also started to transition our enum names too, meaning a value such as gateway_only
should be bound to MyEnum.GatewayOnly
.
The following utility class provides the methods to both validate and parse enums taking into account the EnumMemberAttribute to support binding to alternative member names. Whilst this is natively supported by JSON.NET, binding directly to enum properties would suffer from the issues outlined above.
public class EnumUtils
{
/// <summary>
/// Tries to parse a string into an enum honoring EnumMemberAttribute if present
/// </summary>
public static bool TryParseWithMemberName<TEnum>(string value, out TEnum result) where TEnum : struct
{
result = default;
if (string.IsNullOrEmpty(value))
return false;
Type enumType = typeof(TEnum);
foreach (string name in Enum.GetNames(typeof(TEnum)))
{
if (name.Equals(value, StringComparison.OrdinalIgnoreCase))
{
result = Enum.Parse<TEnum>(name);
return true;
}
EnumMemberAttribute memberAttribute
= enumType.GetField(name).GetCustomAttribute(typeof(EnumMemberAttribute)) as EnumMemberAttribute;
if (memberAttribute is null)
continue;
if (memberAttribute.Value.Equals(value, StringComparison.OrdinalIgnoreCase))
{
result = Enum.Parse<TEnum>(name);
return true;
}
}
return false;
}
/// <summary>
/// Gets the enum value from a string honoring the EnumMemberAttribute if present
/// </summary>
public static TEnum? GetEnumValueOrDefault<TEnum>(string value) where TEnum : struct
{
if (TryParseWithMemberName(value, out TEnum result))
return result;
return default;
}
}
Note that the above code could be optimised by maintaining a cache of the members with [EnumMember]
attribute.
To validate the incoming string value we added the following rule to our FV validator:
RuleFor(x => x.Mode)
.Must(x => EnumUtils.TryParseWithMemberName<ProcessingMode>(x, out _))
.WithErrorCode(ErrorCodes.ProcessorModeInvalid)
.When(x => !string.IsNullOrEmpty(x.Mode));
To bind we use the GetEnumValueOrDefault
method above:
var processingMode = EnumUtils.GetEnumValueOrDefault<ProcessingMode>(request.Mode)