diff --git a/ChartJs.Blazor.Tests/ChartJs.Blazor.Tests.csproj b/ChartJs.Blazor.Tests/ChartJs.Blazor.Tests.csproj new file mode 100644 index 00000000..3f84eadc --- /dev/null +++ b/ChartJs.Blazor.Tests/ChartJs.Blazor.Tests.csproj @@ -0,0 +1,24 @@ + + + + netcoreapp3.1 + + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/ChartJs.Blazor.Tests/ObjectEnumTests.Deserialization.cs b/ChartJs.Blazor.Tests/ObjectEnumTests.Deserialization.cs new file mode 100644 index 00000000..9a54ecf4 --- /dev/null +++ b/ChartJs.Blazor.Tests/ObjectEnumTests.Deserialization.cs @@ -0,0 +1,122 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class ObjectEnumTests + { + [Theory] + [InlineData("0", 0)] + [InlineData("10", 10)] + [InlineData("-1234567489", -1234567489)] + public void Deserialize_IntEnum_FromRoot(string json, int expectedValue) + { + // Act + TestObjectEnum objEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.True(objEnum.Equals(expectedValue)); // expects all the equality stuff to be correct + } + + [Theory] + // [InlineData("0", 0.0)] this would fail because it gets serialized as int, not double + [InlineData("0.0", 0.0)] + [InlineData("123.456", 123.456)] + [InlineData("-654.321", -654.321)] + public void Deserialize_DoubleEnum_FromRoot(string json, double expectedValue) + { + // Act + TestObjectEnum objEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.True(objEnum.Equals(expectedValue)); // expects all the equality stuff to be correct + } + + [Theory] + [InlineData("0", 0)] + [InlineData("10", 10)] + [InlineData("-1234567489", -1234567489)] + public void Deserialize_DoubleEnumThroughInt_FromRoot(string json, double expectedValue) + { + // Act + // the json would result in an int being deserialized but since there is no int + // that would fail. So instead it uses the double constructor. + DoubleStringEnum objEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.True(objEnum.Equals(expectedValue)); // expects all the equality stuff to be correct + } + + [Theory] + [InlineData("\"Hello World!\"", "Hello World!")] + [InlineData("\"\\\"That's what!\\\", she said.\"", "\"That's what!\", she said.")] + [InlineData("\"¨öä$ü¨^'{}][\\\\|/-.,+-/*\"", "¨öä$ü¨^'{}][\\|/-.,+-/*")] + public void Deserialize_StringEnum_FromRoot(string json, string expectedValue) + { + // Act + TestObjectEnum objEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.True(objEnum.Equals(expectedValue)); // expects all the equality stuff to be correct + } + + [Fact] + public void Deserialize_BigIntegerEnum_ThrowsNotSupported() + { + // Arrange + string json = $"{ulong.MaxValue}"; // bigger than long.MaxValue + + // Act & Assert + Assert.Throws(() => JsonConvert.DeserializeObject(json)); + } + + [Fact] + public void Deserialize_JsonArray_ThrowsNotSupported() + { + // Arrange + const string json = "[1,2,3]"; + + // Act & Assert + Assert.Throws(() => JsonConvert.DeserializeObject(json)); + } + + [Fact] + public void Deserialize_JsonObject_ThrowsNotSupported() + { + // Arrange + string json = "{}"; + + // Act & Assert + Assert.Throws(() => JsonConvert.DeserializeObject(json)); + } + + [Fact] + public void Deserialize_Null_ReturnsNull() + { + // Arrange + string json = "null"; + + // Act + TestObjectEnum objEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.Null(objEnum); + } + + [Fact] + public void Deserialize_Undefined_ReturnsNull() + { + // Arrange + string json = "undefined"; + + // Act + TestObjectEnum objEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.Null(objEnum); + } + } +} \ No newline at end of file diff --git a/ChartJs.Blazor.Tests/ObjectEnumTests.Equality.cs b/ChartJs.Blazor.Tests/ObjectEnumTests.Equality.cs new file mode 100644 index 00000000..a495d1a0 --- /dev/null +++ b/ChartJs.Blazor.Tests/ObjectEnumTests.Equality.cs @@ -0,0 +1,245 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class ObjectEnumTests + { + [Fact] + public void Equals_IntEnumAndIntEnum_ReturnsTrue() + { + // Arrange + const int ExampleIntValue = 10; + var a = TestObjectEnum.Int(ExampleIntValue); + var b = TestObjectEnum.Int(ExampleIntValue); + + // Act + bool equal = a.Equals(b); + + // Assert + Assert.True(equal); + } + + [Fact] + public void Equals_IntEnumAndInt_ReturnsTrue() + { + // Arrange + const int ExampleIntValue = 10; + var a = TestObjectEnum.Int(ExampleIntValue); + + // Act + bool equal = a.Equals(ExampleIntValue); + + // Assert + Assert.True(equal); + } + + [Fact] + public void Equals_StringEnumAndStringEnum_ReturnsTrue() + { + // Arrange + var a = TestObjectEnum.Auto; + var b = TestObjectEnum.Auto; // different instance, same inner value + + // Act + bool equal = a.Equals(b); + + // Assert + Assert.True(equal); + } + + [Fact] + public void Equals_StringEnumAndString_ReturnsTrue() + { + // Arrange + const string ExampleStringValue = "abcdefg"; + var a = TestObjectEnum.CustomString(ExampleStringValue); + + // Act + bool equal = a.Equals(ExampleStringValue); + + // Assert + Assert.True(equal); + } + + [Fact] + public void Equals_DoubleEnumAndDoubleEnum_ReturnsTrue() + { + // Arrange + const double ExampleDoubleValue = 123.456; + var a = TestObjectEnum.Double(ExampleDoubleValue); + var b = TestObjectEnum.Double(ExampleDoubleValue); + + // Act + bool equal = a.Equals(b); + + // Assert + Assert.True(equal); + } + + [Fact] + public void Equals_DoubleEnumAndDouble_ReturnsTrue() + { + // Arrange + const double ExampleDoubleValue = 123.456; + var a = TestObjectEnum.Double(ExampleDoubleValue); + + // Act + bool equal = a.Equals(ExampleDoubleValue); + + // Assert + Assert.True(equal); + } + + [Fact] + public void Equals_EnumAndNull_ReturnsFalse() + { + // Arrange + var a = TestObjectEnum.CustomObject(new object()); + + // Act + bool equal = a.Equals(null); + + // Assert + Assert.False(equal); + } + + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + // Arrange + var a = TestObjectEnum.CustomObject(new object()); + var b = a; + + // Act + bool equal = a.Equals(b); + + // Assert + Assert.True(equal); + } + + [Fact] + public void EqualityOperator_IntEnumAndIntEnum_ReturnsTrue() + { + // Arrange + const int ExampleIntValue = 10; + var a = TestObjectEnum.Int(ExampleIntValue); + var b = TestObjectEnum.Int(ExampleIntValue); + + // Act + bool equal = a == b; + + // Assert + Assert.True(equal); + } + + [Fact] + public void EqualityOperator_StringEnumAndStringEnum_ReturnsTrue() + { + // Arrange + var a = TestObjectEnum.Auto; + var b = TestObjectEnum.Auto; // different instance, same inner value + + // Act + bool equal = a == b; + + // Assert + Assert.True(equal); + } + + [Fact] + public void EqualityOperator_DoubleEnumAndDoubleEnum_ReturnsTrue() + { + // Arrange + const double ExampleDoubleValue = 123.456; + var a = TestObjectEnum.Double(ExampleDoubleValue); + var b = TestObjectEnum.Double(ExampleDoubleValue); + + // Act + bool equal = a == b; + + // Assert + Assert.True(equal); + } + + [Fact] + public void EqualityOperator_EnumAndNull_ReturnsFalse() + { + // Arrange + var a = TestObjectEnum.CustomObject(new object()); + + // Act + bool equal = a == null; + + // Assert + Assert.False(equal); + } + + [Fact] + public void EqualityOperator_NullAndNull_ReturnsTrue() + { + // Arrange + TestObjectEnum a = null; + + // Act + bool equal = a == null; + + // Assert + Assert.True(equal); + } + + [Fact] + public void EqualityOperator_SameReference_ReturnsTrue() + { + // Arrange + var a = TestObjectEnum.CustomObject(new object()); + var b = a; + + // Act + bool equal = a == b; + + // Assert + Assert.True(equal); + } + + [Theory] + [InlineData(10)] + [InlineData(123.456)] + [InlineData("asdf")] + [InlineData(false)] + public void GetHashCode_InnerValueAndEnum_Equals(object value) + { + // Arrange + TestObjectEnum objEnum = TestObjectEnum.CustomObject(value); + + // Act + int hashCodeValue = value.GetHashCode(); + int hashCodeEnum = objEnum.GetHashCode(); + + // Assert + Assert.Equal(hashCodeValue, hashCodeEnum); + } + + [Theory] + [InlineData(-10)] + [InlineData(-654.321)] + [InlineData("fdsa")] + [InlineData(true)] + public void GetHashCode_EnumAndEnum_Equals(object value) + { + // Arrange + var a = TestObjectEnum.CustomObject(value); + var b = TestObjectEnum.CustomObject(value); + + // Act + int hashA = a.GetHashCode(); + int hashB = b.GetHashCode(); + + // Assert + Assert.Equal(hashA, hashB); + } + } +} \ No newline at end of file diff --git a/ChartJs.Blazor.Tests/ObjectEnumTests.General.cs b/ChartJs.Blazor.Tests/ObjectEnumTests.General.cs new file mode 100644 index 00000000..5d5f498b --- /dev/null +++ b/ChartJs.Blazor.Tests/ObjectEnumTests.General.cs @@ -0,0 +1,28 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class ObjectEnumTests + { + [Fact] + public void Construct_NullValue_ThrowsArgumentNullException() + { + Assert.Throws(() => TestObjectEnum.Null); + Assert.Throws(() => TestObjectEnum.CustomString(null)); + } + + [Fact] + public void Deserialize_EnumWithoutConstructor_ThrowsNotSupportedException() + { + // Arrange + const string json = "\"asdf\""; + + // Act & Assert + Assert.Throws(() => JsonConvert.DeserializeObject(json)); + } + } +} diff --git a/ChartJs.Blazor.Tests/ObjectEnumTests.Serialization.cs b/ChartJs.Blazor.Tests/ObjectEnumTests.Serialization.cs new file mode 100644 index 00000000..53ba9234 --- /dev/null +++ b/ChartJs.Blazor.Tests/ObjectEnumTests.Serialization.cs @@ -0,0 +1,105 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class ObjectEnumTests + { + [Theory] + [InlineData(0)] + [InlineData(10)] + [InlineData(-10)] + [InlineData(int.MinValue)] + [InlineData(int.MaxValue)] + public void Serialize_IntEnum_AsRoot(int value) + { + // Arrange + TestObjectEnum objEnum = TestObjectEnum.Int(value); + + // Act + string serialized = JsonConvert.SerializeObject(objEnum); + + // Assert + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), serialized); + } + + [Theory] + [InlineData(123.456)] + [InlineData(-654.321)] + [InlineData(double.MinValue)] + [InlineData(double.MaxValue)] + public void Serialize_DoubleEnum_AsRoot(double value) + { + // Arrange + TestObjectEnum objEnum = TestObjectEnum.Double(value); + + // Act + string serialized = JsonConvert.SerializeObject(objEnum); + + // Assert + Assert.Equal(value.ToString(CultureInfo.InvariantCulture), serialized); + } + + [Fact] + public void Serialize_DoubleEnumZero_AsRoot() + { + // Arrange + TestObjectEnum objEnum = TestObjectEnum.Double(0); + + // Act + string serialized = JsonConvert.SerializeObject(objEnum); + + // Assert + Assert.Equal("0.0", serialized); + } + + [Fact] + public void Serialize_FloatEnum_ThrowsNotSupported() + { + // Arrange + TestObjectEnum objEnum = TestObjectEnum.Float(15.2f); + + // Act & Assert + Assert.Throws(() => JsonConvert.SerializeObject(objEnum)); + } + + [Theory] + [InlineData("foo")] + [InlineData("bar")] + [InlineData("whomst'd've'ly'yaint'nt'ed'ies's'y'es")] + [InlineData("\"That's what!\", she said.")] + [InlineData("\uD83D\uDE42 emoji shenanigans")] + [InlineData("¨öä$ü¨^'{}][\\|/-.,+-/*")] + public void Serialize_StringEnum_AsRoot(string value) + { + // Arrange + TestObjectEnum objEnum = TestObjectEnum.CustomString(value); + string escapedValue = JsonConvert.ToString(value); + + // Act + string serialized = JsonConvert.SerializeObject(objEnum); + + // Assert + Assert.Equal(escapedValue, serialized); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void Serialize_BoolEnum_AsRoot(bool value) + { + // Arrange + TestObjectEnum objEnum = TestObjectEnum.CustomBool(value); + string escapedValue = JsonConvert.ToString(value); + + // Act + string serialized = JsonConvert.SerializeObject(objEnum); + + // Assert + Assert.Equal(escapedValue, serialized); + } + } +} \ No newline at end of file diff --git a/ChartJs.Blazor.Tests/ObjectEnumTests.TestClasses.cs b/ChartJs.Blazor.Tests/ObjectEnumTests.TestClasses.cs new file mode 100644 index 00000000..a7628fe7 --- /dev/null +++ b/ChartJs.Blazor.Tests/ObjectEnumTests.TestClasses.cs @@ -0,0 +1,48 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class ObjectEnumTests + { + private class TestObjectEnum : ObjectEnum + { + public static TestObjectEnum Null => new TestObjectEnum(null); + public static TestObjectEnum True => new TestObjectEnum(true); + public static TestObjectEnum Auto => new TestObjectEnum("auto"); + public static TestObjectEnum CustomBool(bool value) => new TestObjectEnum(value); + public static TestObjectEnum Int(int value) => new TestObjectEnum(value); + public static TestObjectEnum Double(double value) => new TestObjectEnum(value); + public static TestObjectEnum Float(float value) => new TestObjectEnum(value); + public static TestObjectEnum CustomString(string value) => new TestObjectEnum(value); + // Only for testing! + public static TestObjectEnum CustomObject(object value) => new TestObjectEnum(value); + + private TestObjectEnum(string value) : base(value) { } + private TestObjectEnum(int value) : base(value) { } + private TestObjectEnum(float value) : base(value) { } + private TestObjectEnum(double value) : base(value) { } + private TestObjectEnum(bool value) : base(value) { } + // Only for testing! + private TestObjectEnum(object value) : base(value) { } + } + + private class DoubleStringEnum : ObjectEnum + { + public static DoubleStringEnum CustomString(string value) => new DoubleStringEnum(value); + public static DoubleStringEnum CustomDouble(double value) => new DoubleStringEnum(value); + + private DoubleStringEnum(string value) : base(value) { } + private DoubleStringEnum(double value) : base(value) { } + } + + private class EnumWithoutConstructor : ObjectEnum + { + // no suitable (supported) constructor + private EnumWithoutConstructor(object value) : base(value) { } + } + } +} \ No newline at end of file diff --git a/ChartJs.Blazor.Tests/StringEnumTests.Deserialization.cs b/ChartJs.Blazor.Tests/StringEnumTests.Deserialization.cs new file mode 100644 index 00000000..df1e6f2f --- /dev/null +++ b/ChartJs.Blazor.Tests/StringEnumTests.Deserialization.cs @@ -0,0 +1,91 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class StringEnumTests + { + [Theory] + [InlineData("\"foo\"", "foo")] + [InlineData("\"Hello World!\"", "Hello World!")] + [InlineData("\"\\\"That's what!\\\", she said.\"", "\"That's what!\", she said.")] + [InlineData("\"¨öä$ü¨^'{}][\\\\|/-.,+-/*\"", "¨öä$ü¨^'{}][\\|/-.,+-/*")] + public void Deserialize_StringEnum_FromRoot(string json, string expectedValue) + { + // Act + TestStringEnum stringEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.True(stringEnum.Equals(expectedValue)); // expects all the equality stuff to be correct + } + + [Fact] + public void Deserialize_Int_ThrowsNotSupported() + { + // Arrange + const string json = "0"; + + // Act & Assert + Assert.Throws(() => JsonConvert.DeserializeObject(json)); + } + + [Fact] + public void Deserialize_Double_ThrowsNotSupported() + { + // Arrange + const string json = "0.0"; + + // Act & Assert + Assert.Throws(() => JsonConvert.DeserializeObject(json)); + } + + [Fact] + public void Deserialize_JsonArray_ThrowsNotSupported() + { + // Arrange + const string json = "[1,2,3]"; + + // Act & Assert + Assert.Throws(() => JsonConvert.DeserializeObject(json)); + } + + [Fact] + public void Deserialize_JsonObject_ThrowsNotSupported() + { + // Arrange + string json = "{}"; + + // Act & Assert + Assert.Throws(() => JsonConvert.DeserializeObject(json)); + } + + [Fact] + public void Deserialize_Null_ReturnsNull() + { + // Arrange + string json = "null"; + + // Act + TestStringEnum stringEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.Null(stringEnum); + } + + [Fact] + public void Deserialize_Undefined_ReturnsNull() + { + // Arrange + string json = "undefined"; + + // Act + TestStringEnum stringEnum = JsonConvert.DeserializeObject(json); + + // Assert + Assert.Null(stringEnum); + } + } +} diff --git a/ChartJs.Blazor.Tests/StringEnumTests.Equality.cs b/ChartJs.Blazor.Tests/StringEnumTests.Equality.cs new file mode 100644 index 00000000..1f0875f5 --- /dev/null +++ b/ChartJs.Blazor.Tests/StringEnumTests.Equality.cs @@ -0,0 +1,150 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class StringEnumTests + { + [Fact] + public void Equals_StringEnumAndStringEnum_ReturnsTrue() + { + // Arrange + var a = TestStringEnum.Auto; + var b = TestStringEnum.Auto; // different instance, same inner value + + // Act + bool equal = a.Equals(b); + + // Assert + Assert.True(equal); + } + + [Fact] + public void Equals_StringEnumAndString_ReturnsTrue() + { + // Arrange + const string ExampleStringValue = "abcdefg"; + var a = TestStringEnum.Custom(ExampleStringValue); + + // Act + bool equal = a.Equals(ExampleStringValue); + + // Assert + Assert.True(equal); + } + + [Fact] + public void Equals_EnumAndNull_ReturnsFalse() + { + // Arrange + var a = TestStringEnum.Custom(string.Empty); + + // Act + bool equal = a.Equals(null); + + // Assert + Assert.False(equal); + } + + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + // Arrange + var a = TestStringEnum.Custom(string.Empty); + var b = a; + + // Act + bool equal = a.Equals(b); + + // Assert + Assert.True(equal); + } + + [Fact] + public void EqualityOperator_StringEnumAndStringEnum_ReturnsTrue() + { + // Arrange + var a = TestStringEnum.Auto; + var b = TestStringEnum.Auto; // different instance, same inner value + + // Act + bool equal = a == b; + + // Assert + Assert.True(equal); + } + + [Fact] + public void EqualityOperator_StringEnumAndNull_ReturnsFalse() + { + // Arrange + var a = TestStringEnum.Custom(string.Empty); + + // Act + bool equal = a == null; + + // Assert + Assert.False(equal); + } + + [Fact] + public void EqualityOperator_NullAndNull_ReturnsTrue() + { + // Arrange + TestStringEnum a = null; + + // Act + bool equal = a == null; + + // Assert + Assert.True(equal); + } + + [Fact] + public void EqualityOperator_SameReference_ReturnsTrue() + { + // Arrange + var a = TestStringEnum.Custom(string.Empty); + var b = a; + + // Act + bool equal = a == b; + + // Assert + Assert.True(equal); + } + + [Fact] + public void GetHashCode_InnerValueAndEnum_Equals() + { + // Arrange + const string ExampleString = "asdf"; + TestStringEnum stringEnum = TestStringEnum.Custom(ExampleString); + + // Act + int hashCodeValue = ExampleString.GetHashCode(); + int hashCodeEnum = stringEnum.GetHashCode(); + + // Assert + Assert.Equal(hashCodeValue, hashCodeEnum); + } + + [Fact] + public void GetHashCode_EnumAndEnum_Equals() + { + // Arrange + var a = TestStringEnum.Auto; + var b = TestStringEnum.Auto; // different instance, same inner value + + // Act + int hashA = a.GetHashCode(); + int hashB = b.GetHashCode(); + + // Assert + Assert.Equal(hashA, hashB); + } + } +} diff --git a/ChartJs.Blazor.Tests/StringEnumTests.General.cs b/ChartJs.Blazor.Tests/StringEnumTests.General.cs new file mode 100644 index 00000000..3a54edfb --- /dev/null +++ b/ChartJs.Blazor.Tests/StringEnumTests.General.cs @@ -0,0 +1,17 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class StringEnumTests + { + [Fact] + public void Construct_NullValue_ThrowsArgumentNullException() + { + Assert.Throws(() => TestStringEnum.Custom(null)); + } + } +} diff --git a/ChartJs.Blazor.Tests/StringEnumTests.Serialization.cs b/ChartJs.Blazor.Tests/StringEnumTests.Serialization.cs new file mode 100644 index 00000000..aa06e268 --- /dev/null +++ b/ChartJs.Blazor.Tests/StringEnumTests.Serialization.cs @@ -0,0 +1,33 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class StringEnumTests + { + [Theory] + [InlineData("foo")] + [InlineData("bar")] + [InlineData("1234567890")] + [InlineData("")] + [InlineData("whomst'd've'ly'yaint'nt'ed'ies's'y'es")] + [InlineData("\"That's what!\", she said.")] + [InlineData("\uD83D\uDE42 emoji shenanigans")] + [InlineData("¨öä$ü¨^'{}][\\|/-.,+-/*")] + public void Serialize_StringEnum_AsRoot(string value) + { + // Arrange + TestStringEnum objEnum = TestStringEnum.Custom(value); + string escapedValue = JsonConvert.ToString(value); + + // Act + string serialized = JsonConvert.SerializeObject(objEnum); + + // Assert + Assert.Equal(escapedValue, serialized); + } + } +} diff --git a/ChartJs.Blazor.Tests/StringEnumTests.TestClasses.cs b/ChartJs.Blazor.Tests/StringEnumTests.TestClasses.cs new file mode 100644 index 00000000..3a72d242 --- /dev/null +++ b/ChartJs.Blazor.Tests/StringEnumTests.TestClasses.cs @@ -0,0 +1,19 @@ +using ChartJs.Blazor.ChartJS.Common.Enums; +using Newtonsoft.Json; +using System; +using System.Globalization; +using Xunit; + +namespace ChartJs.Blazor.Tests +{ + public partial class StringEnumTests + { + private class TestStringEnum : StringEnum + { + public static TestStringEnum Auto => new TestStringEnum("auto"); + public static TestStringEnum Custom(string value) => new TestStringEnum(value); + + private TestStringEnum(string stringRep) : base(stringRep) { } + } + } +} diff --git a/ChartJs.Blazor.sln b/ChartJs.Blazor.sln index 3f0c1a90..e80e1d45 100644 --- a/ChartJs.Blazor.sln +++ b/ChartJs.Blazor.sln @@ -7,7 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{08E01108-AF6 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5474F748-8E27-4FD6-AD9D-1087578ED4D7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChartJs.Blazor", "src\ChartJs.Blazor\ChartJs.Blazor.csproj", "{1990A3D7-7B00-469F-BC97-F614393F7A52}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChartJs.Blazor", "src\ChartJs.Blazor\ChartJs.Blazor.csproj", "{1990A3D7-7B00-469F-BC97-F614393F7A52}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ClientSide", "ClientSide", "{474E0BD0-B8C0-4A00-9B05-B1B33F3B7C94}" EndProject @@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChartJs.Blazor.Sample.Serve EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChartJs.Blazor.Sample.ClientSide", "samples\ClientSide\ChartJs.Blazor.Sample.ClientSide\ChartJs.Blazor.Sample.ClientSide.csproj", "{153F4719-305A-4AD0-975A-91ABD9935F87}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChartJs.Blazor.Tests", "ChartJs.Blazor.Tests\ChartJs.Blazor.Tests.csproj", "{135A9451-FC82-48B4-89F6-3CBF5312B2C2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -43,6 +45,10 @@ Global {153F4719-305A-4AD0-975A-91ABD9935F87}.Debug|Any CPU.Build.0 = Debug|Any CPU {153F4719-305A-4AD0-975A-91ABD9935F87}.Release|Any CPU.ActiveCfg = Release|Any CPU {153F4719-305A-4AD0-975A-91ABD9935F87}.Release|Any CPU.Build.0 = Release|Any CPU + {135A9451-FC82-48B4-89F6-3CBF5312B2C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {135A9451-FC82-48B4-89F6-3CBF5312B2C2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {135A9451-FC82-48B4-89F6-3CBF5312B2C2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {135A9451-FC82-48B4-89F6-3CBF5312B2C2}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -55,6 +61,7 @@ Global {3FB87C6F-5C21-4095-AC64-35AE6299B88F} = {99703BE7-7A02-454D-8066-87BBEBA039BD} {D547792A-5B2C-4FAF-93AA-71BAA9BF845C} = {A9B59800-FB77-43FE-BF42-BE991D9FDBF5} {153F4719-305A-4AD0-975A-91ABD9935F87} = {474E0BD0-B8C0-4A00-9B05-B1B33F3B7C94} + {135A9451-FC82-48B4-89F6-3CBF5312B2C2} = {08E01108-AF62-4E37-824D-0A22DA1988C3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6B9B5C87-8AF0-4846-81C5-798F5C654FB6} diff --git a/src/ChartJs.Blazor/ChartJS/Common/Enums/JsonConverter/JsonObjectEnumConverter.cs b/src/ChartJs.Blazor/ChartJS/Common/Enums/JsonConverter/JsonObjectEnumConverter.cs deleted file mode 100644 index 5ffb3420..00000000 --- a/src/ChartJs.Blazor/ChartJS/Common/Enums/JsonConverter/JsonObjectEnumConverter.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Newtonsoft.Json; - -namespace ChartJs.Blazor.ChartJS.Common.Enums.JsonConverter -{ - /// - /// JsonConverter for converting and writing an ObjectEnum value. This JsonConverter can only write. - /// - internal class JsonObjectEnumConverter : JsonWriteOnlyConverter - { - public override void WriteJson(JsonWriter writer, ObjectEnum wrapper, JsonSerializer serializer) - { - try - { - // if it can be written in a single JToken, - // json.net understands what type the wrapped object (.Value) is and serializes it accordingly -> correct value and type (eg. bool, string, double) - writer.WriteValue(wrapper.Value); - } - catch (JsonWriterException) - { - // if there was an error, try to explicitly serialize it before writing - // if this also fails just let it bubble up because the developer should not have values in their enum that fail here - serializer.Serialize(writer, wrapper.Value); - } - } - } -} diff --git a/src/ChartJs.Blazor/ChartJS/Common/Enums/JsonConverter/JsonStringEnumConverter.cs b/src/ChartJs.Blazor/ChartJS/Common/Enums/JsonConverter/JsonStringEnumConverter.cs deleted file mode 100644 index 5f9c4e94..00000000 --- a/src/ChartJs.Blazor/ChartJS/Common/Enums/JsonConverter/JsonStringEnumConverter.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Newtonsoft.Json; - -namespace ChartJs.Blazor.ChartJS.Common.Enums.JsonConverter -{ - /// - /// JsonConverter for converting and writing a StringEnum value. This JsonConverter can only write. - /// - internal class JsonStringEnumConverter : JsonWriteOnlyConverter - { - public override void WriteJson(JsonWriter writer, StringEnum value, JsonSerializer serializer) - { - // ToString was overwritten by StringEnum -> safe to just print the string representation - writer.WriteValue(value.ToString()); - } - } -} diff --git a/src/ChartJs.Blazor/ChartJS/Common/Enums/ObjectEnum.cs b/src/ChartJs.Blazor/ChartJS/Common/Enums/ObjectEnum.cs index 74297339..ae087db8 100644 --- a/src/ChartJs.Blazor/ChartJS/Common/Enums/ObjectEnum.cs +++ b/src/ChartJs.Blazor/ChartJS/Common/Enums/ObjectEnum.cs @@ -1,53 +1,125 @@ -using Newtonsoft.Json; -using ChartJs.Blazor.ChartJS.Common.Enums.JsonConverter; +using System; +using System.Collections.Generic; +using System.Linq; namespace ChartJs.Blazor.ChartJS.Common.Enums { /// - /// Inherit this class if you are in need of a pseudo-Enum which can hold values of different kinds (eg. string, double and bool). + /// The base class for enums that can represent different types. We also use these + /// "enums" like Discriminated Unions to provide a type safe way of communicating with the + /// dynamic language JavaScript is. + /// + /// De-/serialization is supported but only for the following types: + /// , , and . + /// For the deserialization, the constructors with a single parameter of a supported type + /// are considered for instantiating the object enum. + /// + /// + /// When implementing an object enum, make sure to provide only private constructors + /// with the types that are allowed (DO NOT expose public constructors; expose meaningful + /// static factory methods instead). The actual enum values are static properties that pass + /// the correct value to the private constructor. Make these properties return new values + /// everytime so we don't create all the enum values even though we don't use them. + /// In the classic use case, we don't call many of these properties anyway and usually + /// only a few times. You can also have static factory methods that + /// create an instance of the object enum with the specified value as long as the parameter + /// type is supported. Also consider sealing your enum unless you have a specific reason not to. + /// /// - [JsonConverter(typeof(JsonObjectEnumConverter))] - public abstract class ObjectEnum + [Newtonsoft.Json.JsonConverter(typeof(Serialization.JsonObjectEnumConverter))] + public abstract class ObjectEnum : IEquatable { + /// + /// Gets the s that are supported for serialization and deserialization. + /// can contain objects of different types but you will get a + /// once you try to serialize (or deserialize) that + /// . + /// + private static readonly Type[] SupportedSerializationTypes = new[] + { + typeof(int), typeof(double), typeof(string), typeof(bool) + }; + /// /// Holds the actual value represented by this instance. /// internal object Value { get; } - + /// - /// Creates a new instance of with a value. + /// Creates a new instance of . /// - /// The value this enum-instance is supposed to represent. - protected ObjectEnum(object value) => Value = value; + /// The value this instance is supposed to represent. + protected ObjectEnum(object value) + { + Value = value ?? throw new ArgumentNullException(nameof(value)); + + if (value is ObjectEnum) + throw new ArgumentException("The value cannot be an ObjectEnum. " + + "Recursive ObjectEnums aren't allowed."); + } /// - /// Returns the string representation of the underlying object. Calls .ToString(). + /// Checks if a is in the list of supported serialization types. + /// If this function returns , de-/serialization will fail on + /// s containing an instance of that + /// (). /// - /// The string representation of the underlying object. - public override string ToString() => Value.ToString(); - -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member - public static bool operator == (ObjectEnum a, ObjectEnum b) => a.Value == b.Value; - public static bool operator != (ObjectEnum a, ObjectEnum b) => a.Value != b.Value; -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + /// The to check. + internal static bool IsSupportedSerializationType(Type type) => + SupportedSerializationTypes.Contains(type); /// - /// Determines whether the specified object instance is considered equal to the current instance. + /// Determines whether the specified object is considered equal to the current object. + /// + /// is considered to be equal to this instance if it.. + /// + /// is the same instance as this instance. + /// is another with the same internal value. + /// is the same value as the internal value of this . + /// + /// /// - /// The object to compare with. - /// true if the objects are considered equal; otherwise, false. + /// The object to compare with the current object. + /// true if the specified object is considered to be equal to the current object; + /// otherwise, false. public override bool Equals(object obj) { - if (typeof(ObjectEnum).IsAssignableFrom(obj.GetType()) && obj != null) return Value.Equals(((ObjectEnum)obj).Value); + if (obj is ObjectEnum asEnum) + { + return Equals(asEnum); + } - // it also counts as equal if the object to compare is equal to the object stored in the wrapper return Value.Equals(obj); } /// - /// Returns the hash of the underlying object. + /// Indicates whether the current object is equal to another object of the same type. /// - /// The hash of the underlying object. + /// An object to compare with this object. + /// true if the current object is equal to the other parameter; otherwise, false. + /// + public bool Equals(ObjectEnum other) => + other != null && + Value.Equals(other.Value); + + /// + /// Returns the hash code of the underlying value. + /// + /// The hash code of the underlying value. public override int GetHashCode() => Value.GetHashCode(); + + /// + /// Returns the representation of the underlying object. + /// Calls on the underlying object. + /// + /// The representation of the underlying object. + public override string ToString() => Value.ToString(); + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static bool operator ==(ObjectEnum left, ObjectEnum right) => + EqualityComparer.Default.Equals(left?.Value, right?.Value); + + public static bool operator !=(ObjectEnum left, ObjectEnum right) => !(left == right); +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } } diff --git a/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/JsonObjectEnumConverter.cs b/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/JsonObjectEnumConverter.cs new file mode 100644 index 00000000..eb72f140 --- /dev/null +++ b/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/JsonObjectEnumConverter.cs @@ -0,0 +1,98 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace ChartJs.Blazor.ChartJS.Common.Enums.Serialization +{ + internal class JsonObjectEnumConverter : JsonConverter + { + public override ObjectEnum ReadJson(JsonReader reader, Type objectType, [AllowNull] ObjectEnum existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null || + reader.TokenType == JsonToken.Undefined) + { + return null; + } + + if (reader.Value == null) + { + /* Covers all token types that result in reader.Value not being assigned (yet) except null and undefined + * Examples are: StartArray, StartObject, .. + */ + throw new NotSupportedException($"Deserializing ObjectEnums from token type '{reader.TokenType}' isn't supported."); + } + + object value = reader.Value; + Type readerValueType = value.GetType(); + /* The Type of reader.Value isn't always what it was when it was serialized. + * An issue pointing that out: https://github.com/dotnet/orleans/issues/1269#issuecomment-171233788 + * - Any integer number will be of type System.Int64 unless its smaller + * than Int64.MinValue or higher than Int64.MaxValue, then it will be of type System.Numerics.BigInteger + * - Any non-integer number will be of type System.Double + * + * Currently we cast down long to int and ignore BigInteger. This means that only int is supported + * and we don't waste time checking for other options and converting between types. + * + * Another option would be to check for a suitable constructor and if there isn't one try to find the + * most optimal conversion. Even though that sounds nice, it's really not necessary at the moment. There's + * no need for BigInteger support in the enums and it would hurt performance a bit. + */ + + ObjectEnumFactory factory = ObjectEnumFactory.GetFactory(objectType); + + // special case for long since json.net's default for number deserialization is long but our enums + // use (and only support) int at the moment. BigInteger isn't supported at all and will throw on the next check. + if (readerValueType == typeof(long)) + { + long asLong = (long)value; + if (asLong < int.MinValue || + asLong > int.MaxValue) + { + throw new OverflowException($"The deserialized number ({value}) is out of the range of int ({int.MinValue} - {int.MaxValue})."); + } + else + { + value = (int)asLong; + readerValueType = typeof(int); + } + } + + if (!factory.CanConvertFrom(readerValueType)) + { + if (readerValueType == typeof(int) && + factory.CanConvertFrom(typeof(double))) + { + /* Sometimes a value like "0" or "10" might get deserialized as int even though + * the ObjectEnum meant to handle a double. In that case, we can convert + * the int value to a double and create the enum value from there. + */ + value = (double)(int)value; // both casts are required! (else InvalidCastException) + readerValueType = typeof(double); + } + else + { + throw new NotSupportedException($"Deserialization {nameof(ObjectEnum)} '{objectType.FullName}' from '{readerValueType.Name}' isn't supported."); + } + } + + return factory.Create(value, readerValueType); + } + + public override void WriteJson(JsonWriter writer, ObjectEnum wrapper, JsonSerializer serializer) + { + // Note: wrapper won't be null (json.net wouldn't call this method if it were null) + Type wrappedType = wrapper.Value.GetType(); + if (!ObjectEnum.IsSupportedSerializationType(wrappedType)) + { + throw new NotSupportedException($"The type '{wrappedType.FullName}' isn't supported for serialization " + + $"within an instance of any {nameof(ObjectEnum)}-type."); + } + + // The types we support can always be written in a single Token. + // If that was not the case, we'd need to handle JsonWriterException here. + writer.WriteValue(wrapper.Value); + } + } +} diff --git a/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/JsonStringEnumConverter.cs b/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/JsonStringEnumConverter.cs new file mode 100644 index 00000000..4bd24d04 --- /dev/null +++ b/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/JsonStringEnumConverter.cs @@ -0,0 +1,40 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace ChartJs.Blazor.ChartJS.Common.Enums.Serialization +{ + internal class JsonStringEnumConverter : JsonConverter + { + private static readonly Type[] s_stringParameterArray = new[] { typeof(string) }; + private static readonly ConcurrentDictionary s_constructorCache = new ConcurrentDictionary(); + + public override StringEnum ReadJson(JsonReader reader, Type objectType, [AllowNull] StringEnum existingValue, bool hasExistingValue, JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Null: + case JsonToken.Undefined: + return null; + case JsonToken.String: + ConstructorInfo constructor = s_constructorCache.GetOrAdd(objectType, GetStringConstructor); + + return (StringEnum)constructor.Invoke(new[] { reader.Value }); + default: + throw new NotSupportedException($"Deserializing StringEnums from token type '{reader.TokenType}' isn't supported."); + } + } + + public override void WriteJson(JsonWriter writer, StringEnum value, JsonSerializer serializer) + { + // Note: value won't be null (json.net wouldn't call this method if it were null) + // ToString was overwritten by StringEnum -> safe to just print the string representation + writer.WriteValue(value.ToString()); + } + + private ConstructorInfo GetStringConstructor(Type type) => + type.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, s_stringParameterArray, null); + } +} diff --git a/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/ObjectEnumFactory.cs b/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/ObjectEnumFactory.cs new file mode 100644 index 00000000..3f4796fa --- /dev/null +++ b/src/ChartJs.Blazor/ChartJS/Common/Enums/Serialization/ObjectEnumFactory.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace ChartJs.Blazor.ChartJS.Common.Enums.Serialization +{ + /* We favour using a non-generic design here because the "entry point" where this class is used + * is a JsonConverter that has to work for all types of ObjectEnum. Therefore the converter isn't + * generic and we don't have a generic parameter to begin with. We would need to use reflection + * to create the factory and if we just work with Type, we can reduce the reflection use a bit. + */ + internal class ObjectEnumFactory + { + private static readonly ConcurrentDictionary s_factorySingletons = new ConcurrentDictionary(); + + private readonly Dictionary _constructorCache; + private readonly Type _enumType; + + /// + /// Gets (and creates if needed) the singleton-factory for this . + /// + /// The -type whose factory to get. + public static ObjectEnumFactory GetFactory(Type enumType) + { + if (enumType == null) + throw new ArgumentNullException(nameof(enumType)); + + if (!typeof(ObjectEnum).IsAssignableFrom(enumType)) + throw new ArgumentException($"The type '{enumType.FullName}' doesn't inherit from '{typeof(ObjectEnum).FullName}'"); + + return s_factorySingletons.GetOrAdd(enumType, type => new ObjectEnumFactory(type)); + } + + private ObjectEnumFactory(Type enumType) + { + // checks omitted because the constructor is non-public + _enumType = enumType; + _constructorCache = CreateConstructorDictionary(); + } + + /// + /// Creates a new instance of the -type this factory is for. + /// If there is no suitable constructor for the , a + /// will be thrown. + /// + /// The value used for instantiating the . + public ObjectEnum Create(object value) => Create(value, value.GetType()); + + /// + /// Creates a new instance of the -type this factory is for. + /// If there is no suitable constructor for the type , a + /// will be thrown. + /// + /// Use this method if the type of is already known + /// (and you're sure about it). + /// + /// + /// The value used for instantiating the . + /// The of . + public ObjectEnum Create(object value, Type valueType) + { + if (_constructorCache.TryGetValue(valueType, out ConstructorInfo constructor)) + { + return (ObjectEnum)constructor.Invoke(new[] { value }); + } + + if (ObjectEnum.IsSupportedSerializationType(valueType)) + { + throw new NotSupportedException($"The object enum '{_enumType.FullName}' doesn't have a constructor which takes a single " + + $"argument of type '{valueType.FullName}'."); + } + else + { + throw new NotSupportedException($"The type '{valueType}' isn't supported for serialization within {nameof(ObjectEnum)}."); + } + } + + /// + /// Checks if a suitable constructor for this exists which + /// can be used to create a new instance of that -type. + /// + /// The of the enum-content to look for. + /// if there is a suitable constructor for that ; + /// otherwise . + public bool CanConvertFrom(Type contentType) => _constructorCache.ContainsKey(contentType); + + private Dictionary CreateConstructorDictionary() + { + Dictionary dict = new Dictionary(); + ConstructorInfo[] constructors = _enumType.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic); + foreach (ConstructorInfo constructor in constructors) + { + ParameterInfo[] constructorParams = constructor.GetParameters(); + if (constructorParams.Length != 1) + { + continue; + } + + Type paramType = constructorParams[0].ParameterType; + if (ObjectEnum.IsSupportedSerializationType(paramType)) + { + dict.Add(paramType, constructor); + } + } + + if (dict.Count == 0) + { + throw new NotSupportedException($"The {nameof(ObjectEnum)} type '{_enumType.FullName}' doesn't have any " + + $"suitable constructors for deserialization."); + } + + return dict; + } + } +} diff --git a/src/ChartJs.Blazor/ChartJS/Common/Enums/StringEnum.cs b/src/ChartJs.Blazor/ChartJS/Common/Enums/StringEnum.cs index 37a125e6..72ad380f 100644 --- a/src/ChartJs.Blazor/ChartJS/Common/Enums/StringEnum.cs +++ b/src/ChartJs.Blazor/ChartJS/Common/Enums/StringEnum.cs @@ -1,33 +1,97 @@ -using Newtonsoft.Json; -using ChartJs.Blazor.ChartJS.Common.Enums.JsonConverter; +using System; namespace ChartJs.Blazor.ChartJS.Common.Enums { - [JsonConverter(typeof(JsonStringEnumConverter))] - public abstract class StringEnum + /// + /// The base class for enums that are meant to be serialized. They are more flexible + /// than normal C# enums (through type safe enum pattern). + /// + /// When implementing a , make sure to only implement a single + /// constructor that takes a single . Make this constructor private! + /// The actual enum values are static properties that pass the correct value to the private + /// constructor. Make these properties return new values everytime so we don't create all + /// the enum values even though we don't use them. In the classic use case, we don't call + /// many of these properties anyway and usually only a few times. + /// In the rare case that you need a that can contain any + /// value, expose a static factory method but don't make the constructor + /// public. Also consider sealing your enum unless you have a specific reason not to. + /// + /// + [Newtonsoft.Json.JsonConverter(typeof(Serialization.JsonStringEnumConverter))] + public abstract class StringEnum : IEquatable { private readonly string _value; - protected StringEnum(string stringRep) => _value = stringRep; - - public override string ToString() => _value; - - public static explicit operator string(StringEnum stringEnum) => stringEnum.ToString(); - - - public static bool operator == (StringEnum a, StringEnum b) => a.ToString() == b.ToString(); - public static bool operator != (StringEnum a, StringEnum b) => a.ToString() != b.ToString(); + /// + /// Creates a new instance of . + /// + /// The this instance should represent. + protected StringEnum(string stringRep) + { + _value = stringRep ?? throw new ArgumentNullException(nameof(stringRep)); + } + /// + /// Determines whether the specified object is considered equal to the current object. + /// + /// is considered to be equal to this instance if it.. + /// + /// is the same instance as this instance. + /// is another with the same internal + /// value. + /// is the same value as the internal value + /// of this . + /// + /// + /// + /// The object to compare with the current object. + /// true if the specified object is considered to be equal to the current + /// object; otherwise, false. public override bool Equals(object obj) { - if (typeof(StringEnum).IsAssignableFrom(obj.GetType())) return _value.Equals(obj.ToString()); + if (obj is StringEnum asEnum) + { + return Equals(asEnum); + } - // it also counts as equal if the object to compare is equal to the string stored in the wrapper - if (obj.GetType() == typeof(string)) return _value.Equals((string)obj); + if (obj is string asString) + { + return _value == asString; + } return false; } + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// An object to compare with this object. + /// true if the current object is equal to the other parameter; + /// otherwise, false. + public bool Equals(StringEnum other) => + other != null && + _value == other._value; + + /// + /// Returns the hash code of the underlying value. + /// + /// The hash code of the underlying value. public override int GetHashCode() => _value.GetHashCode(); + + /// + /// Returns the underlying value. + /// + /// The underlying value. + public override string ToString() => _value; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + public static bool operator ==(StringEnum left, StringEnum right) => + left?._value == right?._value; + + public static bool operator !=(StringEnum left, StringEnum right) => + left?._value != right?._value; + + public static explicit operator string(StringEnum stringEnum) => stringEnum._value; +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } }