By Trish Polay and Mike Sigworth
In C#, enums are backed by an integer.
Most tools will serialize or save your enums using that integer value.
But what if you need to save it as a string?
Strings are far more readable in database query results and when parsing JSON output from an API.
A client recently presented us with this challenge.
Add a property to an existing class which is constrained to a limited set of values.
I know what you’re thinking. That’s it? That’s not interesting! Just use an enum
. Not so fast… You haven’t heard the rest of the requirements.
When persisted to the database or JSON-serialized, the value must be a string.
Unfortunately, this throws a bit of a wrench into our plans to use an enum. Our persistence layer uses Dapper and like most other ORMs it persists Enums using the backing integer value. Same goes for JSON serialization. So while an enum does provide us a convenient mechanism for constraining our type’s values, it leaves us constantly having to convert (and validate) to and from the string representation.
That said, our requirements can still be solved using an enum with a few caveats. But there is a more elegant (read: more complicated) solution which may be more bulletproof in some cases. Below we shall present both options.
Option 1: Using an Enum
Step 1. Create the Enum
Create an enum called ProductType
.
public enum ProductType
{
FRUIT = 0,
VEGETABLE = 1
}
Add a ProductType
property to some class.
public class Product
{
public ProductType ProductType { get; set; }
// Existing members
...
}
Step 2. JSON Serialization
By default, when you serialize a Product
to JSON it includes the ProductType
property as an integer. To make it use a string instead we must implement a JsonConverter
to inform the serializer what to do when it encounters a ProductType
. We associate the new converter to ProductType
by applying a JsonConverterAttribute
to it.
Note: There are a few caveats to this implementation. Be sure to read the code comments below.
public class ProductTypeConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// Tell the serializer to use the name of the enum value rather than its integer value. e.g. "FRUIT" or "VEGETABLE"
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer ser)
{
var value = reader.Value?.ToString();
if (value == null)
{
return null;
}
try
{
// This verifies that the string value being deserialized is a valid named constant of the
// ProductType enum.
// We must prevent out of range integer values from being deserialized. Surprisingly, C#
// doesn't do a range check of it's own on enums and it's possible to cast any integer value
// to an enum, despite the defined values in the enum's definition.
if (!Enum.IsDefined(typeof(ProductType), value) &&
!Enum.GetValues(typeof(ProductType)).Cast<int>().Any(i => value == i.ToString()))
{
throw new JsonSerializationException($"Failed to parse {nameof(ProductType)} value: {value}");
}
return Enum.Parse(typeof(ProductType), value);
}
catch (ArgumentException e)
{
throw new JsonSerializationException($"Failed to parse {nameof(ProductType)} value: {value}", e);
}
}
public override bool CanConvert(Type objectType) => true;
}
[JsonConverter(typeof(ProductTypeConverter))]
public enum ProductType
{
FRUIT = 0,
VEGETABLE = 1
}
Step 3. The Database
Now that our ProductType
can be serialized and deserialized to/from a string the only thing left to do is support the database persistence. For that we’ll use Dapper.
By default Dapper will store an enum in the database as an integer. The simplest way around this is to just call ToString()
whenever saving to the database. Reading from the database is much less of an issue as Dapper can parse either an integer or string to the correct enum value. If you are familiar with Dapper you may think that you should instead use an ITypeHandler
for this, but sadly Dapper does not seem to check for registered ITypeHandler
implementations when it encounters an enum. This means that every time you persist the enum, you must always remember to use ToString()
.
Connection.Execute(@"
INSERT INTO [dbo].[Product] ([Id], [Name], [ProductType])
VALUES (@id, @name, @productType)", new {
id = product.Id,
name = product.Name,
productType = product.ProductType.ToString() // store the enum as a string
}, Transaction);
Step 4. Rejoice
That’s it. You can now use your enum and it will be treated as a string from the API to the database!
This approach has the advantage of being extremely simple to implement. Enums just work in C#. However, it is unfortunate that you must always remember to ToString
it. If you ever forget you will wind up with mismatched data in the database, having a column with both integers and strings in it. It would be more ideal if an ITypeHandler
could work.
If your custom type needs to go beyond that of a plain-old-enum, e.g., your values have spaces in them, your type has additional properties, or perhaps you want to include some helper methods in your type for better encapsulation. In this case, you no longer want this simpleton’s implementation. Let me introduce you to the Cadillac version!
Slaps roof of Java-Style-Enum. “This baby can fit so much whitespace and helper methods in it!”
Option 2: Using a Java-Style-Enum
As mentioned above, this is the Cadillac version of an enum. Essentially it is just a class with static properties defining the values of the enum. But it is extremely type-safe, and with a little effort we can make it work just as seamlessly as a regular enum, with the added benefit of using an ITypeHandler
for better Dapper support.
Step 1. Start Simple
Create a new class for our Enum called ProductType
with a private field to hold the value, a private constructor for initialization, and some static properties defining our known values.
public class ProductType
{
private readonly string _value;
// Make the constructor private so that it can only be instantiated internally
private ProductType(string value)
{
_value = value;
}
// Oh look, spaces!
public static ProductType Fruit = new ProductType("I AM FRUIT");
public static ProductType Vegetable = new ProductType("VEGGIES FOR LYFE");
}
Step 2. String Conversion
To enable conversion to and from a string we’ll override the ToString()
method as well as add an implicit operator
to convert from a string.
public override string ToString() => _value;
public static implicit operator ProductType(string value) => new ProductType(value);
However, now we have a problem. Any string value can be implicitly converted to a ProductType
, for example
var p = (ProductType)"Not a valid product type"; // Invalid!
To deal with this we’ll add some validation logic to our constructor. This could be as simple as a few equality checks on our known values, but that isn’t very future proof. Also, we’ve been told by the business to expect new ProductType
s to be added in the future. So when that happens we’d like to limit the number of code changes required to support the new values. So instead we’ll use some reflection (and some LINQ) to get a collection of known values from the actual static properties on the class. Then we can perform a lookup in the constructor of the provided value.
One final caveat to mention is that our static properties should skip this validation logic, or else we’ll wind up with a stack overflow. To account for this, we’ll add an optional validate
parameter to our constructor, making it false by default so our static property definitions don’t have to provide it. The only place where we need to validate is from within the implicit operator
.
private static IEnumerable<string> KnownValues => typeof(ProductType)
.GetFields(BindingFlags.Public | BindingFlags.Static)
.Where(f => f.FieldType == typeof(ProductType))
.Select(f => f.GetValue(null).ToString());
private ProductType(string value, bool validate = false)
{
if (validate && !KnownValues.Contains(value))
{
new FormatException($"Unexpected {nameof(ProductType)}: '{value}'");
}
_value = value;
}
public static ProductType Fruit = new ProductType("I AM FRUIT");
public static ProductType Vegetable = new ProductType("VEGGIES FOR LYFE");
// Notice we're now passing `true` into the constructor call of the implicit operator
public static implicit operator ProductType(string value) => new ProductType(value, true);
Step 3. JSON Serialization
At this point, JSON serialization won’t work since our type has no instance properties on it. The default serializer just returns {}
. To fix this we have to tell Json.NET how to serialize our class. To do this we’ll implement a JsonConverter
then decorate the ProductType
with a JsonConverterAttribute
to associate the two. Much like we did for the basic enum approach in Option 1.
public class ProductTypeConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer ser)
{
var value = reader.Value?.ToString();
if (value == null)
{
return null;
}
try
{
// Use our handy implicit string conversion, which will also validate the value being deserialized!
return (ProductType)value;
}
catch (FormatException e)
{
throw new JsonSerializationException(
$"Failed to parse {nameof(ProductType)} value: {reader.Value}", e);
}
}
public override bool CanConvert(Type objectType) => true;
}
[JsonConverter(typeof(ProductTypeConverter))]
public class ProductType
{
...
}
Step 4. Equality
When working with ProductType
s in code we’ll undoubtedly need to compare for equality with other ProductType
s. To enable this we must implement IEquatable<ProductType>
(along with a host of other supporting methods and operators). Implementing IEquatable
properly is beyond the scope of this blog post, however if you have ReSharper installed it does a much better job of this than we ever could. To leverage this little bit of ReSharper awesomeness add the code to implement IEquatable
:
public class ProductType : IEquatable<ProductType>
Then use the ReSharper refactoring option Generate equality members. Make sure to tick the Overload equality operators checkbox, then click Finish.
This will add code to your class similar to the following:
public bool Equals(ProductType other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return string.Equals(_value, other._value, StringComparison.InvariantCulture);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != typeof(ProductType )) return false;
return Equals((ProductType ) obj);
}
public override int GetHashCode()
{
return (_value != null ? StringComparer.InvariantCulture.GetHashCode(_value) : 0);
}
public static bool operator ==(ProductType left, ProductType right)
{
return Equals(left, right);
}
public static bool operator !=(ProductType left, ProductType right)
{
return !Equals(left, right);
}
Now you can do important equality comparisons like making sure that Fruit is not Vegetable!
ProductType.Fruit == ProductType.Fruit; //=> True
ProductType.Fruit == ProductType.Fruit.ToString(); //=> True
ProductType.Fruit != ProductType.Vegetable; //=> True
Step 5. The Database
Now that our ProductType
can be serialized and deserialized to/from a string, as well as compared to other ProductType
s, the only thing left to do is to support database persistence. Again, for that we’ll use Dapper.
For Dapper to handle parameters of type ProductType
, it requires a custom ITypeHandler
to be registered with the mapper. This has the added benefit over plain-old-enums that you don’t have to worry about calling ToString
all the time. Dapper will always use this mapper when it encounters our custom type.
public class ProductTypeHandler : SqlMapper.ITypeHandler
{
public object Parse(Type destinationType, object value)
{
// Here again we use our handy implicit string conversion
return destinationType == typeof(ProductType) ? (ProductType)value.ToString() : null;
}
public void SetValue(IDbDataParameter parameter, object value)
{
parameter.DbType = DbType.String;
parameter.Value = ((ProductType)value).ToString();
}
}
Now, the ProductTypeHandler
needs to be registered with Dapper. Make the following call somewhere in your startup/configuration code (for example in ConfigureServices()
)
SqlMapper.AddTypeHandler(typeof(ProductType), new ProductTypeHandler());
Step 6. Rejoice!
We’re done! You now have a custom enum implementation that behaves as a string from the API to the database!
I know we promised you helper methods, but please use your imagination. You can put whatever your heart desires in this Java-Style-Enum! It is far more flexible than a regular enum.
Final Thoughts
Well obviously the Java-Style-Enum is a lot of code. But in the event your requirements are complex enough to require it, it sure is a handy tool to have in your toolbelt. I must confess though, in the end we went with Option 1, the Plain-Old-Enum. The way our Dapper persistence layer is written, we don’t really need to worry about multiple places persisting this type and forgetting a ToString
. And our enum values don’t really need to differentiate from the monikers used in code. Sure they’re all caps, but there’s no spaces to worry about.
After all of this, I can’t help but wonder how to also implement this with C# 9 Record Types…. Perhaps one day. Stay tuned…
Originally published October 5, 2020. Information refreshed February 16, 2022.