Following This post : https://stackoverflow.com/questions/630803/associating-enums-with-strings-in-c-sharp/56482413?noredirect=1#comment107635743_56482413
I wanted to go further as it didn't quite fully met my needs for a Enum like Class that would act as string I ended-up with a solution that allows me to do the following:
string test1 = TestEnum.Analyze; //test1 == "ANALYZE"
string test1bis = (string)TestEnum.Analyze; //test1bis == "ANALYZE"
TestEnum test2 = "ANALYZE"; //test2 == {ANALYZE}
TestEnum test3 = "ANYTHING"; //test3 == null
As seen below in the unitTests all these work fine with this:
public class TestEnum : EnumType<TestEnum>
{
public static TestEnum Analyze { get { return new EnumType<TestEnum>("ANALYZE"); } }
public static TestEnum Test { get { return new EnumType<TestEnum>("TEST"); } }
public static implicit operator TestEnum(string s) => (EnumType<TestEnum>) s;
public static implicit operator string(TestEnum e) => e.Value;
}
I can't decide if this solution is elegant or incredibly stupid, It seems to me probably unnecessary complex and I might be messing a much easier solution in any case it could help someone so I'm putting this here.
//for newtonsoft serialization
[JsonConverter(typeof(EnumTypeConverter))]
public class EnumType<T> where T : EnumType<T> , new()
{
public EnumType(string value= null)
{
Value = value;
}
//for servicestack serialization
static EnumType()
{
JsConfig<EnumType<T>>.DeSerializeFn = str =>
{
return (T)str ;
};
JsConfig<EnumType<T>>.SerializeFn = type =>
{
return type.Value;
};
JsConfig<T>.DeSerializeFn = str =>
{
return (T)str;
};
JsConfig<T>.SerializeFn = type =>
{
return type.Value;
};
}
protected string Value { get; set; }
public static T Parse(string s)
{
return (T)s;
}
public override string ToString()
{
return Value;
}
public static EnumType<T> ParseJson(string json)
{
return (T)json;
}
public static implicit operator EnumType<T>(string s)
{
if (All.Any(dt => dt.Value == s))
{
return new T { Value = s };
}
else
{
var ai = new Microsoft.ApplicationInsights.TelemetryClient(Connector.tconfiguration);
ai.TrackException(new Exception($"Value {s} is not acceptable value for {MethodBase.GetCurrentMethod().DeclaringType}, Acceptables values are {All.Select(item => item.Value).Aggregate((x, y) => $"{x},{y}")}"));
return null;
}
}
public static implicit operator string(EnumType<T> dt)
{
return dt?.Value;
}
public static implicit operator EnumType<T>(T dt)
{
if (dt == null) return null;
return new EnumType<T>(dt.Value);
}
public static implicit operator T(EnumType<T> dt)
{
if (dt == null) return null;
return new T { Value = dt.Value };
}
public static bool operator ==(EnumType<T> ct1, EnumType<T> ct2)
{
return (string)ct1 == (string)ct2;
}
public static bool operator !=(EnumType<T> ct1, EnumType<T> ct2)
{
return !(ct1 == ct2);
}
public override bool Equals(object obj)
{
try
{
if(obj.GetType() == typeof(string))
{
return Value == (string)obj;
}
return Value == obj as T;
}
catch(Exception ex)
{
return false;
}
}
public override int GetHashCode()
{
return (!string.IsNullOrWhiteSpace(Value) ? Value.GetHashCode() : 0);
}
public static IEnumerable<T> All
=> typeof(T).GetProperties()
.Where(p => p.PropertyType == typeof(T))
.Select(x => (T)x.GetValue(null, null));
//for serialisation
protected EnumType(SerializationInfo info,StreamingContext context)
{
Value = (string)info.GetValue("Value", typeof(string));
}
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Value",Value);
}
}
Here are the unit tests:
[TestFixture]
public class UnitTestEnum
{
Connector cnx { get;set; }
private class Test
{
public TestEnum PropertyTest { get; set; }
public string PropertyString { get; set; }
}
[SetUp]
public void SetUp()
{
typeof(EnumType<>)
.Assembly
.GetTypes()
.Where(x => x.BaseType?.IsGenericType == true && x.BaseType.GetGenericTypeDefinition() == typeof(EnumType<>))
.Each(x =>
System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(x.BaseType.TypeHandle)
);
cnx = new Connector();
}
[TearDown]
public void Clear()
{
cnx.Dispose();
}
[Test]
public void EqualsString()
{
Assert.AreEqual(TestEnum.Analyze, TestEnum.Analyze);
Assert.AreEqual(TestEnum.Analyze,"ANALYZE");
Assert.IsTrue("ANALYZE" == TestEnum.Analyze);
Assert.IsTrue("ANALYZE".Equals(TestEnum.Analyze));
}
[Test]
public void Casts()
{
string test1 = TestEnum.Analyze;
string test1bis = (string)TestEnum.Analyze;
TestEnum test2 = "ANALYZE";
TestEnum test3 = "NAWAK";
Assert.AreEqual("ANALYZE", test1);
Assert.AreEqual("ANALYZE", test1bis);
Assert.IsTrue(test2 == TestEnum.Analyze);
Assert.IsTrue(test2.Equals(TestEnum.Analyze));
Assert.AreEqual(test3, null);
}
[Test]
public void Deserializations()
{
new List<TestEnum>
{
(TestEnum)ServiceStack.Text.JsonSerializer.DeserializeFromString("\"ANALYZE\"", typeof(TestEnum)),
"\"ANALYZE\"".FromJson<TestEnum>(),
(TestEnum)Newtonsoft.Json.JsonConvert.DeserializeObject("\"ANALYZE\"", typeof(TestEnum)),
Newtonsoft.Json.JsonConvert.DeserializeObject<TestEnum>("\"ANALYZE\"")
}.Each(testEnum => Assert.AreEqual(testEnum, TestEnum.Analyze));
new List<Test>
{
"{\"PropertyTest\":\"ANALYZE\",\"PropertyString\":\"ANALYZE\"}".FromJson<Test>(),
(Test)ServiceStack.Text.JsonSerializer.DeserializeFromString("{\"PropertyTest\":\"ANALYZE\",\"PropertyString\":\"ANALYZE\"}", typeof(Test)),
Newtonsoft.Json.JsonConvert.DeserializeObject<Test>("{\"PropertyTest\":\"ANALYZE\",\"PropertyString\":\"ANALYZE\"}"),
(Test)Newtonsoft.Json.JsonConvert.DeserializeObject("{\"PropertyTest\":\"ANALYZE\",\"PropertyString\":\"ANALYZE\"}",typeof(Test))
}.Each(test =>
{
Assert.AreEqual(test.PropertyTest, TestEnum.Analyze);
Assert.AreEqual(test.PropertyString, "ANALYZE");
});
}
[Test]
public void Serialisations()
{
Assert.AreEqual("{\"PropertyTest\":\"ANALYZE\",\"PropertyString\":\"ANALYZE\"}", new Test { PropertyTest = TestEnum.Analyze, PropertyString = TestEnum.Analyze }.ToJson());
Assert.AreEqual("{\"PropertyTest\":\"ANALYZE\",\"PropertyString\":\"ANALYZE\"}", Newtonsoft.Json.JsonConvert.SerializeObject(new Test { PropertyTest = TestEnum.Analyze, PropertyString = TestEnum.Analyze }));
Assert.AreEqual("\"ANALYZE\"", TestEnum.Analyze.ToJson());
Assert.AreEqual("\"ANALYZE\"", Newtonsoft.Json.JsonConvert.SerializeObject(TestEnum.Analyze));
}
[Test]
public void TestEnums()
{
Assert.AreEqual(TestEnum.All.Count(), 2);
Assert.Contains(TestEnum.Analyze,TestEnum.All.ToList());
Assert.Contains(TestEnum.Test,TestEnum.All.ToList());
}
1 Answer 1
I won't comment on the Json
stuff, as it doesn't seem to be the main subject to the question.
I'm not sure, I quite understand where to use this, so if you have a concrete real use case feel free to update the question with it.
You can't for instance use it in a switch like:
TestEnum te = TestEnum.Analyze;
switch (te)
{
case TestEnum.Analyze:
Console.WriteLine("Analyze");
break;
case TestEnum.Test:
Console.WriteLine("Test");
break;
default:
break;
}
because the-enum properties aren't constant.
You can do:
TestEnum te = TestEnum.Analyze;
switch (te)
{
case TestEnum t when t == TestEnum.Analyze:
Console.WriteLine("Analyze");
break;
case TestEnum t when t == TestEnum.Test:
Console.WriteLine("Test");
break;
default:
break;
}
but IMO that may be tedious in the long run.
The overall impression is, that your cast system is messy. Trying to debug it to find a way for a string or an enum value is confusing, and you (I) easily lose track of the path. IMO you rely too heavily on casting to and from string
.
Be aware, that this:
public static TestEnum Analyze { get { return new EnumType<TestEnum>("ANALYZE"); } }
is different than this:
public static TestEnum Analyze { get; } = new EnumType<TestEnum>("ANALYZE");
Where the first returns a new instance of Analyze
for every call, the latter only instantiates one the first time it is called - just like a static (readonly) property or field should behave. Related to that, I think that each "enum"-property should be a singleton, and only instantiated once. You instantiate various instances of each property in the cast methods. I don't like that. Further I think, I would make the constructor of TestEnum
private to prevent unauthorized instantiation of invalid enum values. If you make the constructor private, you can't specify T
with the constraint new()
. But that is OK, if you make each enum
value a singleton - only instantiated where defined.
As for the initialization of the static enum-properties it goes for All
:
public static IEnumerable<T> All => typeof(T).GetProperties() .Where(p => p.PropertyType == typeof(T)) .Select(x => (T)x.GetValue(null, null));
where
public static IReadOnlyList<T> All { get; } =
typeof(T).GetProperties()
.Where(p => p.PropertyType == typeof(T))
.Select(x => (T)x.GetValue(null, null))
.ToList();
will be much more efficient as reflection is only activated once. Notice that I've changed IEnumerable<T>
to IReadOnlyList<T>
in order to cache the query. Repeatedly using reflection may be a bottleneck - especially if you have many enum-properties.
You can and should narrow down the properties searched for by using BindingFlags
in All
:
typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Static)...
public override bool Equals(object obj) { try { if(obj.GetType() == typeof(string)) { return Value == (string)obj; } return Value == obj as T; } catch(Exception ex) { return false; } }
This seems overly complicated and a catch block here is unnecessary:
public override bool Equals(object obj)
{
if (obj is T other) return ReferenceEquals(this, other) || Value == other.Value;
return obj is string value && Value == value;
}
Here:
public static implicit operator EnumType<T>(string s) { if (All.Any(dt => dt.Value == s)) { return new T { Value = s }; }
I think, I would do:
public static implicit operator EnumType<T>(string s)
{
if (All.FirstOrDefault(dt => dt.Value == s) is T e)
{
return e;
}
else
In this way, the already created enum is reused.
-
\$\begingroup\$ Hi, thanks a lot for your input I used it and wanted to do a proper reply but never got the time... It does seem alambicated but there are many case where I have a string (for instance coming from an api) and want to compare it with an enum. I finally found some place where they used the same kind of approach: github.com/octokit/octokit.net/pull/1595/commits/… I might switch to something more like them. (not sure about the serialisation though). \$\endgroup\$Lomithrani– Lomithrani2020年07月31日 13:07:54 +00:00Commented Jul 31, 2020 at 13:07