1

Following This post : Associating enums with strings in C#

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

        }
Bernard Vander Beken
  • 4,848
  • 5
  • 54
  • 76
Lomithrani
  • 2,033
  • 3
  • 18
  • 24
  • Enum.GetName or Enum.Parse already tie knows between string and enum, why do you go back to using string constants? – Bacon May 27 '20 at 12:46
  • Interesting question, but kind of off-topic on this board. It would better fit to [Code Review Stack Exchange](https://codereview.stackexchange.com/) – nilsK May 27 '20 at 15:01
  • @Lomithrani Thanks! I will give this a try, as I am on a crusade against primitive obsession, and I consider this (the way enums and strings go together in vanilla c#) just to be another symptom of it :-) – Xan-Kun Clark-Davis Oct 04 '22 at 01:40

1 Answers1

4

Elegant code is typically described as simple, clean, terse with clear intent and optimally performant, I'm not seeing any of these traits here which is especially convoluted as instead of using simple C# Enum's as intended they're wrapped in a generic base class with implicit casts and custom serialization handling for different JSON Serialization libraries (that's unlikely to work in other serialization libraries/formats) and with with all the additional artificial complexity added it's not even clear what the benefit or purpose of all the boilerplate is?

Code that's drowned in so much boilerplate increases the maintenance burden and inhibits refactoring as no one else is going to know what the desired intent or purpose of the code is meant to be & why simpler naive solutions weren't adopted instead.

If you just want to have a different Wire Serialization format to the Symbol name used in code (which IMO should have a good reason for being different), you can just use the [EnumMember] Serialization attribute:

[DataContract]
public enum TestEnum 
{
    [EnumMember(Value = "ANALYZE")]
    Analyze,
    [EnumMember(Value = "TEST")]
    Test,
}

Otherwise I'd dispense with using Enums and go back to using string constants, which are simpler, faster, memory efficient and works in all Serialization libraries without custom serialization hacks:

public static class MyConstants
{
    public const string Analyze = "ANALYZE";
    public const string Test = "TEST";
}
mythz
  • 141,670
  • 29
  • 246
  • 390
  • The first solution is the one I'm using currently but I'm not satisfied with it. The idea behind this code is to replace legacy code using strings for test etc without breaking / rewriting everything. I wanted for something to really be "interchangable" I want a string with constraints. I need to illustrate more with usesCases but for instance the first solution requires a lot of Parse / GetName or extensions method. While this method only requires the asignment operator both ways – Lomithrani May 27 '20 at 13:50
  • But then again I agree about your comment on Elegant code and its the reason why I post here and didn't push into prod – Lomithrani May 27 '20 at 14:29