10-Jul-2022 - StringEnumConverter work in progress

CSharp · Console

/Program.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters;
using System.IO;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Runtime.CompilerServices;
using System.Diagnostics;
using System.Runtime.Serialization;

using System.Text.Json;
using System.Text.Json.Serialization;

using NUnit.Framework; // Throws NUnit.Framework.AssertionException

//https://stackoverflow.com/questions/59059989/system-text-json-how-do-i-specify-a-custom-name-for-an-enum-value/59061296#59061296
// 10-Jul-2022

#nullable enable

	public class JsonPropertyNameStringEnumConverter : GeneralJsonStringEnumConverter
	{
		public JsonPropertyNameStringEnumConverter() : base() { }
		public JsonPropertyNameStringEnumConverter(JsonNamingPolicy? namingPolicy = default, bool allowIntegerValues = true) : base(namingPolicy, allowIntegerValues) { }

		protected override bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName)
		{
			if (EnumExtensions.TryGetEnumAttribute<JsonPropertyNameAttribute>(enumType, name, out var attr) && attr.Name != null)
			{
				overrideName = attr.Name.AsMemory();
				return true;
			}
			return base.TryOverrideName(enumType, name, out overrideName);
		}
	}

	public class JsonEnumMemberStringEnumConverter : GeneralJsonStringEnumConverter
	{
		public JsonEnumMemberStringEnumConverter() : base() { }
		public JsonEnumMemberStringEnumConverter(JsonNamingPolicy? namingPolicy = default, bool allowIntegerValues = true) : base(namingPolicy, allowIntegerValues) { }

		protected override bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName)
		{
			if (EnumExtensions.TryGetEnumAttribute<System.Runtime.Serialization.EnumMemberAttribute>(enumType, name, out var attr) && attr.Value != null)
			{
				overrideName = attr.Value.AsMemory();
				return true;
			}
			return base.TryOverrideName(enumType, name, out overrideName);
		}
	}
	
	public class GeneralJsonStringEnumConverter : JsonConverterFactory
	{
		readonly JsonNamingPolicy? namingPolicy;
		readonly bool allowIntegerValues;
		
		public GeneralJsonStringEnumConverter() : this(null, true) { }
		
		public GeneralJsonStringEnumConverter(JsonNamingPolicy? namingPolicy = default, bool allowIntegerValues = true) => (this.namingPolicy, this.allowIntegerValues) = (namingPolicy, allowIntegerValues);
	
		public override bool CanConvert(Type typeToConvert) => typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
 
		public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
		{
			var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
			JsonConverter enumConverter;
			TryOverrideName tryOverrideName = (Type t, string n, out ReadOnlyMemory<char> o) => TryOverrideName(t, n, out o);
			var converterType = typeof(JsonEnumConverter<>).MakeGenericType(new [] {enumType});
			enumConverter = (JsonConverter)Activator.CreateInstance(converterType,
																	BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
																	binder: null,
																	args: new object[] { namingPolicy!, allowIntegerValues, tryOverrideName },
																	culture: null)!;
			if (enumType == typeToConvert)
				return enumConverter;
			else
			{
				var nullableConverter = (JsonConverter)Activator.CreateInstance(typeof(NullableConverterDecorator<>).MakeGenericType(new [] {enumType}), 
																				BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
																				binder: null,
																				args: new object[] { enumConverter },
																				culture: null)!;
				return nullableConverter;
			}
		}
		
		protected virtual bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName)
		{
			overrideName = default;
			return false;
		}
		
		class JsonEnumConverter<TEnum> : JsonConverter<TEnum> where TEnum: struct, Enum
		{
			protected static TypeCode EnumTypeCode { get; } = Type.GetTypeCode(typeof(TEnum));	

			private bool AllowNumbers { get; }
			EnumTypeData<TEnum> EnumTypeData { get; }

			public JsonEnumConverter(JsonNamingPolicy? namingPolicy, bool allowNumbers, TryOverrideName? tryOverrideName) 
			{
				this.AllowNumbers = allowNumbers;
				this.EnumTypeData = EnumTypeData<TEnum>.Create(namingPolicy, tryOverrideName);
			}

			public sealed override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options)
			{
				// Todo: consider caching a small number of JsonEncodedText values for the first N enums encountered, as is done in 
				// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs
				if (EnumTypeData.TryFormatAsString(value, out var name))
					writer.WriteStringValue(name.Span);
				else
				{
					if (!AllowNumbers)
						throw new JsonException();
					WriteEnumAsNumber(writer, value);
				}
			}

			public sealed override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
				reader.TokenType switch
				{
					JsonTokenType.String => EnumTypeData.TryLookup(reader.GetString().AsMemory(), out var value) ? value : throw new JsonException(),
					JsonTokenType.Number => AllowNumbers ? ReadNumberAsEnum(ref reader) : throw new JsonException(),
					_ => throw new JsonException(),
				};

			static void WriteEnumAsNumber(Utf8JsonWriter writer, TEnum value)
			{
				switch (EnumTypeCode)
				{
					case TypeCode.SByte:
						writer.WriteNumberValue(Unsafe.As<TEnum, SByte>(ref value));
						break;
					case TypeCode.Int16:
						writer.WriteNumberValue(Unsafe.As<TEnum, Int16>(ref value));
						break;
					case TypeCode.Int32:
						writer.WriteNumberValue(Unsafe.As<TEnum, Int32>(ref value));
						break;
					case TypeCode.Int64:
						writer.WriteNumberValue(Unsafe.As<TEnum, Int64>(ref value));
						break;
					case TypeCode.Byte:
						writer.WriteNumberValue(Unsafe.As<TEnum, Byte>(ref value));
						break;
					case TypeCode.UInt16:
						writer.WriteNumberValue(Unsafe.As<TEnum, UInt16>(ref value));
						break;
					case TypeCode.UInt32:
						writer.WriteNumberValue(Unsafe.As<TEnum, UInt32>(ref value));
						break;
					case TypeCode.UInt64:
						writer.WriteNumberValue(Unsafe.As<TEnum, UInt64>(ref value));
						break;
					default:
						throw new JsonException();
				}
			}

			static TEnum ReadNumberAsEnum(ref Utf8JsonReader reader)
			{
				switch (EnumTypeCode)
				{
					case TypeCode.SByte:
						{
							var i = reader.GetSByte();
							return Unsafe.As<SByte, TEnum>(ref i);
						};
					case TypeCode.Int16:
						{
							var i = reader.GetInt16();
							return Unsafe.As<Int16, TEnum>(ref i);
						};
					case TypeCode.Int32:
						{
							var i = reader.GetInt32();
							return Unsafe.As<Int32, TEnum>(ref i);
						};
					case TypeCode.Int64:
						{
							var i = reader.GetInt64();
							return Unsafe.As<Int64, TEnum>(ref i);
						};
					case TypeCode.Byte:
						{
							var i = reader.GetByte();
							return Unsafe.As<Byte, TEnum>(ref i);
						};
					case TypeCode.UInt16:
						{
							var i = reader.GetUInt16();
							return Unsafe.As<UInt16, TEnum>(ref i);
						};
					case TypeCode.UInt32:
						{
							var i = reader.GetUInt32();
							return Unsafe.As<UInt32, TEnum>(ref i);
						};
					case TypeCode.UInt64:
						{
							var i = reader.GetUInt64();
							return Unsafe.As<UInt64, TEnum>(ref i);
						};
					default:
						throw new JsonException();
				}
			}
		}
	}

	public sealed class NullableConverterDecorator<T> : JsonConverter<T?> where T : struct
	{
		// Read() and Write() are never called with null unless HandleNull is overwridden -- which it is not.
		readonly JsonConverter<T> innerConverter;
		public NullableConverterDecorator(JsonConverter<T> innerConverter) => this.innerConverter = innerConverter ?? throw new ArgumentNullException(nameof(innerConverter));
		public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => innerConverter.Read(ref reader, Nullable.GetUnderlyingType(typeToConvert)!, options);
		public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) => innerConverter.Write(writer, value!.Value, options);
		public override bool CanConvert(Type type) => base.CanConvert(type) && innerConverter.CanConvert(Nullable.GetUnderlyingType(type)!);
	}

	/********************************************************/
	// Begin general enum parsing code

	public delegate bool TryOverrideName(Type enumType, string name, out ReadOnlyMemory<char> overrideName);

	public class EnumTypeData<TEnum> where TEnum : struct, Enum
	{
		readonly record struct EnumData(ReadOnlyMemory<char> name, TEnum value, UInt64 UInt64Value);

		internal static TypeCode EnumTypeCode { get; } = Type.GetTypeCode(typeof(TEnum));	
		static Func<EnumData, UInt64, int> EntryComparer { get; } = (item, key) => item.UInt64Value.CompareTo(key);
		static bool Flagged { get; } = typeof(TEnum).IsDefined(typeof(FlagsAttribute), false);

		private EnumData [] EnumDataArray { get; }
		private ILookup<ReadOnlyMemory<char>, int> NameLookup { get; }
	
		private EnumTypeData(EnumData [] enumDataArray, ILookup<ReadOnlyMemory<char>, int> nameLookup) => 
			(this.EnumDataArray, this.NameLookup) = (enumDataArray ?? throw new ArgumentNullException(nameof(enumDataArray)), nameLookup ?? throw new ArgumentNullException(nameof(nameLookup)));
	
		public static EnumTypeData<TEnum> Create(JsonNamingPolicy? namingPolicy, TryOverrideName? tryOverrideName)
		{
			var enumData = GetEnumData(namingPolicy, tryOverrideName);
			return new EnumTypeData<TEnum>(enumData, GetLookupTable(enumData));
		}
		
		// Return data about the enum sorted by the binary values of the enumeration constants (that is, by their unsigned magnitude)
		static EnumData [] GetEnumData(JsonNamingPolicy? namingPolicy, TryOverrideName? tryOverrideName)
		{
			var names = Enum.GetNames<TEnum>();
			var values = Enum.GetValues<TEnum>();
			return names.Zip(values, (n, v) => 
				{ 
					if (tryOverrideName == null || !tryOverrideName(typeof(TEnum), n, out var jsonName))
						jsonName = (namingPolicy == null ? n.AsMemory() : namingPolicy.ConvertName(n).AsMemory());
					return new EnumData(jsonName, v, v.ToUInt64(EnumTypeCode));
				})
				.ToArray();
		}

		static int FindEnumDataIndex(EnumData [] enumData, TEnum value, out UInt64 UInt64value)
		{
			UInt64value = value.ToUInt64(EnumTypeCode);
			return enumData.BinarySearchFirst(UInt64value, EntryComparer);
		}

		static ILookup<ReadOnlyMemory<char>, int> GetLookupTable(EnumData [] namesAndValues) =>
			Enumerable.Range(0, namesAndValues.Length).ToLookup(i => namesAndValues[i].name, CharMemoryComparer.OrdinalIgnoreCase);
			
		public bool TryFormatAsString(TEnum value, out ReadOnlyMemory<char> name)
		{
			if (!Flagged)
				return TryFormatSimpleAsString(value, out name);
			else
				return TryFormatFlaggedAsString(value, out name);
		}

		bool TryFormatSimpleAsString(TEnum value, out ReadOnlyMemory<char> name)
		{
			var index = FindEnumDataIndex(EnumDataArray, value, out var _);
			if (index >= 0)
			{
				name = EnumDataArray[index].name;
				return true;
			}
			name = default;
			return false;
		}

		bool TryFormatFlaggedAsString(TEnum value, out ReadOnlyMemory<char> name)
		{
			var index = FindEnumDataIndex(EnumDataArray, value, out var UInt64Value);
			if (index >= 0)
			{
				// A single flag
				name = EnumDataArray[index].name;
				return true;
			}
			if (UInt64Value != 0)
			{
				StringBuilder? sb = null;
				for (int i = (~index) - 1; i >= 0; i--)
				{
					if ((UInt64Value & EnumDataArray[i].UInt64Value) ==  EnumDataArray[i].UInt64Value && EnumDataArray[i].UInt64Value != 0)
					{
						if (sb == null)
						{
							sb = new StringBuilder();
							sb.Append(EnumDataArray[i].name.Span);
						}
						else
						{
							sb.Insert(0, EnumExtensions.FlagSeparatorString);
							sb.Insert(0, EnumDataArray[i].name.Span);
						}
						UInt64Value -= EnumDataArray[i].UInt64Value;
					}
				}
				if (UInt64Value == 0 && sb != null)
				{
					name = sb.ToString().AsMemory();
					return true;
				}
			}
			name = default;
			return false;
		}

		public bool TryLookup(ReadOnlyMemory<char> name, out TEnum value)
		{
			if (!Flagged)
				return TryLookupSimple(name, out value);
			else
				return TryLookupFlagged(name, out value);
		}

		bool TryLookupSimple(ReadOnlyMemory<char> name, out TEnum value)
		{
			int i = 0;
			int firstMatch = -1;
			foreach (var index in NameLookup[name])
			{
				if (firstMatch == -1)
					firstMatch = index;
				else 
				{
					if (i == 1 && MemoryExtensions.Equals(EnumDataArray[firstMatch].name.Span, name.Span, StringComparison.Ordinal))
					{
						value = EnumDataArray[firstMatch].value;
						return true;
					}
					if (MemoryExtensions.Equals(EnumDataArray[index].name.Span, name.Span, StringComparison.Ordinal))
					{
						value = EnumDataArray[index].value;
						return true;
					}
				}
				i++;
			}
			value = (firstMatch == -1 ? default : EnumDataArray[firstMatch].value);
			return firstMatch != -1;
		}
		
		bool TryLookupFlagged(ReadOnlyMemory<char> name, out TEnum value)
		{
			UInt64 UInt64Value = 0;
			foreach (var slice in name.Split(EnumExtensions.FlagSeparatorChar, StringSplitOptions.TrimEntries))
			{
				if (TryLookupSimple(slice, out var thisValue))
					UInt64Value |= thisValue.ToUInt64();
				else
				{
					value = default;
					return false;
				}
			}
			value = UInt64Value.ToEnum<TEnum>(EnumTypeCode);
			return true;
		}
	}

	public static partial class EnumExtensions
	{
		public const char FlagSeparatorChar = ',';
		public const string FlagSeparatorString = ", ";
		
		public static bool TryGetEnumAttribute<TAttribute>(Type type, string name, [System.Diagnostics.CodeAnalysis.NotNullWhen(returnValue: true)] out TAttribute? attribute) where TAttribute : System.Attribute
		{
			var member = type.GetMember(name).SingleOrDefault();
			attribute = member?.GetCustomAttribute<TAttribute>(false);
			return attribute != null;
		}
		
		public static UInt64 ToUInt64<TEnum>(this TEnum value) where TEnum : struct, Enum => value.ToUInt64(EnumTypeData<TEnum>.EnumTypeCode);
		
		internal static UInt64 ToUInt64<TEnum>(this TEnum value, TypeCode enumTypeCode) where TEnum : struct, Enum
		{
			Debug.Assert(enumTypeCode == Type.GetTypeCode(typeof(TEnum)));
			return enumTypeCode switch
			{
				TypeCode.SByte => unchecked((ulong)Unsafe.As<TEnum, SByte>(ref value)),
				TypeCode.Int16 => unchecked((ulong)Unsafe.As<TEnum, Int16>(ref value)),
				TypeCode.Int32 => unchecked((ulong)Unsafe.As<TEnum, Int32>(ref value)),
				TypeCode.Int64 => unchecked((ulong)Unsafe.As<TEnum, Int64>(ref value)),
				TypeCode.Byte => Unsafe.As<TEnum, Byte>(ref value),
				TypeCode.UInt16 => Unsafe.As<TEnum, UInt16>(ref value),
				TypeCode.UInt32 => Unsafe.As<TEnum, UInt32>(ref value),
				TypeCode.UInt64 => Unsafe.As<TEnum, UInt64>(ref value),
				_ => throw new ArgumentException(enumTypeCode.ToString()),
			};
		}

		public static TEnum ToEnum<TEnum>(this UInt64 value) where TEnum : struct, Enum => value.ToEnum<TEnum>(EnumTypeData<TEnum>.EnumTypeCode);
		
		internal static TEnum ToEnum<TEnum>(this UInt64 value, TypeCode enumTypeCode) where TEnum : struct, Enum
		{
			Debug.Assert(enumTypeCode == Type.GetTypeCode(typeof(TEnum)));
			switch (enumTypeCode)
			{
				case TypeCode.SByte:
					{
						var i = unchecked((SByte)value);
						return Unsafe.As<SByte, TEnum>(ref i);
					};
				case TypeCode.Int16:
					{
						var i = unchecked((Int16)value);
						return Unsafe.As<Int16, TEnum>(ref i);
					};
				case TypeCode.Int32:
					{
						var i = unchecked((Int32)value);
						return Unsafe.As<Int32, TEnum>(ref i);
					};
				case TypeCode.Int64:
					{
						var i = unchecked((Int64)value);
						return Unsafe.As<Int64, TEnum>(ref i);
					};
				case TypeCode.Byte:
					{
						var i = unchecked((Byte)value);
						return Unsafe.As<Byte, TEnum>(ref i);
					};
				case TypeCode.UInt16:
					{
						var i = unchecked((UInt16)value);
						return Unsafe.As<UInt16, TEnum>(ref i);
					};
				case TypeCode.UInt32:
					{
						var i = unchecked((UInt32)value);
						return Unsafe.As<UInt32, TEnum>(ref i);
					};
				case TypeCode.UInt64:
					{
						var i = unchecked((UInt64)value);
						return Unsafe.As<UInt64, TEnum>(ref i);
					};
				default:
					throw new ArgumentException(enumTypeCode.ToString());
			}
		}
	}

	public static class StringExtensions
	{
		public static IEnumerable<ReadOnlyMemory<char>> Split(this ReadOnlyMemory<char> chars, char separator, StringSplitOptions options = StringSplitOptions.None)
		{
			int index;
			while ((index = chars.Span.IndexOf(separator)) >= 0)
			{
				var slice = chars.Slice(0, index);
				if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
					slice = slice.Trim();
				if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || slice.Length > 0)
					yield return slice;
				chars = chars.Slice(index + 1);
			}
			if ((options & StringSplitOptions.TrimEntries) == StringSplitOptions.TrimEntries)
				chars = chars.Trim();
			if ((options & StringSplitOptions.RemoveEmptyEntries) == 0 || chars.Length > 0)
				yield return chars;
		}
	}

	public static class ListExtensions
	{
        public static int BinarySearch<TValue, TKey>(this TValue [] list, TKey key, Func<TValue, TKey, int> comparer)
        {
            if (list == null || comparer == null)
                throw new ArgumentNullException();
            int low = 0;
            int high = list.Length - 1;
            while (low <= high)
            {
                var mid = low + ((high - low) >> 1);
                var order = comparer(list[mid], key);
                if (order == 0)
                    return mid;
                else if (order > 0)
                    high = mid - 1;
                else
                    low = mid + 1;
            }
            return ~low;
        }
		
		public static int BinarySearchFirst<TValue, TKey>(this TValue [] list, TKey key, Func<TValue, TKey, int> comparer)
		{
            int index = list.BinarySearch(key, comparer);
            for (; index > 0 && comparer(list[index-1], key) == 0; index--)
                ;
            return index;
		}
	}

	public class CharMemoryComparer : IEqualityComparer<ReadOnlyMemory<char>>
	{
		public static CharMemoryComparer OrdinalIgnoreCase { get; } = new CharMemoryComparer(StringComparison.OrdinalIgnoreCase);
		public static CharMemoryComparer Ordinal { get; }  = new CharMemoryComparer(StringComparison.Ordinal);
	
		readonly StringComparison comparison;
		CharMemoryComparer(StringComparison comparison) => this.comparison = comparison;
        public bool Equals(ReadOnlyMemory<char> x, ReadOnlyMemory<char> y) => MemoryExtensions.Equals(x.Span, y.Span, comparison);
        public int GetHashCode(ReadOnlyMemory<char> obj) => String.GetHashCode(obj.Span, comparison);
	}

	/********************************************************/
	
	public enum Example {
		Trick, 
		Treat, 
		[JsonPropertyName("Trick-Or-Treat")] // Error: Attribute 'JsonPropertyName' is not valid on this declaration type. It is only valid on 'property, indexer' declarations.
		TrickOrTreat
	}

    enum LongEnum : long
    {
		Min = long.MinValue,
        Zero = ((long)1 << 0),
        One = ((long)1 << 1),
        Two = ((long)1 << 2),
        Seven = ((long)1 << 7),
        Fourteen = ((long)1 << 14),
        ThirtyOne = ((long)1 << 31),
        ThirtyTwo = ((long)1 << 32),
        SixtyTwo = ((long)1 << 62),
        SixtyThree = ((long)1 << 63),
		Max = long.MaxValue
    };

    enum ULongEnum : ulong
    {
		Min = ulong.MinValue,
        Zero = ((ulong)1 << 0),
        One = ((ulong)1 << 1),
        Two = ((ulong)1 << 2),
        Seven = ((ulong)1 << 7),
        Fourteen = ((ulong)1 << 14),
        ThirtyOne = ((ulong)1 << 31),
        ThirtyTwo = ((ulong)1 << 32),
        ThirtyThree = ((ulong)1 << 33),
        Sixty = ((ulong)1 << 60),
        SixtyOne = ((ulong)1 << 61),
        SixtyTwo = ((ulong)1 << 62),
        SixtyThree = ((ulong)1 << 63),
		Max = ulong.MaxValue,
    };

	enum Int32Enum : Int32
	{
		Min = Int32.MinValue,
		Max = Int32.MaxValue,
	}

	enum UInt32Enum : UInt32
	{
		Min = UInt32.MinValue,
		Max = UInt32.MaxValue,
	}
	
	enum Int16Enum : Int16
	{
		Min = Int16.MinValue,
		Zero = 0,
		One = 1,
		Max = Int16.MaxValue,
	}

	enum UInt16Enum : UInt16
	{
		Min = UInt16.MinValue,
		Max = UInt16.MaxValue,
	}

	enum SByteEnum : SByte
	{
		Min = SByte.MinValue,
		Zero = 0,
		Max = SByte.MaxValue,
	}

	enum ByteEnum : Byte
	{
		Min = Byte.MinValue,
		Max = Byte.MaxValue,
	}

	public enum DuplicateEnum : short
	{
		One = 1,
		Two = 1,
	}

	public enum EnumDifferingInCase : short { AA, Aa, aa }

	public enum EmptyEnum { }

	[Flags]
	public enum FlaggedEmptyEnum { }

	[Flags]
	public enum FlaggedWithComposite
	{
		Zero = 0,
		One = (1<<0),
		Two = (1<<1),
		Three = One | Two,
		Four = (1<<2),
		Eight = (1<<3),
		All = One | Two | Four | Eight,
	}

	[Flags]
    enum FlagLongEnum : long
    {
        Zero = ((long)1 << 0),
        One = ((long)1 << 1),
        Two = ((long)1 << 2),
        Seven = ((long)1 << 7),
        Fourteen = ((long)1 << 14),
        ThirtyOne = ((long)1 << 31),
        ThirtyTwo = ((long)1 << 32),
        SixtyTwo = ((long)1 << 62),
        SixtyThree = ((long)1 << 63),
    };

	[Flags]
    enum FlagULongEnum : ulong
    {
        Zero = ((ulong)1 << 0),
        One = ((ulong)1 << 1),
        Two = ((ulong)1 << 2),
        Seven = ((ulong)1 << 7),
        Fourteen = ((ulong)1 << 14),
        ThirtyOne = ((ulong)1 << 31),
        ThirtyTwo = ((ulong)1 << 32),
        ThirtyThree = ((ulong)1 << 33),
        Sixty = ((ulong)1 << 60),
        SixtyOne = ((ulong)1 << 61),
        SixtyTwo = ((ulong)1 << 62),
        SixtyThree = ((ulong)1 << 63),
    };

	[Flags]
	enum FlagsWithManyDuplicates
	{
		aAAA = 1,
		aaaa = 1,
		AAAA = 1,
		aAaA = 1,
		AaAa = 1,
		Aaaa = 1,
		AAAa = 1,
		aaaA = 1,
		aaAA = 1,
		AAaa = 1,
	}

	public enum EnumMemberExample {
		Trick, 
		Treat, 
		[EnumMember(Value = "Trick-Or-Treat")] // Error: Attribute 'JsonPropertyName' is not valid on this declaration type. It is only valid on 'property, indexer' declarations.
		TrickOrTreat
	}

	public class WackyNamingPolicy : JsonNamingPolicy
	{
		public override string ConvertName(string name) => "Wacky-" + name;
	}

    class TestClass
    {
        public static void Test()
        {
			TestMisc();
			
			TestEnumMemberOverride();
			
			TestOverride();
			
			TestNullable();
			
			Assert.AreEqual(TypeCode.Int64, Type.GetTypeCode(typeof(LongEnum)));
			
			var sysOptions = new JsonSerializerOptions
			{
				Converters = { new JsonStringEnumConverter() },
			};
			
			// Check the behavior of JsonSerializer with enums with duplicate values or that differ in case
			Assert.AreEqual("[\"One\",\"One\"]", JsonSerializer.Serialize(Enum.GetValues<DuplicateEnum>(), sysOptions));
			Assert.AreEqual("[\"AA\",\"Aa\",\"aa\"]", JsonSerializer.Serialize(Enum.GetValues<EnumDifferingInCase>(), sysOptions));

			// Check the behavior of JsonSerializer for enums with names that only differ in case.
			Assert.AreEqual(EnumDifferingInCase.AA, JsonSerializer.Deserialize<EnumDifferingInCase>("\"AA\"", sysOptions));
			Assert.AreEqual(EnumDifferingInCase.aa, JsonSerializer.Deserialize<EnumDifferingInCase>("\"aa\"", sysOptions));
			Assert.AreEqual(EnumDifferingInCase.Aa, JsonSerializer.Deserialize<EnumDifferingInCase>("\"Aa\"", sysOptions));
			Assert.AreEqual(EnumDifferingInCase.AA, JsonSerializer.Deserialize<EnumDifferingInCase>("\"aA\"", sysOptions)); // There is no EnumDifferingInCase.aA
			
			TestUnflagged();
			
			Console.WriteLine();
			
			TestFlagged();
		}	
		
		static void TestEnumMemberOverride()
		{
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new JsonEnumMemberStringEnumConverter(null, true) },
			};
			
			var test = Enum.GetValues<EnumMemberExample>();
			
			var json = JsonSerializer.Serialize(test, myOptions);
			Assert.AreEqual("[\"Trick\",\"Treat\",\"Trick-Or-Treat\"]", json);
			
			Console.WriteLine(json);
			
			var myOptions2 = new JsonSerializerOptions
			{
				Converters = { new JsonEnumMemberStringEnumConverter(JsonNamingPolicy.CamelCase, true) },
			};
			var json2 = JsonSerializer.Serialize(test, myOptions2);
			Assert.AreEqual("[\"trick\",\"treat\",\"Trick-Or-Treat\"]", json2);
			
			Console.WriteLine(json2);
		}

		static void TestOverride()
		{
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new JsonPropertyNameStringEnumConverter(null, true) },
			};
			
			var test = Enum.GetValues<Example>();
			
			var json = JsonSerializer.Serialize(test, myOptions);
			Assert.AreEqual("[\"Trick\",\"Treat\",\"Trick-Or-Treat\"]", json);
			
			Console.WriteLine(json);
			
			var myOptions2 = new JsonSerializerOptions
			{
				Converters = { new JsonPropertyNameStringEnumConverter(JsonNamingPolicy.CamelCase, true) },
			};
			var json2 = JsonSerializer.Serialize(test, myOptions2);
			Assert.AreEqual("[\"trick\",\"treat\",\"Trick-Or-Treat\"]", json2);
			
			Console.WriteLine(json2);
		}
		
		static void TestNullable()
		{
			TestNullableNoPolicy();
			TestNullableWithPolicy();
		}

		static void TestNullableNoPolicy()
		{
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new JsonPropertyNameStringEnumConverter(null, true) },
			};
			
			var json = "[null,\"Zero\",\"SixtyThree\",\"One, Two\"]";
			var array = JsonSerializer.Deserialize<FlagULongEnum? []>(json, myOptions);
			Assert.IsTrue(array!.SequenceEqual(new [] { (FlagULongEnum?)null, FlagULongEnum.Zero, FlagULongEnum.SixtyThree, FlagULongEnum.One | FlagULongEnum.Two }));
			Assert.AreEqual(json, JsonSerializer.Serialize(array, myOptions), json);
		}

		static void TestNullableWithPolicy()
		{
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new GeneralJsonStringEnumConverter(JsonNamingPolicy.CamelCase, true) },
			};
			
			var json = "[null,\"zero\",\"sixtyThree\",\"one, two\"]";
			var array = JsonSerializer.Deserialize<FlagULongEnum? []>(json, myOptions);
			Assert.IsTrue(array!.SequenceEqual(new [] { (FlagULongEnum?)null, FlagULongEnum.Zero, FlagULongEnum.SixtyThree, FlagULongEnum.One | FlagULongEnum.Two }));
			Assert.AreEqual(json, JsonSerializer.Serialize(array, myOptions), json);
		}
		
		
		static void TestMisc()
		{
			Assert.AreEqual(4, ",,,".AsMemory().Split(',').Count());
			Assert.AreEqual(0, ",,,".AsMemory().Split(',', StringSplitOptions.RemoveEmptyEntries).Count());
			Assert.AreEqual(0, " , , , ".AsMemory().Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Count());
			Assert.AreEqual(4, " , , , ".AsMemory().Split(',', StringSplitOptions.RemoveEmptyEntries).Count());
		}
		
		static void TestFlagged()
		{
			var sysOptions = new JsonSerializerOptions
			{
				Converters = { new JsonStringEnumConverter(new WackyNamingPolicy()) },
			};
			
			var test = Enum.GetValues<FlaggedWithComposite>().Concat(new [] { FlaggedWithComposite.Four | FlaggedWithComposite.Two }).ToArray();
			
			TestWriteArrayFlagged(test, null);
			TestWriteArrayFlagged(new [] { default(FlaggedEmptyEnum) });
			TestRoundTripFlagged<FlaggedEmptyEnum>();
			TestRoundTripFlagged<FlaggedWithComposite>(test);

			TestRoundTripFlagged<FlagLongEnum>();
			TestRoundTripFlagged<FlagULongEnum>();

			TestRoundTripFlagged<FlagsWithManyDuplicates>();

			TestRoundTripWithNamingPolicyFlagged(Enum.GetValues<FlagLongEnum>(), new WackyNamingPolicy(), true);
			TestRoundTripWithNamingPolicyFlagged(Enum.GetValues<FlagULongEnum>(), new WackyNamingPolicy(), true);
			TestRoundTripWithNamingPolicyFlagged(Enum.GetValues<FlagsWithManyDuplicates>(), new WackyNamingPolicy(), true);
		}
		
		static void TestRoundTripFlagged<TEnum>() where TEnum : struct, Enum => 
			TestRoundTripFlagged(Enum.GetValues<TEnum>());
		
		static void TestRoundTripFlagged<TEnum>(TEnum [] values, bool print = false) where TEnum : struct, Enum
		{
			var json = TestWriteArrayFlagged(values);
			if (print)
				Console.WriteLine(json);
			TestReadArrayFlagged<TEnum>(json);
		}
		
		static void TestRoundTripWithNamingPolicyFlagged<TEnum>(TEnum [] array, JsonNamingPolicy? namingPolicy, bool print = false) where TEnum : struct, Enum
		{
			var json = TestWriteArrayFlagged(array, namingPolicy);
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new GeneralJsonStringEnumConverter(namingPolicy, true) },
			};

			if (print)
				Console.WriteLine(json);
			
			var myArray = JsonSerializer.Deserialize<TEnum []>(json, myOptions);
			Assert.IsTrue(array.SequenceEqual(myArray!), json);
		}
		
		static string TestWriteArrayFlagged<TEnum>(TEnum [] array, JsonNamingPolicy? namingPolicy = default) where TEnum : struct, Enum
		{
			var sysOptions = new JsonSerializerOptions
			{
				Converters = { new JsonStringEnumConverter(namingPolicy) },
			};
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new GeneralJsonStringEnumConverter(namingPolicy, true) },
			};
			
			var sysJson = JsonSerializer.Serialize(array, sysOptions);
			var myJson = JsonSerializer.Serialize(array, myOptions);
			
			Assert.AreEqual(sysJson, myJson, myJson);
			
			return myJson;
		}
		
		static void TestReadArrayFlagged<TEnum>(string arrayJson) where TEnum : struct, Enum
		{
			var sysOptions = new JsonSerializerOptions
			{
				Converters = { new JsonStringEnumConverter() },
			};
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new GeneralJsonStringEnumConverter(null, true) },
			};
			
			var sysArray = JsonSerializer.Deserialize<TEnum []>(arrayJson, sysOptions);
			var myArray = JsonSerializer.Deserialize<TEnum []>(arrayJson, myOptions);
			
			Assert.IsTrue(sysArray!.SequenceEqual(myArray!));
		}
		
		static void TestUnflagged()
		{
			TestRoundTrip(Enumerable.Range(-4, 20).Select(i => (LongEnum)i).ToArray());
			TestRoundTripWithNamingPolicy(new [] { default(EmptyEnum), (EmptyEnum)1 }, new WackyNamingPolicy(), false);

			TestRoundTrip<ByteEnum>();
			TestRoundTrip<SByteEnum>();
			TestRoundTrip<Int16Enum>();
			TestRoundTrip<UInt16Enum>();
			TestRoundTrip<Int32Enum>();
			TestRoundTrip<UInt32Enum>();
			TestRoundTrip<LongEnum>();
			TestRoundTrip<ULongEnum>();
			TestRoundTrip<DuplicateEnum>();
			TestRoundTrip<EnumDifferingInCase>();
			
			TestReadArray<EnumDifferingInCase>("[\"AA\", \"aA\", \"Aa\", \"aa\"]");
			
			TestRoundTripWithNamingPolicy(Enum.GetValues<ULongEnum>(), new WackyNamingPolicy(), true);
			TestRoundTripWithNamingPolicy(Enum.GetValues<EnumDifferingInCase>(), new WackyNamingPolicy(), true);
			TestWriteArray(Enum.GetValues<EnumDifferingInCase>(), new WackyNamingPolicy());
			TestRoundTripWithNamingPolicy(Enumerable.Range(-4, 20).Select(i => (LongEnum)i).ToArray(), null, true);
			TestRoundTripWithNamingPolicy(Enumerable.Range(-4, 20).Select(i => (LongEnum)i).ToArray(), new WackyNamingPolicy(), false);
			TestRoundTripWithNamingPolicy(Enumerable.Range(-4, 20).Select(i => (ULongEnum)i).ToArray(), new WackyNamingPolicy(), false);
		}
		
		static void TestRoundTrip<TEnum>() where TEnum : struct, Enum => 
			TestRoundTrip(Enum.GetValues<TEnum>());
		
		static void TestRoundTrip<TEnum>(TEnum [] values, bool print = false) where TEnum : struct, Enum
		{
			var json = TestWriteArray(values);
			if (print)
				Console.WriteLine(json);
			TestReadArray<TEnum>(json);
		}
		
		static void TestRoundTripWithNamingPolicy<TEnum>(TEnum [] array, JsonNamingPolicy? namingPolicy, bool print = false) where TEnum : struct, Enum
		{
			var json = TestWriteArray(array, namingPolicy);
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new GeneralJsonStringEnumConverter(namingPolicy, true) },
			};

			if (print)
				Console.WriteLine(json);
			
			var myArray = JsonSerializer.Deserialize<TEnum []>(json, myOptions);
			Assert.IsTrue(array.SequenceEqual(myArray!), json);
		}
		
		static string TestWriteArray<TEnum>(TEnum [] array, JsonNamingPolicy? namingPolicy = default) where TEnum : struct, Enum
		{
			var sysOptions = new JsonSerializerOptions
			{
				Converters = { new JsonStringEnumConverter(namingPolicy) },
			};
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new GeneralJsonStringEnumConverter(namingPolicy, true) },
			};
			
			var sysJson = JsonSerializer.Serialize(array, sysOptions);
			var myJson = JsonSerializer.Serialize(array, myOptions);
			
			Assert.AreEqual(sysJson, myJson, myJson);
			
			return myJson;
		}
		
		static void TestReadArray<TEnum>(string arrayJson) where TEnum : struct, Enum
		{
			var sysOptions = new JsonSerializerOptions
			{
				Converters = { new JsonStringEnumConverter() },
			};
			var myOptions = new JsonSerializerOptions
			{
				Converters = { new GeneralJsonStringEnumConverter(null, true) },
			};
			
			var sysArray = JsonSerializer.Deserialize<TEnum []>(arrayJson, sysOptions);
			var myArray = JsonSerializer.Deserialize<TEnum []>(arrayJson, myOptions);
			
			Assert.IsTrue(sysArray!.SequenceEqual(myArray!));
		}
	}

	public class Program
	{
		public static void Main()
		{
			Console.WriteLine("Environment version: {0} ({1}), OS: {2}.", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription, Environment.Version, Environment.OSVersion);
			Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
			Console.WriteLine();

			try
			{
				TestClass.Test();
			}
			catch (Exception ex)
			{
				Console.WriteLine("Failed with unhandled exception: ");
				Console.WriteLine(ex);
				throw;
			}
		}
	}
;