3
\$\begingroup\$

How can StringEnumeration be changed in the code below into a generic class, so that ConsensusState wouldn't need the JsonConverter attribute to enable instances of the class to be used as if they were serializable enums with associated string values?

I'm using StringEnumeration to allow checking instances of ConsensusState agains string objects in conditional statements or to print them directly to the console as string values, while also be able to use them as properties in Serializable classes. The serialization is made using System.Text.Json.Serialization.

Here are the classes:

/// <summary>Consensus state returned by the server.</summary>
[Serializable]
[JsonConverter(typeof(StringEnumerationConverter<ConsensusState>))]
public class ConsensusState : StringEnumeration
{
    /// <summary>Connecting.</summary>
    public static readonly ConsensusState Connecting = new ConsensusState("connecting");
    /// <summary>Syncing blocks.</summary>
    public static readonly ConsensusState Syncing = new ConsensusState("syncing");
    /// <summary>Consensus established.</summary>
    public static readonly ConsensusState Established = new ConsensusState("established");

    private ConsensusState(string value) : base(value) { }
}

/// <summary>JsonConverter used in string enumeration serialization.</summary>
public class StringEnumerationConverter<T> : JsonConverter<T>
{
    /// <summary>Read the string value.</summary>
    /// <param name="reader">Reader to access the encoded JSON text.</param>
    /// <param name="typeToConvert">Type of the object to deserialize.</param>
    /// <param name="options">Options for the deserialization.</param>
    /// <returns>Underlying string enumeration type.</returns>
    public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return (T)Activator.CreateInstance(typeof(T), BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { reader.GetString() }, null, null);
    }

    /// <summary>Write the string value.</summary>
    /// <param name="writer">Writer to encode the JSON text.</param>
    /// <param name="value">Object to serialize.</param>
    /// <param name="options">Options for the serialization.</param>
    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
    {
        writer.WriteStringValue((string)typeof(T).GetProperty("Value").GetValue(value, null));
    }
}

/// <summary>Abstract base class used in string enumerations.</summary>
public abstract class StringEnumeration
{
    /// <summary>Associated value.</summary>
    public string Value { get; set; }

    /// <summary>Initializes the enumeration from a string.</summary>
    /// <param name="value">The associated value.</param>
    public StringEnumeration(string value) { Value = value; }

    /// <summary>Implicit conversion to string.</summary>
    /// <param name="obj">And StringEnumeration object.</param>
    /// <returns>string object.</returns>
    public static implicit operator string(StringEnumeration obj)
    {
        return obj.Value;
    }

    /// <summary>Get the string associated value.</summary>
    /// <returns>string object.</returns>
    public override string ToString()
    {
        return Value;
    }

    /// <summary>Test whether a StringEnumeration is equal to another object.</summary>
    /// <param name="a">StringEnumeration object.</param>
    /// <param name="b">Another object.</param>
    /// <returns>true if the two objects are equal.</returns>
    public static bool operator ==(StringEnumeration a, object b)
    {
        var other = b as StringEnumeration;
        if (other is null)
        {
            return false;
        }
        return a.Value == other.Value;
    }

    /// <summary>Test whether a StringEnumeration is different to other object.</summary>
    /// <param name="a">StringEnumeration object.</param>
    /// <param name="b">Another object.</param>
    /// <returns>true if the two objects are different.</returns>
    public static bool operator !=(StringEnumeration a, object b)
    {
        var other = b as StringEnumeration;
        if (other is null)
        {
            return true;
        }
        return a.Value != other.Value;
    }

    /// <summary>Test whether a StringEnumeration is equal to another object.</summary>
    /// <param name="obj">Another object.</param>
    /// <returns>true if the objects are equal.</returns>
    public override bool Equals(object obj)
    {
        var other = obj as StringEnumeration;
        if (other is null)
        {
            return false;
        }
        return Value == other.Value;
    }

    /// <summary>Get the hash code of the associated value.</summary>
    /// <returns>An integer value representing the hash of the associated value.</returns>
    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }
}
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The implementation of StringEnumeration below allows a subclass to behave as if it was a string enum, and also has some quality-of-life improvements like being able to specify the string associated value for each field using the attribute JsonStringValue().

/// <summary>Attribute to assign the string value of fields in StringEnumeration.</summary>
[AttributeUsage(AttributeTargets.Field)]
public class JsonStringValue : Attribute
{
    /// <summary>String value.</summary>
    public string Value;

    /// <summary>Initializes the Attribute instance to a given value.</summary>
    public JsonStringValue(string value)
    {
        Value = value;
    }
}

/// <summary>JsonConverter used in string enumeration serialization.</summary>
public class StringEnumerationConverter : JsonConverter<StringEnumeration>
{
    /// <summary>Whether a type is a subclass of <c>StringEnumeration</c>.</summary>
    /// <param name="typeToConvert">Type to check.</param>
    /// <returns>True if is a subclass.</returns>
    public override bool CanConvert(Type typeToConvert)
    {
        return typeof(StringEnumeration).IsAssignableFrom(typeToConvert);
    }

    /// <summary>Read the string value.</summary>
    /// <param name="reader">Reader to access the encoded JSON text.</param>
    /// <param name="typeToConvert">Type of the object to deserialize.</param>
    /// <param name="options">Options for the deserialization.</param>
    /// <returns>Underlying string enumeration type.</returns>
    public override StringEnumeration Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var obj = (StringEnumeration)FormatterServices.GetUninitializedObject(typeToConvert);
        obj.Value = reader.GetString();
        return obj;
    }

    /// <summary>Write the string value.</summary>
    /// <param name="writer">Writer to encode the JSON text.</param>
    /// <param name="value">Object to serialize.</param>
    /// <param name="options">Options for the serialization.</param>
    public override void Write(Utf8JsonWriter writer, StringEnumeration value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(value);
    }
}

/// <summary>Abstract base class used in string enumerations.</summary>
public abstract class StringEnumeration
{
    /// <summary>Associated value.</summary>
    public string Value { get; set; }

    /// <summary>Initializes all static fields in subclasses.</summary>
    static StringEnumeration()
    {
        var types = typeof(StringEnumeration).Assembly.GetTypes();
        foreach (var type in types)
        {
            if (type.BaseType == typeof(StringEnumeration))
            {
                var fields = type.GetFields(BindingFlags.Public | BindingFlags.Static);
                foreach (var field in fields)
                {
                    var attribute = field.GetCustomAttribute<JsonStringValue>();
                    var name = attribute is null ? field.Name : attribute.Value;
                    var value = (StringEnumeration)FormatterServices.GetUninitializedObject(type);
                    value.Value = name;
                    field.SetValue(null, value);
                }
            }
        }
    }

    /// <summary>Implicit conversion to string.</summary>
    /// <param name="obj">And StringEnumeration object.</param>
    /// <returns>string object.</returns>
    public static implicit operator string(StringEnumeration obj)
    {
        if (obj is null)
        {
            return null;
        }
        return obj.ToString();
    }

    /// <summary>Get the string associated value.</summary>
    /// <returns>string object.</returns>
    public override string ToString()
    {
        return Value;
    }

    /// <summary>Test whether a StringEnumeration is equal to another object.</summary>
    /// <param name="a">StringEnumeration object.</param>
    /// <param name="b">Another object.</param>
    /// <returns>true if the two objects are equal.</returns>
    public static bool operator ==(StringEnumeration a, object b)
    {
        if (a is null)
        {
            return b is null;
        }
        return !(b is null) && a.Value == b.ToString();
    }

    /// <summary>Test whether a StringEnumeration is different to other object.</summary>
    /// <param name="a">StringEnumeration object.</param>
    /// <param name="b">Another object.</param>
    /// <returns>true if the two objects are different.</returns>
    public static bool operator !=(StringEnumeration a, object b)
    {
        return !(a == b);
    }

    /// <summary>Test whether a StringEnumeration is equal to another object.</summary>
    /// <param name="obj">Another object.</param>
    /// <returns>true if the objects are equal.</returns>
    public override bool Equals(object obj)
    {
        return this == obj;
    }

    /// <summary>Get the hash code of the associated value.</summary>
    /// <returns>An integer value representing the hash of the associated value.</returns>
    public override int GetHashCode()
    {
        return Value.GetHashCode();
    }
}

This is how to use it:

/// <summary>Consensus state returned by the server.</summary>
[Serializable]
[JsonConverter(typeof(StringEnumerationConverter))]
public class ConsensusState : StringEnumeration
{
    /// <summary>Connecting.</summary>
    [JsonStringValue("connecting")]
    public static ConsensusState Connecting;
    /// <summary>Syncing blocks.</summary>
    [JsonStringValue("syncing")]
    public static ConsensusState Syncing;
    /// <summary>Consensus established.</summary>
    [JsonStringValue("established")]
    public static ConsensusState Established;
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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