3
\$\begingroup\$

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());

        }
\$\endgroup\$
2
\$\begingroup\$

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.

| improve this answer | |
\$\endgroup\$
  • \$\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 Jul 31 at 13:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.