Parsing for Encapsulated Enumeration Classes
Easier conversion of raw values to encapsulated enumeration objects.
Last year I wrote a short article called Serialization for Encapsulated Enumeration Classes in which I described a technique I use to manipulate code/description pairs in ways that are similar to enum
but without the many limitations of a real enum
. Towards the end of the article I showed this bit of ugly code to demonstrate how to parse a raw code value:
1
2
string EliteDataValue = QueryEliteFlag(customerId); // returns Y or N
var EliteStatus = (EnumYesNo)EnumYesNo.Undefined.ReadJson(EliteDataValue);
I was never satisfied with that. ReadJson
returns an object
so the cast is unavoidable, and it’s an instance method, so we need to choose one of the instances declared in the derived class (in this case I chose Undefined
), and of course, we aren’t actually reading JSON data. The example was unsatisfying, but more importantly, I strongly disliked leaving these oddities in production code. When I wrote the article, my real-world usage involved a lot of JSON as input data, but I later began using the encapsulation classes with raw code values. When you add additional real-world factors like casting DataRow
lookups, the result is downright embarrassing:
1
var EliteStatus = (EnumYesNo)EnumYesNo.Undefined.ReadJson((string)row["EliteStatus"]);
Clearly a dedicated parser method was needed.
Code for this article has been added to the code for the earlier article, still available from GitHub at MV10/Serializing.Encapsulated.Enumerators.
Better, Not Great
My plan was to implement a static Parse
method in the Enumeration<T>
base class, which would be used like this:
1
2
string code = "Y";
var parsed = EnumYesNo.Parse(code);
Unfortunately that isn’t possible. This is as close as I could get:
1
2
string code = "Y";
var parsed = EnumYesNo.Parse<EnumYesNo>(code);
I can live with “a little weird” versus “nearly incomprehensible”.
What Doesn’t Work
I had thought I could use the DeclaringType
reflection property, but static method calls are optimized in ways that prevent that from working. When a derived class calls a static method declared in the base class, the compiler output is a call made directly against the base class. There is no information at runtime which can be reflected in order to determine the derived class from that base class method. Consider this:
1
2
3
4
5
6
7
8
9
10
11
public class MyBaseClass
{
public static string WhoAmI()
=> MethodBase.GetCurrentMethod().DeclaringType.Name;
}
public class MyDerivedClass : MyBaseClass
{ }
MyBaseClass.WhoAmI();
MyDerivedClass.WhoAmI();
Both calls to WhoAmI()
return MyBaseClass
because the compiled code looks like this:
1
2
3
IL_0001: call MyBaseClass.WhoAmI
IL_0006: nop
IL_0007: call MyBaseClass.WhoAmI
Reading DeclaringType
from the static Parse
method I wanted to write only yielded Enumeration<T>
rather than EnumYesNo
or whatever other derived type that I actually needed. The solution was to pass the desired type to the Parse
method.
The Compromise
Now the extra type delcaration makes sense:
1
2
3
4
5
// as written in the source code:
parsed = EnumYesNo.Parse<EnumYesNo>(code);
// as compiled, calling the base class:
parsed = Enumeration<string>.Parse<EnumYesNo>(code);
The desired type is passed as a generic type parameter which allows us to declare the same return type, which at least avoids the need to cast from an object
. The method itself then uses the GetAll<>
method already in the base class to locate a match. I also added a parser for the Description
property:
1
2
3
4
5
6
7
8
9
public static E Parse<E>(T code) where E : Enumeration<T>, new()
=> GetAll<E>()
.Where(n => n.Code.Equals(code))
.FirstOrDefault();
public static E ParseDescription<E>(string description) where E : Enumeration<T>, new()
=> GetAll<E>()
.Where(n => n.Description.Equals(description, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault();
The Description
property is declared as a string, so this allows us to specify a case-insensitive comparison. Unfortunately, the Code
property is of generic type T
so the Equals
method has no case-sensitivity override. As written above, Parse
requires an exact match: the sample EnumYesNo
implementation can parse Y
but not y
.
The solution is to conditionally cast the code
argument and Code
properties when T
is a string:
1
2
3
4
5
6
7
public static E Parse<E>(T code) where E : Enumeration<T>, new()
=> GetAll<E>()
.Where(n =>
(typeof(T) == typeof(string)) ?
(code as string).Equals(n.Code as string, StringComparison.OrdinalIgnoreCase)
: n.Code.Equals(code))
.FirstOrDefault();
Incidentally, direct casting such as (string)code
will not compile. You will get an error message stating that T
can’t be converted to a string, because the compiler can’t guarantee the conversion will always work. However, using the as
keyword allows for the possibility of a null
result, which can be short-circuited, and so the compiler accepts this. At runtime, of course, the type-checking condition guarantees that the conversion will work, and we get the desired results:
1
2
3
4
5
6
7
8
9
10
11
12
Raw value:
y
Parsed enum:
Code: Y
Description: Yes
Raw value:
no
Parsed enum:
Code: N
Description: No
Conclusion
Adding a simple value-parser required jumping through a lot more hoops than I initially expected, but the cleaner code was well worth the effort.
I hope you find it useful!
Comments