diff --git a/CryptoExchange.Net.UnitTests/AsyncResetEventTests.cs b/CryptoExchange.Net.UnitTests/AsyncResetEventTests.cs index 31285b2..db829e3 100644 --- a/CryptoExchange.Net.UnitTests/AsyncResetEventTests.cs +++ b/CryptoExchange.Net.UnitTests/AsyncResetEventTests.cs @@ -1,5 +1,6 @@ using CryptoExchange.Net.Objects; using NUnit.Framework; +using NUnit.Framework.Legacy; using System; using System.Collections.Generic; using System.Linq; @@ -24,8 +25,8 @@ namespace CryptoExchange.Net.UnitTests var result1 = await waiter1; var result2 = await waiter2; - Assert.True(result1); - Assert.True(result2); + Assert.That(result1); + Assert.That(result2); } [Test] @@ -39,8 +40,8 @@ namespace CryptoExchange.Net.UnitTests var result1 = await waiter1; var result2 = await waiter2; - Assert.True(result1); - Assert.True(result2); + Assert.That(result1); + Assert.That(result2); } [Test] @@ -55,14 +56,14 @@ namespace CryptoExchange.Net.UnitTests var result1 = await waiter1; - Assert.True(result1); - Assert.True(waiter2.Status != TaskStatus.RanToCompletion); + Assert.That(result1); + Assert.That(waiter2.Status != TaskStatus.RanToCompletion); evnt.Set(); var result2 = await waiter2; - Assert.True(result2); + Assert.That(result2); } [Test] @@ -75,13 +76,13 @@ namespace CryptoExchange.Net.UnitTests var result1 = await waiter1; - Assert.True(result1); - Assert.True(waiter2.Status != TaskStatus.RanToCompletion); + Assert.That(result1); + Assert.That(waiter2.Status != TaskStatus.RanToCompletion); evnt.Set(); var result2 = await waiter2; - Assert.True(result2); + Assert.That(result2); } [Test] @@ -105,12 +106,12 @@ namespace CryptoExchange.Net.UnitTests for(var i = 1; i <= 10; i++) { evnt.Set(); - Assert.AreEqual(10 - i, waiters.Count(w => w.Status != TaskStatus.RanToCompletion)); + Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion)); } await resultsWaiter; - Assert.AreEqual(10, results.Count(r => r)); + Assert.That(10 == results.Count(r => r)); } [Test] @@ -124,7 +125,7 @@ namespace CryptoExchange.Net.UnitTests var result1 = await waiter1; - Assert.True(result1); + Assert.That(result1); } [Test] @@ -134,9 +135,9 @@ namespace CryptoExchange.Net.UnitTests var waiter1 = evnt.WaitAsync(TimeSpan.FromMilliseconds(100)); - var result1 = await waiter1; + var result1 = await waiter1; - Assert.False(result1); + ClassicAssert.False(result1); } } } diff --git a/CryptoExchange.Net.UnitTests/BaseClientTests.cs b/CryptoExchange.Net.UnitTests/BaseClientTests.cs index ae99787..f64b00e 100644 --- a/CryptoExchange.Net.UnitTests/BaseClientTests.cs +++ b/CryptoExchange.Net.UnitTests/BaseClientTests.cs @@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.UnitTests.TestImplementations; using Microsoft.Extensions.Logging; using NUnit.Framework; +using NUnit.Framework.Legacy; using System; using System.Collections.Generic; @@ -21,7 +22,7 @@ namespace CryptoExchange.Net.UnitTests var result = client.SubClient.Deserialize("{\"testProperty\": 123}"); // assert - Assert.IsTrue(result.Success); + Assert.That(result.Success); } [TestCase] @@ -34,8 +35,8 @@ namespace CryptoExchange.Net.UnitTests var result = client.SubClient.Deserialize("{\"testProperty\": 123"); // assert - Assert.IsFalse(result.Success); - Assert.IsTrue(result.Error != null); + ClassicAssert.IsFalse(result.Success); + Assert.That(result.Error != null); } [TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")] @@ -48,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests public void AppendPathTests(string baseUrl, string[] path, string expected) { var result = baseUrl.AppendPath(path); - Assert.AreEqual(expected, result); + Assert.That(expected == result); } } } diff --git a/CryptoExchange.Net.UnitTests/CallResultTests.cs b/CryptoExchange.Net.UnitTests/CallResultTests.cs index b445664..823fee8 100644 --- a/CryptoExchange.Net.UnitTests/CallResultTests.cs +++ b/CryptoExchange.Net.UnitTests/CallResultTests.cs @@ -1,5 +1,6 @@ using CryptoExchange.Net.Objects; using NUnit.Framework; +using NUnit.Framework.Legacy; using System; using System.Collections.Generic; using System.Linq; @@ -17,9 +18,9 @@ namespace CryptoExchange.Net.UnitTests { var result = new CallResult(new ServerError("TestError")); - Assert.AreEqual(result.Error.Message, "TestError"); - Assert.IsFalse(result); - Assert.IsFalse(result.Success); + ClassicAssert.AreSame(result.Error.Message, "TestError"); + ClassicAssert.IsFalse(result); + ClassicAssert.IsFalse(result.Success); } [Test] @@ -27,9 +28,9 @@ namespace CryptoExchange.Net.UnitTests { var result = new CallResult(null); - Assert.IsNull(result.Error); - Assert.IsTrue(result); - Assert.IsTrue(result.Success); + ClassicAssert.IsNull(result.Error); + Assert.That(result); + Assert.That(result.Success); } [Test] @@ -37,10 +38,10 @@ namespace CryptoExchange.Net.UnitTests { var result = new CallResult(new ServerError("TestError")); - Assert.AreEqual(result.Error.Message, "TestError"); - Assert.IsNull(result.Data); - Assert.IsFalse(result); - Assert.IsFalse(result.Success); + ClassicAssert.AreSame(result.Error.Message, "TestError"); + ClassicAssert.IsNull(result.Data); + ClassicAssert.IsFalse(result); + ClassicAssert.IsFalse(result.Success); } [Test] @@ -48,10 +49,10 @@ namespace CryptoExchange.Net.UnitTests { var result = new CallResult(new object()); - Assert.IsNull(result.Error); - Assert.IsNotNull(result.Data); - Assert.IsTrue(result); - Assert.IsTrue(result.Success); + ClassicAssert.IsNull(result.Error); + ClassicAssert.IsNotNull(result.Data); + Assert.That(result); + Assert.That(result.Success); } [Test] @@ -60,11 +61,11 @@ namespace CryptoExchange.Net.UnitTests var result = new CallResult(new TestObjectResult()); var asResult = result.As(result.Data.InnerData); - Assert.IsNull(asResult.Error); - Assert.IsNotNull(asResult.Data); - Assert.IsTrue(asResult.Data is TestObject2); - Assert.IsTrue(asResult); - Assert.IsTrue(asResult.Success); + ClassicAssert.IsNull(asResult.Error); + ClassicAssert.IsNotNull(asResult.Data); + Assert.That(asResult.Data is not null); + Assert.That(asResult); + Assert.That(asResult.Success); } [Test] @@ -73,11 +74,11 @@ namespace CryptoExchange.Net.UnitTests var result = new CallResult(new ServerError("TestError")); var asResult = result.As(default); - Assert.IsNotNull(asResult.Error); - Assert.AreEqual(asResult.Error.Message, "TestError"); - Assert.IsNull(asResult.Data); - Assert.IsFalse(asResult); - Assert.IsFalse(asResult.Success); + ClassicAssert.IsNotNull(asResult.Error); + ClassicAssert.AreSame(asResult.Error.Message, "TestError"); + ClassicAssert.IsNull(asResult.Data); + ClassicAssert.IsFalse(asResult); + ClassicAssert.IsFalse(asResult.Success); } [Test] @@ -86,11 +87,11 @@ namespace CryptoExchange.Net.UnitTests var result = new CallResult(new ServerError("TestError")); var asResult = result.AsError(new ServerError("TestError2")); - Assert.IsNotNull(asResult.Error); - Assert.AreEqual(asResult.Error.Message, "TestError2"); - Assert.IsNull(asResult.Data); - Assert.IsFalse(asResult); - Assert.IsFalse(asResult.Success); + ClassicAssert.IsNotNull(asResult.Error); + ClassicAssert.AreSame(asResult.Error.Message, "TestError2"); + ClassicAssert.IsNull(asResult.Data); + ClassicAssert.IsFalse(asResult); + ClassicAssert.IsFalse(asResult.Success); } [Test] @@ -99,11 +100,11 @@ namespace CryptoExchange.Net.UnitTests var result = new WebCallResult(new ServerError("TestError")); var asResult = result.AsError(new ServerError("TestError2")); - Assert.IsNotNull(asResult.Error); - Assert.AreEqual(asResult.Error.Message, "TestError2"); - Assert.IsNull(asResult.Data); - Assert.IsFalse(asResult); - Assert.IsFalse(asResult.Success); + ClassicAssert.IsNotNull(asResult.Error); + ClassicAssert.AreSame(asResult.Error.Message, "TestError2"); + ClassicAssert.IsNull(asResult.Data); + ClassicAssert.IsFalse(asResult); + ClassicAssert.IsFalse(asResult.Success); } [Test] @@ -124,15 +125,15 @@ namespace CryptoExchange.Net.UnitTests null); var asResult = result.AsError(new ServerError("TestError2")); - Assert.IsNotNull(asResult.Error); - Assert.AreEqual(asResult.Error.Message, "TestError2"); - Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK); - Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1)); - Assert.AreEqual(asResult.RequestUrl, "https://test.com/api"); - Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get); - Assert.IsNull(asResult.Data); - Assert.IsFalse(asResult); - Assert.IsFalse(asResult.Success); + ClassicAssert.IsNotNull(asResult.Error); + Assert.That(asResult.Error.Message == "TestError2"); + Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK); + Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1)); + Assert.That(asResult.RequestUrl == "https://test.com/api"); + Assert.That(asResult.RequestMethod == HttpMethod.Get); + ClassicAssert.IsNull(asResult.Data); + ClassicAssert.IsFalse(asResult); + ClassicAssert.IsFalse(asResult.Success); } [Test] @@ -153,14 +154,14 @@ namespace CryptoExchange.Net.UnitTests null); var asResult = result.As(result.Data.InnerData); - Assert.IsNull(asResult.Error); - Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK); - Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1)); - Assert.AreEqual(asResult.RequestUrl, "https://test.com/api"); - Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get); - Assert.IsNotNull(asResult.Data); - Assert.IsTrue(asResult); - Assert.IsTrue(asResult.Success); + ClassicAssert.IsNull(asResult.Error); + Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK); + Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1)); + Assert.That(asResult.RequestUrl == "https://test.com/api"); + Assert.That(asResult.RequestMethod == HttpMethod.Get); + ClassicAssert.IsNotNull(asResult.Data); + Assert.That(asResult); + Assert.That(asResult.Success); } } diff --git a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj index e2fcd01..045f38e 100644 --- a/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj +++ b/CryptoExchange.Net.UnitTests/CryptoExchange.Net.UnitTests.csproj @@ -6,10 +6,10 @@ - + - - + + diff --git a/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs b/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs index 3514f89..bdb8501 100644 --- a/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs +++ b/CryptoExchange.Net.UnitTests/ExchangeHelpersTests.cs @@ -1,5 +1,6 @@ using CryptoExchange.Net.Objects; using NUnit.Framework; +using NUnit.Framework.Legacy; using System.Globalization; namespace CryptoExchange.Net.UnitTests @@ -16,7 +17,7 @@ namespace CryptoExchange.Net.UnitTests public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected) { var result = ExchangeHelpers.ClampValue(min, max, input); - Assert.AreEqual(expected, result); + Assert.That(expected == result); } [TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)] @@ -33,7 +34,7 @@ namespace CryptoExchange.Net.UnitTests public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected) { var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input); - Assert.AreEqual(expected, result); + Assert.That(expected == result); } [TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)] @@ -48,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected) { var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input); - Assert.AreEqual(expected, result); + Assert.That(expected == result); } [TestCase(5, 0.1563158, 0.15631)] @@ -59,7 +60,7 @@ namespace CryptoExchange.Net.UnitTests public void RoundDownTests(int decimalPlaces, decimal input, decimal expected) { var result = ExchangeHelpers.RoundDown(input, decimalPlaces); - Assert.AreEqual(expected, result); + Assert.That(expected == result); } [TestCase(0.1234560000, "0.123456")] @@ -67,7 +68,7 @@ namespace CryptoExchange.Net.UnitTests public void NormalizeTests(decimal input, string expected) { var result = ExchangeHelpers.Normalize(input); - Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture)); + Assert.That(expected == result.ToString(CultureInfo.InvariantCulture)); } } } diff --git a/CryptoExchange.Net.UnitTests/ConverterTests.cs b/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs similarity index 84% rename from CryptoExchange.Net.UnitTests/ConverterTests.cs rename to CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs index f1b6181..d6d56e0 100644 --- a/CryptoExchange.Net.UnitTests/ConverterTests.cs +++ b/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs @@ -1,7 +1,9 @@ using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Converters; +using CryptoExchange.Net.Converters.JsonNet; using Newtonsoft.Json; using NUnit.Framework; +using NUnit.Framework.Legacy; using System; using System.Collections.Generic; using System.Linq; @@ -11,7 +13,7 @@ using System.Threading.Tasks; namespace CryptoExchange.Net.UnitTests { [TestFixture()] - public class ConverterTests + public class JsonNetConverterTests { [TestCase("2021-05-12")] [TestCase("20210512")] @@ -27,7 +29,7 @@ namespace CryptoExchange.Net.UnitTests public void TestDateTimeConverterString(string input, bool expectNull = false) { var output = JsonConvert.DeserializeObject($"{{ \"time\": \"{input}\" }}"); - Assert.AreEqual(output.Time, expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc))); } [TestCase(1620777600.000)] @@ -35,7 +37,7 @@ namespace CryptoExchange.Net.UnitTests public void TestDateTimeConverterDouble(double input) { var output = JsonConvert.DeserializeObject($"{{ \"time\": {input} }}"); - Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); } [TestCase(1620777600)] @@ -46,7 +48,7 @@ namespace CryptoExchange.Net.UnitTests public void TestDateTimeConverterLong(long input, bool expectNull = false) { var output = JsonConvert.DeserializeObject($"{{ \"time\": {input} }}"); - Assert.AreEqual(output.Time, expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc))); } [TestCase(1620777600)] @@ -54,14 +56,14 @@ namespace CryptoExchange.Net.UnitTests public void TestDateTimeConverterFromSeconds(double input) { var output = DateTimeConverter.ConvertFromSeconds(input); - Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); } [Test] public void TestDateTimeConverterToSeconds() { var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); - Assert.AreEqual(output, 1620777600); + Assert.That(output == 1620777600); } [TestCase(1620777600000)] @@ -69,49 +71,49 @@ namespace CryptoExchange.Net.UnitTests public void TestDateTimeConverterFromMilliseconds(double input) { var output = DateTimeConverter.ConvertFromMilliseconds(input); - Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); } [Test] public void TestDateTimeConverterToMilliseconds() { var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); - Assert.AreEqual(output, 1620777600000); + Assert.That(output == 1620777600000); } [TestCase(1620777600000000)] public void TestDateTimeConverterFromMicroseconds(long input) { var output = DateTimeConverter.ConvertFromMicroseconds(input); - Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); } [Test] public void TestDateTimeConverterToMicroseconds() { var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); - Assert.AreEqual(output, 1620777600000000); + Assert.That(output == 1620777600000000); } [TestCase(1620777600000000000)] public void TestDateTimeConverterFromNanoseconds(long input) { var output = DateTimeConverter.ConvertFromNanoseconds(input); - Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); } [Test] public void TestDateTimeConverterToNanoseconds() { var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); - Assert.AreEqual(output, 1620777600000000000); + Assert.That(output == 1620777600000000000); } [TestCase()] public void TestDateTimeConverterNull() { var output = JsonConvert.DeserializeObject($"{{ \"time\": null }}"); - Assert.AreEqual(output.Time, null); + Assert.That(output.Time == null); } [TestCase(TestEnum.One, "1")] @@ -122,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected) { var output = EnumConverter.GetString(value); - Assert.AreEqual(output, expected); + Assert.That(output == expected); } [TestCase(TestEnum.One, "1")] @@ -132,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests public void TestEnumConverterGetStringTests(TestEnum value, string expected) { var output = EnumConverter.GetString(value); - Assert.AreEqual(output, expected); + Assert.That(output == expected); } [TestCase("1", TestEnum.One)] @@ -147,7 +149,7 @@ namespace CryptoExchange.Net.UnitTests { var val = value == null ? "null" : $"\"{value}\""; var output = JsonConvert.DeserializeObject($"{{ \"Value\": {val} }}"); - Assert.AreEqual(output.Value, expected); + Assert.That(output.Value == expected); } [TestCase("1", TestEnum.One)] @@ -162,7 +164,7 @@ namespace CryptoExchange.Net.UnitTests { var val = value == null ? "null" : $"\"{value}\""; var output = JsonConvert.DeserializeObject($"{{ \"Value\": {val} }}"); - Assert.AreEqual(output.Value, expected); + Assert.That(output.Value == expected); } [TestCase("1", true)] @@ -181,7 +183,7 @@ namespace CryptoExchange.Net.UnitTests { var val = value == null ? "null" : $"\"{value}\""; var output = JsonConvert.DeserializeObject($"{{ \"Value\": {val} }}"); - Assert.AreEqual(output.Value, expected); + Assert.That(output.Value == expected); } [TestCase("1", true)] @@ -200,7 +202,7 @@ namespace CryptoExchange.Net.UnitTests { var val = value == null ? "null" : $"\"{value}\""; var output = JsonConvert.DeserializeObject($"{{ \"Value\": {val} }}"); - Assert.AreEqual(output.Value, expected); + Assert.That(output.Value == expected); } } diff --git a/CryptoExchange.Net.UnitTests/OptionsTests.cs b/CryptoExchange.Net.UnitTests/OptionsTests.cs index 95ba0f2..90c832d 100644 --- a/CryptoExchange.Net.UnitTests/OptionsTests.cs +++ b/CryptoExchange.Net.UnitTests/OptionsTests.cs @@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.UnitTests.TestImplementations; using Microsoft.Extensions.Logging; using NUnit.Framework; +using NUnit.Framework.Legacy; using System; using System.Collections.Generic; using System.Linq; @@ -49,9 +50,9 @@ namespace CryptoExchange.Net.UnitTests }; // assert - Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10)); - Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123"); - Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456"); + Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10)); + Assert.That(options.ApiCredentials.Key.GetString() == "123"); + Assert.That(options.ApiCredentials.Secret.GetString() == "456"); } [Test] @@ -63,10 +64,10 @@ namespace CryptoExchange.Net.UnitTests options.Api2Options.ApiCredentials = new ApiCredentials("789", "101"); // assert - Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123"); - Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456"); - Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789"); - Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101"); + Assert.That(options.Api1Options.ApiCredentials.Key.GetString() == "123"); + Assert.That(options.Api1Options.ApiCredentials.Secret.GetString() == "456"); + Assert.That(options.Api2Options.ApiCredentials.Key.GetString() == "789"); + Assert.That(options.Api2Options.ApiCredentials.Secret.GetString() == "101"); } [Test] @@ -79,10 +80,10 @@ namespace CryptoExchange.Net.UnitTests var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider; var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider; - Assert.AreEqual(authProvider1.GetKey(), "111"); - Assert.AreEqual(authProvider1.GetSecret(), "222"); - Assert.AreEqual(authProvider2.GetKey(), "333"); - Assert.AreEqual(authProvider2.GetSecret(), "444"); + Assert.That(authProvider1.GetKey() == "111"); + Assert.That(authProvider1.GetSecret() == "222"); + Assert.That(authProvider2.GetKey() == "333"); + Assert.That(authProvider2.GetSecret() == "444"); } [Test] @@ -95,10 +96,10 @@ namespace CryptoExchange.Net.UnitTests var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider; var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider; - Assert.AreEqual(authProvider1.GetKey(), "111"); - Assert.AreEqual(authProvider1.GetSecret(), "222"); - Assert.AreEqual(authProvider2.GetKey(), "123"); - Assert.AreEqual(authProvider2.GetSecret(), "456"); + Assert.That(authProvider1.GetKey() == "111"); + Assert.That(authProvider1.GetSecret() == "222"); + Assert.That(authProvider2.GetKey() == "123"); + Assert.That(authProvider2.GetSecret() == "456"); } [Test] @@ -115,11 +116,11 @@ namespace CryptoExchange.Net.UnitTests var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider; var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider; - Assert.AreEqual(authProvider1.GetKey(), "333"); - Assert.AreEqual(authProvider1.GetSecret(), "444"); - Assert.AreEqual(authProvider2.GetKey(), "123"); - Assert.AreEqual(authProvider2.GetSecret(), "456"); - Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123"); + Assert.That(authProvider1.GetKey() == "333"); + Assert.That(authProvider1.GetSecret() == "444"); + Assert.That(authProvider2.GetKey() == "123"); + Assert.That(authProvider2.GetSecret() == "456"); + Assert.That(client.Api2.BaseAddress == "https://localhost:123"); } } diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 9b73737..5e33246 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -12,6 +12,7 @@ using Microsoft.Extensions.Logging; using System.Net.Http; using System.Threading.Tasks; using System.Threading; +using NUnit.Framework.Legacy; namespace CryptoExchange.Net.UnitTests { @@ -30,8 +31,8 @@ namespace CryptoExchange.Net.UnitTests var result = client.Api1.Request().Result; // assert - Assert.IsTrue(result.Success); - Assert.IsTrue(TestHelpers.AreEqual(expected, result.Data)); + Assert.That(result.Success); + Assert.That(TestHelpers.AreEqual(expected, result.Data)); } [TestCase] @@ -45,8 +46,8 @@ namespace CryptoExchange.Net.UnitTests var result = client.Api1.Request().Result; // assert - Assert.IsFalse(result.Success); - Assert.IsTrue(result.Error != null); + ClassicAssert.IsFalse(result.Success); + Assert.That(result.Error != null); } [TestCase] @@ -60,8 +61,8 @@ namespace CryptoExchange.Net.UnitTests var result = await client.Api1.Request(); // assert - Assert.IsFalse(result.Success); - Assert.IsTrue(result.Error != null); + ClassicAssert.IsFalse(result.Success); + Assert.That(result.Error != null); } [TestCase] @@ -75,11 +76,11 @@ namespace CryptoExchange.Net.UnitTests var result = await client.Api1.Request(); // assert - Assert.IsFalse(result.Success); - Assert.IsTrue(result.Error != null); - Assert.IsTrue(result.Error is ServerError); - Assert.IsTrue(result.Error.Message.Contains("Invalid request")); - Assert.IsTrue(result.Error.Message.Contains("123")); + ClassicAssert.IsFalse(result.Success); + Assert.That(result.Error != null); + Assert.That(result.Error is ServerError); + Assert.That(result.Error.Message.Contains("Invalid request")); + Assert.That(result.Error.Message.Contains("123")); } [TestCase] @@ -93,11 +94,11 @@ namespace CryptoExchange.Net.UnitTests var result = await client.Api2.Request(); // assert - Assert.IsFalse(result.Success); - Assert.IsTrue(result.Error != null); - Assert.IsTrue(result.Error is ServerError); - Assert.IsTrue(result.Error.Code == 123); - Assert.IsTrue(result.Error.Message == "Invalid request"); + ClassicAssert.IsFalse(result.Success); + Assert.That(result.Error != null); + Assert.That(result.Error is ServerError); + Assert.That(result.Error.Code == 123); + Assert.That(result.Error.Message == "Invalid request"); } [TestCase] @@ -112,9 +113,9 @@ namespace CryptoExchange.Net.UnitTests var client = new TestBaseClient(options); // assert - Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1); - Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail); - Assert.IsTrue(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1)); + Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1); + Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail); + Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1)); } [TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid @@ -145,13 +146,13 @@ namespace CryptoExchange.Net.UnitTests }); // assert - Assert.AreEqual(request.Method, new HttpMethod(method)); - Assert.AreEqual(request.Content?.Contains("TestParam1") == true, pos == HttpMethodParameterPosition.InBody); - Assert.AreEqual(request.Uri.ToString().Contains("TestParam1"), pos == HttpMethodParameterPosition.InUri); - Assert.AreEqual(request.Content?.Contains("TestParam2") == true, pos == HttpMethodParameterPosition.InBody); - Assert.AreEqual(request.Uri.ToString().Contains("TestParam2"), pos == HttpMethodParameterPosition.InUri); - Assert.AreEqual(request.GetHeaders().First().Key, "TestHeader"); - Assert.IsTrue(request.GetHeaders().First().Value.Contains("123")); + Assert.That(request.Method == new HttpMethod(method)); + Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody)); + Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri)); + Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody)); + Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri)); + Assert.That(request.GetHeaders().First().Key == "TestHeader"); + Assert.That(request.GetHeaders().First().Value.Contains("123")); } @@ -167,12 +168,12 @@ namespace CryptoExchange.Net.UnitTests for (var i = 0; i < requests + 1; i++) { var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); - Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0); + Assert.That(i == requests? result1.Data > 1 : result1.Data == 0); } await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); - Assert.IsTrue(result2.Data == 0); + Assert.That(result2.Data == 0); } [TestCase("/sapi/test1", true)] @@ -189,7 +190,7 @@ namespace CryptoExchange.Net.UnitTests { var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0; - Assert.IsTrue(expected); + Assert.That(expected); } } [TestCase("/sapi/", "/sapi/", true)] @@ -203,8 +204,8 @@ namespace CryptoExchange.Net.UnitTests var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); - Assert.IsTrue(result1.Data == 0); - Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0); + Assert.That(result1.Data == 0); + Assert.That(expectLimiting ? result2.Data > 0 : result2.Data == 0); } [TestCase(1, 0.1)] @@ -219,12 +220,12 @@ namespace CryptoExchange.Net.UnitTests for (var i = 0; i < requests + 1; i++) { var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); - Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0); + Assert.That(i == requests ? result1.Data > 1 : result1.Data == 0); } await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); - Assert.IsTrue(result2.Data == 0); + Assert.That(result2.Data == 0); } [TestCase("/", false)] @@ -239,7 +240,7 @@ namespace CryptoExchange.Net.UnitTests { var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0; - Assert.IsTrue(expected); + Assert.That(expected); } } @@ -256,7 +257,7 @@ namespace CryptoExchange.Net.UnitTests { var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0; - Assert.IsTrue(expected); + Assert.That(expected); } } @@ -288,8 +289,8 @@ namespace CryptoExchange.Net.UnitTests var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default); var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default); - Assert.IsTrue(result1.Data == 0); - Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0); + Assert.That(result1.Data == 0); + Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0); } [TestCase("/sapi/test", "/sapi/test", true)] @@ -302,8 +303,8 @@ namespace CryptoExchange.Net.UnitTests var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); - Assert.IsTrue(result1.Data == 0); - Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0); + Assert.That(result1.Data == 0); + Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0); } [TestCase("/sapi/test", true, true, true, false)] @@ -318,8 +319,8 @@ namespace CryptoExchange.Net.UnitTests var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default); - Assert.IsTrue(result1.Data == 0); - Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0); + Assert.That(result1.Data == 0); + Assert.That(expectLimited ? result2.Data > 0 : result2.Data == 0); } } } diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index b4fa97f..81b3952 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging; using Moq; using Newtonsoft.Json; using NUnit.Framework; +using NUnit.Framework.Legacy; namespace CryptoExchange.Net.UnitTests { @@ -28,10 +29,9 @@ namespace CryptoExchange.Net.UnitTests options.SubOptions.MaxSocketConnections = 1; }); - //assert - Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials); - Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections); + ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials); + Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections); } [TestCase(true)] @@ -47,11 +47,11 @@ namespace CryptoExchange.Net.UnitTests var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null)); //assert - Assert.IsTrue(connectResult.Success == canConnect); + Assert.That(connectResult.Success == canConnect); } [TestCase] - public async Task SocketMessages_Should_BeProcessedInDataHandlers() + public void SocketMessages_Should_BeProcessedInDataHandlers() { // arrange var client = new TestSocketClient(options => { @@ -67,23 +67,25 @@ namespace CryptoExchange.Net.UnitTests client.SubClient.ConnectSocketSub(sub); - sub.AddSubscription(new TestSubscription>(Mock.Of(), (messageEvent) => + var subObj = new TestSubscription>(Mock.Of(), (messageEvent) => { result = messageEvent.Data; rstEvent.Set(); - })); + }); + subObj.HandleUpdatesBeforeConfirmation = true; + sub.AddSubscription(subObj); // act - await socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}"); + socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}"); rstEvent.WaitOne(1000); // assert - Assert.IsTrue(result["property"] == "123"); + Assert.That(result["property"] == "123"); } [TestCase(false)] [TestCase(true)] - public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled) + public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled) { // arrange var client = new TestSocketClient(options => @@ -100,19 +102,21 @@ namespace CryptoExchange.Net.UnitTests string original = null; client.SubClient.ConnectSocketSub(sub); - sub.AddSubscription(new TestSubscription>(Mock.Of(), (messageEvent) => + var subObj = new TestSubscription>(Mock.Of(), (messageEvent) => { original = messageEvent.OriginalData; rstEvent.Set(); - })); + }); + subObj.HandleUpdatesBeforeConfirmation = true; + sub.AddSubscription(subObj); var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 }); // act - await socket.InvokeMessage(msgToSend); + socket.InvokeMessage(msgToSend); rstEvent.WaitOne(1000); // assert - Assert.IsTrue(original == (enabled ? msgToSend : null)); + Assert.That(original == (enabled ? msgToSend : null)); } [TestCase()] @@ -136,7 +140,7 @@ namespace CryptoExchange.Net.UnitTests client.UnsubscribeAsync(ups).Wait(); // assert - Assert.IsTrue(socket.Connected == false); + Assert.That(socket.Connected == false); } [TestCase()] @@ -164,8 +168,8 @@ namespace CryptoExchange.Net.UnitTests client.UnsubscribeAllAsync().Wait(); // assert - Assert.IsTrue(socket1.Connected == false); - Assert.IsTrue(socket2.Connected == false); + Assert.That(socket1.Connected == false); + Assert.That(socket2.Connected == false); } [TestCase()] @@ -181,7 +185,7 @@ namespace CryptoExchange.Net.UnitTests var connectResult = client.SubClient.ConnectSocketSub(sub1); // assert - Assert.IsFalse(connectResult.Success); + ClassicAssert.IsFalse(connectResult.Success); } [TestCase()] @@ -200,11 +204,11 @@ namespace CryptoExchange.Net.UnitTests // act var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); - await socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "error" })); + socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "error" })); await sub; // assert - Assert.IsFalse(client.SubClient.TestSubscription.Confirmed); + ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed); } [TestCase()] @@ -223,11 +227,11 @@ namespace CryptoExchange.Net.UnitTests // act var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); - await socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "confirmed" })); + socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "confirmed" })); await sub; // assert - Assert.IsTrue(client.SubClient.TestSubscription.Confirmed); + Assert.That(client.SubClient.TestSubscription.Confirmed); } } } diff --git a/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs b/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs index e349d72..f466e41 100644 --- a/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs +++ b/CryptoExchange.Net.UnitTests/SymbolOrderBookTests.cs @@ -8,6 +8,7 @@ using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.OrderBook; using NUnit.Framework; +using NUnit.Framework.Legacy; namespace CryptoExchange.Net.UnitTests { @@ -56,31 +57,31 @@ namespace CryptoExchange.Net.UnitTests public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry() { var symbolOrderBook = new TestableSymbolOrderBook(); - Assert.IsNotNull(symbolOrderBook.BestBid); - Assert.AreEqual(0m, symbolOrderBook.BestBid.Price); - Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity); + ClassicAssert.IsNotNull(symbolOrderBook.BestBid); + Assert.That(0m == symbolOrderBook.BestBid.Price); + Assert.That(0m == symbolOrderBook.BestAsk.Quantity); } [TestCase] public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry() { var symbolOrderBook = new TestableSymbolOrderBook(); - Assert.IsNotNull(symbolOrderBook.BestBid); - Assert.AreEqual(0m, symbolOrderBook.BestBid.Price); - Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity); + ClassicAssert.IsNotNull(symbolOrderBook.BestBid); + Assert.That(0m == symbolOrderBook.BestBid.Price); + Assert.That(0m == symbolOrderBook.BestAsk.Quantity); } [TestCase] public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries() { var symbolOrderBook = new TestableSymbolOrderBook(); - Assert.IsNotNull(symbolOrderBook.BestOffers); - Assert.IsNotNull(symbolOrderBook.BestOffers.Bid); - Assert.IsNotNull(symbolOrderBook.BestOffers.Ask); - Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price); - Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity); - Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price); - Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity); + ClassicAssert.IsNotNull(symbolOrderBook.BestOffers); + ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Bid); + ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Ask); + Assert.That(0m == symbolOrderBook.BestOffers.Bid.Price); + Assert.That(0m == symbolOrderBook.BestOffers.Bid.Quantity); + Assert.That(0m == symbolOrderBook.BestOffers.Ask.Price); + Assert.That(0m == symbolOrderBook.BestOffers.Ask.Quantity); } [TestCase] @@ -103,12 +104,12 @@ namespace CryptoExchange.Net.UnitTests var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid); var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask); - Assert.True(resultBids.Success); - Assert.True(resultAsks.Success); - Assert.AreEqual(1.05m, resultBids.Data); - Assert.AreEqual(1.25m, resultAsks.Data); - Assert.AreEqual(1.06666667m, resultBids2.Data); - Assert.AreEqual(1.23333333m, resultAsks2.Data); + Assert.That(resultBids.Success); + Assert.That(resultAsks.Success); + Assert.That(1.05m == resultBids.Data); + Assert.That(1.25m == resultAsks.Data); + Assert.That(1.06666667m == resultBids2.Data); + Assert.That(1.23333333m == resultAsks2.Data); } [TestCase] @@ -131,12 +132,12 @@ namespace CryptoExchange.Net.UnitTests var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid); var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask); - Assert.True(resultBids.Success); - Assert.True(resultAsks.Success); - Assert.AreEqual(1.9m, resultBids.Data); - Assert.AreEqual(1.61538462m, resultAsks.Data); - Assert.AreEqual(1.4m, resultBids2.Data); - Assert.AreEqual(1.23076923m, resultAsks2.Data); + Assert.That(resultBids.Success); + Assert.That(resultAsks.Success); + Assert.That(1.9m == resultBids.Data); + Assert.That(1.61538462m == resultAsks.Data); + Assert.That(1.4m == resultBids2.Data); + Assert.That(1.23076923m == resultAsks2.Data); } } } diff --git a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs new file mode 100644 index 0000000..c328126 --- /dev/null +++ b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs @@ -0,0 +1,235 @@ +using CryptoExchange.Net.Attributes; +using CryptoExchange.Net.Converters.SystemTextJson; +using System.Text.Json; +using NUnit.Framework; +using System; +using System.Text.Json.Serialization; +using NUnit.Framework.Legacy; + +namespace CryptoExchange.Net.UnitTests +{ + [TestFixture()] + public class SystemTextJsonConverterTests + { + [TestCase("2021-05-12")] + [TestCase("20210512")] + [TestCase("210512")] + [TestCase("1620777600.000")] + [TestCase("1620777600000")] + [TestCase("2021-05-12T00:00:00.000Z")] + [TestCase("2021-05-12T00:00:00.000000000Z")] + [TestCase("0.000000", true)] + [TestCase("0", true)] + [TestCase("", true)] + [TestCase(" ", true)] + public void TestDateTimeConverterString(string input, bool expectNull = false) + { + var output = JsonSerializer.Deserialize($"{{ \"time\": \"{input}\" }}"); + Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc))); + } + + [TestCase(1620777600.000)] + [TestCase(1620777600000d)] + public void TestDateTimeConverterDouble(double input) + { + var output = JsonSerializer.Deserialize($"{{ \"time\": {input} }}"); + Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + } + + [TestCase(1620777600)] + [TestCase(1620777600000)] + [TestCase(1620777600000000)] + [TestCase(1620777600000000000)] + [TestCase(0, true)] + public void TestDateTimeConverterLong(long input, bool expectNull = false) + { + var output = JsonSerializer.Deserialize($"{{ \"time\": {input} }}"); + Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc))); + } + + [TestCase(1620777600)] + [TestCase(1620777600.000)] + public void TestDateTimeConverterFromSeconds(double input) + { + var output = DateTimeConverter.ConvertFromSeconds(input); + Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + } + + [Test] + public void TestDateTimeConverterToSeconds() + { + var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output == 1620777600); + } + + [TestCase(1620777600000)] + [TestCase(1620777600000.000)] + public void TestDateTimeConverterFromMilliseconds(double input) + { + var output = DateTimeConverter.ConvertFromMilliseconds(input); + Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + } + + [Test] + public void TestDateTimeConverterToMilliseconds() + { + var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output == 1620777600000); + } + + [TestCase(1620777600000000)] + public void TestDateTimeConverterFromMicroseconds(long input) + { + var output = DateTimeConverter.ConvertFromMicroseconds(input); + Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + } + + [Test] + public void TestDateTimeConverterToMicroseconds() + { + var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output == 1620777600000000); + } + + [TestCase(1620777600000000000)] + public void TestDateTimeConverterFromNanoseconds(long input) + { + var output = DateTimeConverter.ConvertFromNanoseconds(input); + Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + } + + [Test] + public void TestDateTimeConverterToNanoseconds() + { + var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)); + Assert.That(output == 1620777600000000000); + } + + [TestCase()] + public void TestDateTimeConverterNull() + { + var output = JsonSerializer.Deserialize($"{{ \"time\": null }}"); + Assert.That(output.Time == null); + } + + [TestCase(TestEnum.One, "1")] + [TestCase(TestEnum.Two, "2")] + [TestCase(TestEnum.Three, "three")] + [TestCase(TestEnum.Four, "Four")] + [TestCase(null, null)] + public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected) + { + var output = EnumConverter.GetString(value); + Assert.That(output == expected); + } + + [TestCase(TestEnum.One, "1")] + [TestCase(TestEnum.Two, "2")] + [TestCase(TestEnum.Three, "three")] + [TestCase(TestEnum.Four, "Four")] + public void TestEnumConverterGetStringTests(TestEnum value, string expected) + { + var output = EnumConverter.GetString(value); + Assert.That(output == expected); + } + + [TestCase("1", TestEnum.One)] + [TestCase("2", TestEnum.Two)] + [TestCase("3", TestEnum.Three)] + [TestCase("three", TestEnum.Three)] + [TestCase("Four", TestEnum.Four)] + [TestCase("four", TestEnum.Four)] + [TestCase("Four1", null)] + [TestCase(null, null)] + public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected) + { + var val = value == null ? "null" : $"\"{value}\""; + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + Assert.That(output.Value == expected); + } + + [TestCase("1", TestEnum.One)] + [TestCase("2", TestEnum.Two)] + [TestCase("3", TestEnum.Three)] + [TestCase("three", TestEnum.Three)] + [TestCase("Four", TestEnum.Four)] + [TestCase("four", TestEnum.Four)] + [TestCase("Four1", TestEnum.One)] + [TestCase(null, TestEnum.One)] + public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected) + { + var val = value == null ? "null" : $"\"{value}\""; + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + Assert.That(output.Value == expected); + } + + [TestCase("1", true)] + [TestCase("true", true)] + [TestCase("yes", true)] + [TestCase("y", true)] + [TestCase("on", true)] + [TestCase("-1", false)] + [TestCase("0", false)] + [TestCase("n", false)] + [TestCase("no", false)] + [TestCase("false", false)] + [TestCase("off", false)] + [TestCase("", null)] + public void TestBoolConverter(string value, bool? expected) + { + var val = value == null ? "null" : $"\"{value}\""; + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + Assert.That(output.Value == expected); + } + + [TestCase("1", true)] + [TestCase("true", true)] + [TestCase("yes", true)] + [TestCase("y", true)] + [TestCase("on", true)] + [TestCase("-1", false)] + [TestCase("0", false)] + [TestCase("n", false)] + [TestCase("no", false)] + [TestCase("false", false)] + [TestCase("off", false)] + [TestCase("", false)] + public void TestBoolConverterNotNullable(string value, bool expected) + { + var val = value == null ? "null" : $"\"{value}\""; + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + Assert.That(output.Value == expected); + } + } + + public class STJTimeObject + { + [JsonConverter(typeof(DateTimeConverter))] + [JsonPropertyName("time")] + public DateTime? Time { get; set; } + } + + public class STJEnumObject + { + [JsonConverter(typeof(EnumConverter))] + public TestEnum? Value { get; set; } + } + + public class NotNullableSTJEnumObject + { + [JsonConverter(typeof(EnumConverter))] + public TestEnum Value { get; set; } + } + + public class STJBoolObject + { + [JsonConverter(typeof(BoolConverter))] + public bool? Value { get; set; } + } + + public class NotNullableSTJBoolObject + { + [JsonConverter(typeof(BoolConverter))] + public bool Value { get; set; } + } +} diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs index ad618e1..d0d62e3 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs @@ -32,14 +32,14 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets ListenerIdentifiers = new HashSet { channel }; } - public override Task> HandleMessageAsync(SocketConnection connection, DataEvent message) + public override CallResult HandleMessage(SocketConnection connection, DataEvent message) { if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase)) { - return Task.FromResult(new CallResult(new ServerError(message.Data.Status))); + return new CallResult(new ServerError(message.Data.Status)); } - return base.HandleMessageAsync(connection, message); + return base.HandleMessage(connection, message); } } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscription.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscription.cs index 84747cf..d5a634e 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscription.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscription.cs @@ -1,7 +1,7 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -22,11 +22,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets _handler = handler; } - public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) + public override CallResult DoHandleMessage(SocketConnection connection, DataEvent message) { var data = (T)message.Data; _handler.Invoke(message.As(data)); - return Task.FromResult(new CallResult(null)); + return new CallResult(null); } public override Type GetMessageType(IMessageAccessor message) => typeof(T); diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscriptionWithResponseCheck.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscriptionWithResponseCheck.cs index 0789a97..4a4372a 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscriptionWithResponseCheck.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestSubscriptionWithResponseCheck.cs @@ -1,7 +1,7 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using Microsoft.Extensions.Logging; using Moq; using System; @@ -24,11 +24,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets _channel = channel; } - public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) + public override CallResult DoHandleMessage(SocketConnection connection, DataEvent message) { var data = (T)message.Data; _handler.Invoke(message.As(data)); - return Task.FromResult(new CallResult(null)); + return new CallResult(null); } public override Type GetMessageType(IMessageAccessor message) => typeof(T); diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index bc53c57..6752845 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net.Http; +using System.Text; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Clients; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.UnitTests.TestImplementations; @@ -39,7 +42,17 @@ namespace CryptoExchange.Net.UnitTests { } - public CallResult Deserialize(string data) => Deserialize(data, null, null); + public CallResult Deserialize(string data) + { + var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); + var accessor = CreateAccessor(); + var valid = accessor.Read(stream, true); + if (!valid) + return new CallResult(new ServerError(data)); + + var deserializeResult = accessor.Deserialize(); + return deserializeResult; + } public override TimeSpan? GetTimeOffset() => null; public override TimeSyncInfo GetTimeSyncInfo() => null; @@ -53,7 +66,7 @@ namespace CryptoExchange.Net.UnitTests { } - public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, out SortedDictionary uriParameters, out SortedDictionary bodyParameters, out Dictionary headers) + public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat, out SortedDictionary uriParameters, out SortedDictionary bodyParameters, out Dictionary headers) { bodyParameters = new SortedDictionary(); uriParameters = new SortedDictionary(); diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index b4a4637..237c959 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -14,6 +14,7 @@ using CryptoExchange.Net.Authentication; using System.Collections.Generic; using CryptoExchange.Net.Objects.Options; using Microsoft.Extensions.Logging; +using CryptoExchange.Net.Clients; namespace CryptoExchange.Net.UnitTests.TestImplementations { @@ -182,11 +183,11 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations return await SendRequestAsync(new Uri("http://www.test.com"), HttpMethod.Get, ct); } - protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable>> responseHeaders, string data) + protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable>> responseHeaders, IMessageAccessor accessor) { - var errorData = ValidateJson(data); + var errorData = accessor.Deserialize(); - return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]); + return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage); } public override TimeSpan? GetTimeOffset() @@ -208,6 +209,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations } } + public class TestError + { + public int ErrorCode { get; set; } + public string ErrorMessage { get; set; } + } + public class ParseErrorTestRestClient: TestRestClient { public ParseErrorTestRestClient() { } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs index 82f1812..6a33a5f 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocket.cs @@ -20,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public event Func OnReconnecting; #pragma warning restore 0067 public event Func OnRequestSent; - public event Func OnStreamMessage; + public event Action> OnStreamMessage; public event Func OnError; public event Func OnOpen; public Func> GetReconnectionUrl { get; set; } @@ -111,10 +111,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations OnOpen?.Invoke(); } - public async Task InvokeMessage(string data) + public void InvokeMessage(string data) { - var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)); - await OnStreamMessage?.Invoke(WebSocketMessageType.Text, stream); + OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(data))); } public void SetProxy(ApiProxy proxy) diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs index ce5d04d..4751b63 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs @@ -3,13 +3,13 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Converters.MessageParsing; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; -using CryptoExchange.Net.Sockets.MessageParsing; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using CryptoExchange.Net.UnitTests.TestImplementations.Sockets; using Microsoft.Extensions.Logging; using Moq; diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 735defe..a0cae49 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -1,8 +1,8 @@ using System; using System.IO; using System.Security; -using System.Text; -using Newtonsoft.Json.Linq; +using CryptoExchange.Net.Converters.SystemTextJson; +using CryptoExchange.Net.Converters.MessageParsing; namespace CryptoExchange.Net.Authentication { @@ -94,38 +94,21 @@ namespace CryptoExchange.Net.Authentication /// A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'. public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null) { - using var reader = new StreamReader(inputStream, Encoding.UTF8, false, 512, true); - - var stringData = reader.ReadToEnd(); - var jsonData = stringData.ToJToken(); - if(jsonData == null) + var accessor = new SystemTextJsonStreamMessageAccessor(); + if (!accessor.Read(inputStream, false)) throw new ArgumentException("Input stream not valid json data"); - var key = TryGetValue(jsonData, identifierKey ?? "apiKey"); - var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret"); - + var key = accessor.GetValue(MessagePath.Get().Property(identifierKey ?? "apiKey")); + var secret = accessor.GetValue(MessagePath.Get().Property(identifierSecret ?? "apiSecret")); if (key == null || secret == null) throw new ArgumentException("apiKey or apiSecret value not found in Json credential file"); Key = key.ToSecureString(); - Secret = secret.ToSecureString(); + Secret = secret.ToSecureString(); inputStream.Seek(0, SeekOrigin.Begin); } - /// - /// Try get the value of a key from a JToken - /// - /// - /// - /// - protected string? TryGetValue(JToken data, string key) - { - if (data[key] == null) - return null; - return (string) data[key]!; - } - /// /// Dispose /// diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index e3517a7..5221546 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Converters; +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; @@ -47,6 +48,7 @@ namespace CryptoExchange.Net.Authentication /// If the requests should be authenticated /// Array serialization type /// The position where the providedParameters should go + /// The formatting of the request body /// Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri /// Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body /// The headers that should be send with the request @@ -58,6 +60,7 @@ namespace CryptoExchange.Net.Authentication bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, + RequestBodyFormat requestBodyFormat, out SortedDictionary uriParameters, out SortedDictionary bodyParameters, out Dictionary headers diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index 2b0846e..3e2446a 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -10,10 +10,8 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -namespace CryptoExchange.Net +namespace CryptoExchange.Net.Clients { /// /// Base API for all API clients @@ -35,37 +33,6 @@ namespace CryptoExchange.Net /// public AuthenticationProvider? AuthenticationProvider { get; private set; } - /// - /// Where to put the parameters for requests with different Http methods - /// - public Dictionary ParameterPositions { get; set; } = new Dictionary - { - { HttpMethod.Get, HttpMethodParameterPosition.InUri }, - { HttpMethod.Post, HttpMethodParameterPosition.InBody }, - { HttpMethod.Delete, HttpMethodParameterPosition.InBody }, - { HttpMethod.Put, HttpMethodParameterPosition.InBody } - }; - - /// - /// Request body content type - /// - public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json; - - /// - /// Whether or not we need to manually parse an error instead of relying on the http status code - /// - public bool manualParseError = false; - - /// - /// How to serialize array parameters when making requests - /// - public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array; - - /// - /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) - /// - public string requestBodyEmptyContent = "{}"; - /// /// The environment this client communicates to /// @@ -76,11 +43,6 @@ namespace CryptoExchange.Net /// public bool OutputOriginalData { get; } - /// - /// The default serializer - /// - protected virtual JsonSerializer DefaultSerializer { get; set; } = JsonSerializer.Create(SerializerOptions.Default); - /// /// Api options /// @@ -133,195 +95,6 @@ namespace CryptoExchange.Net } } - /// - /// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json - /// - /// The data to parse - /// - protected CallResult ValidateJson(string data) - { - if (string.IsNullOrEmpty(data)) - { - var info = "Empty data object received"; - _logger.Log(LogLevel.Error, info); - return new CallResult(new DeserializeError(info, data)); - } - - try - { - return new CallResult(JToken.Parse(data)); - } - catch (JsonReaderException jre) - { - var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}"; - return new CallResult(new DeserializeError(info, data)); - } - catch (JsonSerializationException jse) - { - var info = $"Deserialize JsonSerializationException: {jse.Message}"; - return new CallResult(new DeserializeError(info, data)); - } - catch (Exception ex) - { - var exceptionInfo = ex.ToLogString(); - var info = $"Deserialize Unknown Exception: {exceptionInfo}"; - return new CallResult(new DeserializeError(info, data)); - } - } - - /// - /// Deserialize a string into an object - /// - /// The type to deserialize into - /// The data to deserialize - /// A specific serializer to use - /// Id of the request the data is returned from (used for grouping logging by request) - /// - protected CallResult Deserialize(string data, JsonSerializer? serializer = null, int? requestId = null) - { - var tokenResult = ValidateJson(data); - if (!tokenResult) - { - _logger.Log(LogLevel.Error, tokenResult.Error!.Message); - return new CallResult(tokenResult.Error); - } - - return Deserialize(tokenResult.Data, serializer, requestId); - } - - /// - /// Deserialize a JToken into an object - /// - /// The type to deserialize into - /// The data to deserialize - /// A specific serializer to use - /// Id of the request the data is returned from (used for grouping logging by request) - /// - protected CallResult Deserialize(JToken obj, JsonSerializer? serializer = null, int? requestId = null) - { - serializer ??= DefaultSerializer; - - try - { - return new CallResult(obj.ToObject(serializer)!); - } - catch (JsonReaderException jre) - { - var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}"; - _logger.Log(LogLevel.Error, info); - return new CallResult(new DeserializeError(info, obj)); - } - catch (JsonSerializationException jse) - { - var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}"; - _logger.Log(LogLevel.Error, info); - return new CallResult(new DeserializeError(info, obj)); - } - catch (Exception ex) - { - var exceptionInfo = ex.ToLogString(); - var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}"; - _logger.Log(LogLevel.Error, info); - return new CallResult(new DeserializeError(info, obj)); - } - } - - /// - /// Deserialize a stream into an object - /// - /// The type to deserialize into - /// The stream to deserialize - /// A specific serializer to use - /// Id of the request the data is returned from (used for grouping logging by request) - /// Milliseconds response time for the request this stream is a response for - /// - protected async Task> DeserializeAsync(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null) - { - serializer ??= DefaultSerializer; - string? data = null; - - try - { - // Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream. - using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); - // If we have to output the original json data or output the data into the logging we'll have to read to full response - // in order to log/return the json data - if (OutputOriginalData == true) - { - data = await reader.ReadToEndAsync().ConfigureAwait(false); - var result = Deserialize(data, serializer, requestId); - result.OriginalData = data; - return result; - } - - // If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly - // into the desired object, which has increased performance over first reading the string value into memory and deserializing from that - using var jsonReader = new JsonTextReader(reader); - return new CallResult(serializer.Deserialize(jsonReader)!); - } - catch (JsonReaderException jre) - { - if (data == null) - { - if (stream.CanSeek) - { - // If we can seek the stream rewind it so we can retrieve the original data that was sent - stream.Seek(0, SeekOrigin.Begin); - data = await ReadStreamAsync(stream).ConfigureAwait(false); - } - else - { - data = "[Data only available in Trace LogLevel]"; - } - } - _logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}"); - return new CallResult(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data)); - } - catch (JsonSerializationException jse) - { - if (data == null) - { - if (stream.CanSeek) - { - stream.Seek(0, SeekOrigin.Begin); - data = await ReadStreamAsync(stream).ConfigureAwait(false); - } - else - { - data = "[Data only available in Trace LogLevel]"; - } - } - - _logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}"); - return new CallResult(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data)); - } - catch (Exception ex) - { - if (data == null) - { - if (stream.CanSeek) - { - stream.Seek(0, SeekOrigin.Begin); - data = await ReadStreamAsync(stream).ConfigureAwait(false); - } - else - { - data = "[Data only available in Trace LogLevel]"; - } - } - - var exceptionInfo = ex.ToLogString(); - _logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}"); - return new CallResult(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data)); - } - } - - private static async Task ReadStreamAsync(Stream stream) - { - using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true); - return await reader.ReadToEndAsync().ConfigureAwait(false); - } - /// /// Dispose /// diff --git a/CryptoExchange.Net/Clients/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs index 6a2c280..6455958 100644 --- a/CryptoExchange.Net/Clients/BaseClient.cs +++ b/CryptoExchange.Net/Clients/BaseClient.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; -namespace CryptoExchange.Net +namespace CryptoExchange.Net.Clients { /// /// The base for all clients, websocket client and rest client @@ -15,7 +15,7 @@ namespace CryptoExchange.Net /// /// The name of the API the client is for /// - internal string Name { get; } + public string Exchange { get; } /// /// Api clients in this client @@ -26,7 +26,7 @@ namespace CryptoExchange.Net /// The log object /// protected internal ILogger _logger; - + /// /// Provided client options /// @@ -36,14 +36,14 @@ namespace CryptoExchange.Net /// ctor /// /// Logger - /// The name of the API this client is for + /// The name of the exchange this client is for #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. - protected BaseClient(ILoggerFactory? logger, string name) + protected BaseClient(ILoggerFactory? logger, string exchange) #pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. { - _logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name); + _logger = logger?.CreateLogger(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange); - Name = name; + Exchange = exchange; } /// @@ -57,7 +57,7 @@ namespace CryptoExchange.Net throw new ArgumentNullException(nameof(options)); ClientOptions = options; - _logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}"); + _logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Exchange}.Net: v{GetType().Assembly.GetName().Version}"); } /// @@ -74,7 +74,7 @@ namespace CryptoExchange.Net /// Register an API client /// /// The client - protected T AddApiClient(T apiClient) where T: BaseApiClient + protected T AddApiClient(T apiClient) where T : BaseApiClient { if (ClientOptions == null) throw new InvalidOperationException("Client should have called Initialize before adding API clients"); diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs index 9e735a2..6ff8b03 100644 --- a/CryptoExchange.Net/Clients/BaseRestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -2,7 +2,7 @@ using System.Linq; using CryptoExchange.Net.Interfaces; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net +namespace CryptoExchange.Net.Clients { /// /// Base rest client diff --git a/CryptoExchange.Net/Clients/BaseSocketClient.cs b/CryptoExchange.Net/Clients/BaseSocketClient.cs index e817282..0186956 100644 --- a/CryptoExchange.Net/Clients/BaseSocketClient.cs +++ b/CryptoExchange.Net/Clients/BaseSocketClient.cs @@ -7,20 +7,20 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; -namespace CryptoExchange.Net +namespace CryptoExchange.Net.Clients { /// /// Base for socket client implementations /// - public abstract class BaseSocketClient: BaseClient, ISocketClient + public abstract class BaseSocketClient : BaseClient, ISocketClient { #region fields - + /// /// If client is disposing /// protected bool _disposing; - + /// public int CurrentConnections => ApiClients.OfType().Sum(c => c.CurrentConnections); /// @@ -33,8 +33,8 @@ namespace CryptoExchange.Net /// ctor /// /// Logger - /// The name of the API this client is for - protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name) + /// The name of the exchange this client is for + protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange) { } @@ -45,11 +45,11 @@ namespace CryptoExchange.Net /// public virtual async Task UnsubscribeAsync(int subscriptionId) { - foreach(var socket in ApiClients.OfType()) + foreach (var socket in ApiClients.OfType()) { var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false); if (result) - break; + break; } } @@ -73,10 +73,10 @@ namespace CryptoExchange.Net /// public virtual async Task UnsubscribeAllAsync() { - var tasks = new List(); + var tasks = new List(); foreach (var client in ApiClients.OfType()) tasks.Add(client.UnsubscribeAllAsync()); - + await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); } diff --git a/CryptoExchange.Net/Clients/CryptoRestClient.cs b/CryptoExchange.Net/Clients/CryptoRestClient.cs index e5bc182..c1255aa 100644 --- a/CryptoExchange.Net/Clients/CryptoRestClient.cs +++ b/CryptoExchange.Net/Clients/CryptoRestClient.cs @@ -42,6 +42,6 @@ namespace CryptoExchange.Net.Clients /// /// /// - public ISpotClient? SpotClient(string exchangeName) => _serviceProvider.GetServices()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase)); + public ISpotClient? SpotClient(string exchangeName) => _serviceProvider?.GetServices()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase)); } } diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index bf80113..601c4ae 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -8,15 +8,14 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using CryptoExchange.Net.Converters.JsonNet; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Requests; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -namespace CryptoExchange.Net +namespace CryptoExchange.Net.Clients { /// /// Base rest API client for interacting with a REST API @@ -35,6 +34,21 @@ namespace CryptoExchange.Net /// public int TotalRequestsMade { get; set; } + /// + /// Request body content type + /// + protected RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json; + + /// + /// How to serialize array parameters when making requests + /// + protected ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array; + + /// + /// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody) + /// + protected string RequestBodyEmptyContent = "{}"; + /// /// Request headers to be sent with each request /// @@ -45,12 +59,24 @@ namespace CryptoExchange.Net /// internal IEnumerable RateLimiters { get; } + /// + /// Where to put the parameters for requests with different Http methods + /// + public Dictionary ParameterPositions { get; set; } = new Dictionary + { + { HttpMethod.Get, HttpMethodParameterPosition.InUri }, + { HttpMethod.Post, HttpMethodParameterPosition.InBody }, + { HttpMethod.Delete, HttpMethodParameterPosition.InBody }, + { HttpMethod.Put, HttpMethodParameterPosition.InBody } + }; + /// public new RestExchangeOptions ClientOptions => (RestExchangeOptions)base.ClientOptions; /// public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions; + /// /// ctor /// @@ -59,9 +85,9 @@ namespace CryptoExchange.Net /// Base address for this API client /// The base client options /// The Api client options - public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) - : base(logger, - apiOptions.OutputOriginalData ?? options.OutputOriginalData, + public RestApiClient(ILogger logger, HttpClient? httpClient, string baseAddress, RestExchangeOptions options, RestApiOptions apiOptions) + : base(logger, + apiOptions.OutputOriginalData ?? options.OutputOriginalData, apiOptions.ApiCredentials ?? options.ApiCredentials, baseAddress, options, @@ -75,6 +101,18 @@ namespace CryptoExchange.Net RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient); } + /// + /// Create a message accessor instance + /// + /// + protected virtual IStreamMessageAccessor CreateAccessor() => new JsonNetStreamMessageAccessor(); + + /// + /// Create a serializer instance + /// + /// + protected virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer(); + /// /// Execute a request to the uri and returns if it was successful /// @@ -87,7 +125,6 @@ namespace CryptoExchange.Net /// Where the parameters should be placed, overwrites the value set in the client /// How array parameters should be serialized, overwrites the value set in the client /// Credits used for the request - /// The JsonSerializer to use for deserialization /// Additional headers to send with the request /// Ignore rate limits for this request /// @@ -102,7 +139,6 @@ namespace CryptoExchange.Net HttpMethodParameterPosition? parameterPosition = null, ArrayParametersSerialization? arraySerialization = null, int requestWeight = 1, - JsonSerializer? deserializer = null, Dictionary? additionalHeaders = null, bool ignoreRatelimit = false) { @@ -110,15 +146,15 @@ namespace CryptoExchange.Net while (true) { currentTry++; - var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false); + var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, ignoreRatelimit).ConfigureAwait(false); if (!request) return new WebCallResult(request.Error!); - var result = await GetResponseAsync(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false); + var result = await GetResponseAsync(request.Data, cancellationToken).ConfigureAwait(false); if (!result) _logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] {result.ResponseStatusCode} Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}"); else - _logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}"); + _logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? ": " + result.OriginalData : "")}"); if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false)) continue; @@ -140,7 +176,6 @@ namespace CryptoExchange.Net /// Where the parameters should be placed, overwrites the value set in the client /// How array parameters should be serialized, overwrites the value set in the client /// Credits used for the request - /// The JsonSerializer to use for deserialization /// Additional headers to send with the request /// Ignore rate limits for this request /// @@ -155,7 +190,6 @@ namespace CryptoExchange.Net HttpMethodParameterPosition? parameterPosition = null, ArrayParametersSerialization? arraySerialization = null, int requestWeight = 1, - JsonSerializer? deserializer = null, Dictionary? additionalHeaders = null, bool ignoreRatelimit = false ) where T : class @@ -164,15 +198,15 @@ namespace CryptoExchange.Net while (true) { currentTry++; - var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false); + var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, ignoreRatelimit).ConfigureAwait(false); if (!request) return new WebCallResult(request.Error!); - var result = await GetResponseAsync(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false); + var result = await GetResponseAsync(request.Data, cancellationToken).ConfigureAwait(false); if (!result) _logger.Log(LogLevel.Warning, $"[Req {result.RequestId}] {result.ResponseStatusCode} Error received in {result.ResponseTime!.Value.TotalMilliseconds}ms: {result.Error}"); else - _logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? (": " + result.OriginalData) : "")}"); + _logger.Log(LogLevel.Debug, $"[Req {result.RequestId}] {result.ResponseStatusCode} Response received in {result.ResponseTime!.Value.TotalMilliseconds}ms{(OutputOriginalData ? ": " + result.OriginalData : "")}"); if (await ShouldRetryRequestAsync(result, currentTry).ConfigureAwait(false)) continue; @@ -193,7 +227,6 @@ namespace CryptoExchange.Net /// Where the parameters should be placed, overwrites the value set in the client /// How array parameters should be serialized, overwrites the value set in the client /// Credits used for the request - /// The JsonSerializer to use for deserialization /// Additional headers to send with the request /// Ignore rate limits for this request /// @@ -207,7 +240,6 @@ namespace CryptoExchange.Net HttpMethodParameterPosition? parameterPosition = null, ArrayParametersSerialization? arraySerialization = null, int requestWeight = 1, - JsonSerializer? deserializer = null, Dictionary? additionalHeaders = null, bool ignoreRatelimit = false) { @@ -248,7 +280,7 @@ namespace CryptoExchange.Net _logger.Log(LogLevel.Information, $"[Req {requestId}] Creating request for " + uri); var paramsPosition = parameterPosition ?? ParameterPositions[method]; - var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestBodyFormat ?? this.requestBodyFormat, requestId, additionalHeaders); + var request = ConstructRequest(uri, method, parameters?.OrderBy(p => p.Key).ToDictionary(p => p.Key, p => p.Value), signed, paramsPosition, arraySerialization ?? ArraySerialization, requestBodyFormat ?? RequestBodyFormat, requestId, additionalHeaders); string? paramString = ""; if (paramsPosition == HttpMethodParameterPosition.InBody) @@ -267,109 +299,64 @@ namespace CryptoExchange.Net /// Executes the request and returns the result deserialized into the type parameter class /// /// The request object to execute - /// The JsonSerializer to use for deserialization /// Cancellation token - /// If an empty response is expected /// protected virtual async Task> GetResponseAsync( IRequest request, - JsonSerializer? deserializer, - CancellationToken cancellationToken, - bool expectedEmptyResponse) + CancellationToken cancellationToken) { var sw = Stopwatch.StartNew(); + Stream? responseStream = null; + IResponse? response = null; + IStreamMessageAccessor? accessor = null; try { - var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); + response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); sw.Stop(); var statusCode = response.StatusCode; var headers = response.ResponseHeaders; var responseLength = response.ContentLength; - var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); - if (response.IsSuccessStatusCode) + responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false); + var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; + + accessor = CreateAccessor(); + if (!response.IsSuccessStatusCode) { - // If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full - // response before being able to deserialize it into the resulting type since we don't know if its an error response or data - if (manualParseError) - { - using var reader = new StreamReader(responseStream); - var data = await reader.ReadToEndAsync().ConfigureAwait(false); - responseLength ??= data.Length; - responseStream.Close(); - response.Close(); - - if (!expectedEmptyResponse) - { - // Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example - var parseResult = ValidateJson(data); - if (!parseResult.Success) - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!); - - // Let the library implementation see if it is an error response, and if so parse the error - var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false); - if (error != null) - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); - - // Not an error, so continue deserializing - var deserializeResult = Deserialize(parseResult.Data, deserializer, request.RequestId); - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error); - } - else - { - if (!string.IsNullOrEmpty(data)) - { - var parseResult = ValidateJson(data); - if (!parseResult.Success) - // Not empty, and not json - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!); - - var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false); - if (error != null) - // Error response - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); - } - - // Empty success response; okay - return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? data : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default); - } - } - else - { - if (expectedEmptyResponse) - { - // We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct - responseStream.Close(); - response.Close(); - - return new WebCallResult(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null); - } - - // Success status code, and we don't have to check for errors. Continue deserializing directly from the stream - var desResult = await DeserializeAsync(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false); - responseStream.Close(); - response.Close(); - - return new WebCallResult(statusCode, headers, sw.Elapsed, responseLength, OutputOriginalData ? desResult.OriginalData : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error); - } - } - else - { - // Http status code indicates error - using var reader = new StreamReader(responseStream); - var data = await reader.ReadToEndAsync().ConfigureAwait(false); - responseStream.Close(); - response.Close(); + // Error response + accessor.Read(responseStream, true); Error error; if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) - error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, data); + error = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor); else - error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, data); + error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor); if (error.Code == null || error.Code == 0) error.Code = (int)response.StatusCode; - return new WebCallResult(statusCode, headers, sw.Elapsed, data.Length, data, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error); + + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!); } + + if (typeof(T) == typeof(object)) + // Success status code and expected empty response, assume it's correct + return new WebCallResult(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null); + + var valid = accessor.Read(responseStream, outputOriginalData); + if (!valid) + { + // Invalid json + var error = new ServerError(accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error); + } + + // Json response received + var parsedError = TryParseError(accessor); + if (parsedError != null) + // Success status code, but TryParseError determined it was an error response + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parsedError); + + var deserializeResult = accessor.Deserialize(); + return new WebCallResult(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error); } catch (HttpRequestException requestException) { @@ -390,6 +377,12 @@ namespace CryptoExchange.Net return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out")); } } + finally + { + accessor?.Clear(); + responseStream?.Close(); + response?.Close(); + } } /// @@ -397,12 +390,9 @@ namespace CryptoExchange.Net /// When setting manualParseError to true this method will be called for each response to be able to check if the response is an error or not. /// If the response is an error this method should return the parsed error, else it should return null /// - /// Received data + /// Data accessor /// Null if not an error, Error otherwise - protected virtual Task TryParseErrorAsync(JToken data) - { - return Task.FromResult(null); - } + protected virtual ServerError? TryParseError(IMessageAccessor accessor) => null; /// /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. @@ -468,6 +458,7 @@ namespace CryptoExchange.Net signed, arraySerialization, parameterPosition, + bodyFormat, out uriParameters, out bodyParameters, out headers); @@ -519,7 +510,7 @@ namespace CryptoExchange.Net if (bodyParameters.Any()) WriteParamBody(request, bodyParameters, contentType); else - request.SetContent(requestBodyEmptyContent, contentType); + request.SetContent(RequestBodyEmptyContent, contentType); } return request; @@ -536,7 +527,7 @@ namespace CryptoExchange.Net if (contentType == Constants.JsonContentHeader) { // Write the parameters as json in the body - var stringData = JsonConvert.SerializeObject(parameters); + var stringData = CreateSerializer().Serialize(parameters); request.SetContent(stringData, contentType); } else if (contentType == Constants.FormContentHeader) @@ -552,35 +543,38 @@ namespace CryptoExchange.Net /// /// The response status code /// The response headers - /// The response data + /// Data accessor /// - protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable>> responseHeaders, string data) + protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable>> responseHeaders, IMessageAccessor accessor) { - return new ServerError(data); + var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]"; + return new ServerError(message); } - + /// /// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418 /// /// The response status code /// The response headers - /// The response data + /// Data accessor /// - protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable>> responseHeaders, string data) + protected virtual Error ParseRateLimitResponse(int httpStatusCode, IEnumerable>> responseHeaders, IMessageAccessor accessor) { + var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]"; + // Handle retry after header var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase)); if (retryAfterHeader.Value?.Any() != true) - return new ServerRateLimitError(data); + return new ServerRateLimitError(message); var value = retryAfterHeader.Value.First(); if (int.TryParse(value, out var seconds)) - return new ServerRateLimitError(data) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; + return new ServerRateLimitError(message) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; if (DateTime.TryParse(value, out var datetime)) - return new ServerRateLimitError(data) { RetryAfter = datetime }; + return new ServerRateLimitError(message) { RetryAfter = datetime }; - return new ServerRateLimitError(data); + return new ServerRateLimitError(message); } /// @@ -597,7 +591,7 @@ namespace CryptoExchange.Net if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false)) { - if (!timeSyncParams.SyncTime || (DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)) + if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval) { timeSyncParams.TimeSyncState.Semaphore.Release(); return new WebCallResult(null, null, null, null, null, null, null, null, null, null, true, null); @@ -624,7 +618,7 @@ namespace CryptoExchange.Net } // Calculate time offset between local and server - var offset = result.Data - (localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2)); + var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2); timeSyncParams.UpdateTimeOffset(offset); timeSyncParams.TimeSyncState.Semaphore.Release(); } diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index 982fcdf..a0928ad 100644 --- a/CryptoExchange.Net/Clients/SocketApiClient.cs +++ b/CryptoExchange.Net/Clients/SocketApiClient.cs @@ -1,9 +1,9 @@ +using CryptoExchange.Net.Converters.JsonNet; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; @@ -17,7 +17,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace CryptoExchange.Net +namespace CryptoExchange.Net.Clients { /// /// Base socket API client for interaction with a websocket API @@ -63,6 +63,11 @@ namespace CryptoExchange.Net /// protected internal IEnumerable? RateLimiters { get; set; } + /// + /// The max size a websocket message size can be + /// + protected internal int? MessageSendSizeLimit { get; set; } + /// /// Periodic task regisrations /// @@ -110,8 +115,8 @@ namespace CryptoExchange.Net /// Client options /// Base address for this API client /// The Api client options - public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) - : base(logger, + public SocketApiClient(ILogger logger, string baseAddress, SocketExchangeOptions options, SocketApiOptions apiOptions) + : base(logger, apiOptions.OutputOriginalData ?? options.OutputOriginalData, apiOptions.ApiCredentials ?? options.ApiCredentials, baseAddress, @@ -124,6 +129,18 @@ namespace CryptoExchange.Net RateLimiters = rateLimiters; } + /// + /// Create a message accessor instance + /// + /// + protected internal virtual IByteMessageAccessor CreateAccessor() => new JsonNetByteMessageAccessor(); + + /// + /// Create a serializer instance + /// + /// + protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer(); + /// /// Add a query to periodically send on each connection /// @@ -191,9 +208,10 @@ namespace CryptoExchange.Net return socketResult.As(null); socketConnection = socketResult.Data; + subscription.HandleUpdatesBeforeConfirmation = HandleMessageBeforeConfirmation; // Add a subscription on the socket connection - var success = socketConnection.CanAddSubscription(); + var success = socketConnection.AddSubscription(subscription); if (!success) { _logger.Log(LogLevel.Trace, $"[Sckt {socketConnection.SocketId}] failed to add subscription, retrying on different connection"); @@ -228,23 +246,19 @@ namespace CryptoExchange.Net return new CallResult(new ServerError("Socket is paused")); } - var waitEvent = new AsyncResetEvent(false); + var waitEvent = new ManualResetEvent(false); var subQuery = subscription.GetSubQuery(socketConnection); if (subQuery != null) { - if (HandleMessageBeforeConfirmation) - socketConnection.AddSubscription(subscription); - // Send the request and wait for answer var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent).ConfigureAwait(false); if (!subResult) { waitEvent?.Set(); - _logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] failed to subscribe: {subResult.Error}"); + _logger.Log(LogLevel.Warning, $"[Sckt {socketConnection.SocketId}] failed to subscribe: {subResult.Error}"); // If this was a timeout we still need to send an unsubscribe to prevent messages coming in later var unsubscribe = subResult.Error is CancellationRequestedError; await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false); - return new CallResult(subResult.Error!); } @@ -261,9 +275,6 @@ namespace CryptoExchange.Net }, false); } - if (!HandleMessageBeforeConfirmation) - socketConnection.AddSubscription(subscription); - waitEvent?.Set(); _logger.Log(LogLevel.Information, $"[Sckt {socketConnection.SocketId}] subscription {subscription.Id} completed successfully"); return new CallResult(new UpdateSubscription(socketConnection, subscription)); @@ -443,16 +454,14 @@ namespace CryptoExchange.Net { var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected) && s.Value.Tag.TrimEnd('/') == address.TrimEnd('/') - && (s.Value.ApiClient.GetType() == GetType()) + && s.Value.ApiClient.GetType() == GetType() && (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault(); var result = socketResult.Equals(default(KeyValuePair)) ? null : socketResult.Value; if (result != null) { - if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))) - { + if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)) // Use existing socket if it has less than target connections OR it has the least connections and we can't make new return new CallResult(result); - } } var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); @@ -587,7 +596,7 @@ namespace CryptoExchange.Net { var socketList = socketConnections.Values; foreach (var sub in socketList) - tasks.Add(sub.CloseAsync()); + tasks.Add(sub.CloseAsync()); } await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); @@ -613,7 +622,7 @@ namespace CryptoExchange.Net /// /// Log the current state of connections and subscriptions /// - public string GetSubscriptionsState() + public string GetSubscriptionsState(bool includeSubDetails = true) { var sb = new StringBuilder(); sb.AppendLine($"{GetType().Name}"); @@ -629,12 +638,15 @@ namespace CryptoExchange.Net sb.AppendLine($" Authenticated: {connection.Value.Authenticated}"); sb.AppendLine($" Download speed: {connection.Value.IncomingKbps} kbps"); sb.AppendLine($" Subscriptions:"); - foreach (var subscription in connection.Value.Subscriptions) + if (includeSubDetails) { - sb.AppendLine($" Id: {subscription.Id}"); - sb.AppendLine($" Confirmed: {subscription.Confirmed}"); - sb.AppendLine($" Invocations: {subscription.TotalInvocations}"); - sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]"); + foreach (var subscription in connection.Value.Subscriptions) + { + sb.AppendLine($" Id: {subscription.Id}"); + sb.AppendLine($" Confirmed: {subscription.Confirmed}"); + sb.AppendLine($" Invocations: {subscription.TotalInvocations}"); + sb.AppendLine($" Identifiers: [{string.Join(", ", subscription.ListenerIdentifiers)}]"); + } } } return sb.ToString(); @@ -666,8 +678,8 @@ namespace CryptoExchange.Net /// Preprocess a stream message /// /// - /// + /// /// - public virtual Stream PreprocessStreamMessage(WebSocketMessageType type, Stream stream) => stream; + public virtual ReadOnlyMemory PreprocessStreamMessage(WebSocketMessageType type, ReadOnlyMemory data) => data; } } diff --git a/CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs b/CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs new file mode 100644 index 0000000..29bd7b5 --- /dev/null +++ b/CryptoExchange.Net/Converters/ArrayPropertyAttribute.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Converters +{ + /// + /// Mark property as an index in the array + /// + [AttributeUsage(AttributeTargets.Property)] + public class ArrayPropertyAttribute : Attribute + { + /// + /// The index in the array + /// + public int Index { get; } + + /// + /// ctor + /// + /// + public ArrayPropertyAttribute(int index) + { + Index = index; + } + } +} diff --git a/CryptoExchange.Net/Converters/ArrayConverter.cs b/CryptoExchange.Net/Converters/JsonNet/ArrayConverter.cs similarity index 93% rename from CryptoExchange.Net/Converters/ArrayConverter.cs rename to CryptoExchange.Net/Converters/JsonNet/ArrayConverter.cs index 3ab5c0a..6a286da 100644 --- a/CryptoExchange.Net/Converters/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/JsonNet/ArrayConverter.cs @@ -8,7 +8,7 @@ using CryptoExchange.Net.Attributes; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters.JsonNet { /// /// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties @@ -192,25 +192,4 @@ namespace CryptoExchange.Net.Converters private static T? GetCustomAttribute(Type type) where T : Attribute => (T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))); } - - /// - /// Mark property as an index in the array - /// - [AttributeUsage(AttributeTargets.Property)] - public class ArrayPropertyAttribute: Attribute - { - /// - /// The index in the array - /// - public int Index { get; } - - /// - /// ctor - /// - /// - public ArrayPropertyAttribute(int index) - { - Index = index; - } - } } diff --git a/CryptoExchange.Net/Converters/BaseConverter.cs b/CryptoExchange.Net/Converters/JsonNet/BaseConverter.cs similarity index 98% rename from CryptoExchange.Net/Converters/BaseConverter.cs rename to CryptoExchange.Net/Converters/JsonNet/BaseConverter.cs index b34d1dc..9422d62 100644 --- a/CryptoExchange.Net/Converters/BaseConverter.cs +++ b/CryptoExchange.Net/Converters/JsonNet/BaseConverter.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Linq; using Newtonsoft.Json; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters.JsonNet { /// /// Base class for enum converters diff --git a/CryptoExchange.Net/Converters/BoolConverter.cs b/CryptoExchange.Net/Converters/JsonNet/BoolConverter.cs similarity index 98% rename from CryptoExchange.Net/Converters/BoolConverter.cs rename to CryptoExchange.Net/Converters/JsonNet/BoolConverter.cs index 40476eb..4b60ee2 100644 --- a/CryptoExchange.Net/Converters/BoolConverter.cs +++ b/CryptoExchange.Net/Converters/JsonNet/BoolConverter.cs @@ -1,7 +1,7 @@ using System; using Newtonsoft.Json; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters.JsonNet { /// /// Boolean converter with support for "0"/"1" (strings) diff --git a/CryptoExchange.Net/Converters/DateTimeConverter.cs b/CryptoExchange.Net/Converters/JsonNet/DateTimeConverter.cs similarity index 99% rename from CryptoExchange.Net/Converters/DateTimeConverter.cs rename to CryptoExchange.Net/Converters/JsonNet/DateTimeConverter.cs index e3f2a1d..b403815 100644 --- a/CryptoExchange.Net/Converters/DateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/JsonNet/DateTimeConverter.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters.JsonNet { /// /// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01. diff --git a/CryptoExchange.Net/Converters/DecimalStringWriterConverter.cs b/CryptoExchange.Net/Converters/JsonNet/DecimalStringWriterConverter.cs similarity index 94% rename from CryptoExchange.Net/Converters/DecimalStringWriterConverter.cs rename to CryptoExchange.Net/Converters/JsonNet/DecimalStringWriterConverter.cs index 4053372..694b3a9 100644 --- a/CryptoExchange.Net/Converters/DecimalStringWriterConverter.cs +++ b/CryptoExchange.Net/Converters/JsonNet/DecimalStringWriterConverter.cs @@ -2,7 +2,7 @@ using System; using System.Globalization; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters.JsonNet { /// /// Converter for serializing decimal values as string diff --git a/CryptoExchange.Net/Converters/EnumConverter.cs b/CryptoExchange.Net/Converters/JsonNet/EnumConverter.cs similarity index 99% rename from CryptoExchange.Net/Converters/EnumConverter.cs rename to CryptoExchange.Net/Converters/JsonNet/EnumConverter.cs index 5c65abc..edd97a0 100644 --- a/CryptoExchange.Net/Converters/EnumConverter.cs +++ b/CryptoExchange.Net/Converters/JsonNet/EnumConverter.cs @@ -7,7 +7,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters.JsonNet { /// /// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value diff --git a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs new file mode 100644 index 0000000..0c6d749 --- /dev/null +++ b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs @@ -0,0 +1,318 @@ +using CryptoExchange.Net.Converters.MessageParsing; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace CryptoExchange.Net.Converters.JsonNet +{ + /// + /// Json.Net message accessor + /// + public abstract class JsonNetMessageAccessor : IMessageAccessor + { + /// + /// The json token loaded + /// + protected JToken? _token; + private static JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters); + + /// + public bool IsJson { get; protected set; } + + /// + public abstract bool OriginalDataAvailable { get; } + + /// + public object? Underlying => _token; + + /// + public CallResult Deserialize(Type type, MessagePath? path = null) + { + if (!IsJson) + return new CallResult(GetOriginalString()); + + var source = _token; + if (path != null) + source = GetPathNode(path.Value); + + try + { + var result = source!.ToObject(type, _serializer)!; + return new CallResult(result); + } + catch (JsonReaderException jre) + { + var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } + catch (JsonSerializationException jse) + { + var info = $"Deserialize JsonSerializationException: {jse.Message}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } + catch (Exception ex) + { + var exceptionInfo = ex.ToLogString(); + var info = $"Deserialize Unknown Exception: {exceptionInfo}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } + } + + /// + public CallResult Deserialize(MessagePath? path = null) + { + var source = _token; + if (path != null) + source = GetPathNode(path.Value); + + try + { + var result = source!.ToObject(_serializer)!; + return new CallResult(result); + } + catch (JsonReaderException jre) + { + var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } + catch (JsonSerializationException jse) + { + var info = $"Deserialize JsonSerializationException: {jse.Message}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } + catch (Exception ex) + { + var exceptionInfo = ex.ToLogString(); + var info = $"Deserialize Unknown Exception: {exceptionInfo}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } + } + + /// + public NodeType? GetNodeType() + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + if (_token == null) + return null; + + if (_token.Type == JTokenType.Object) + return NodeType.Object; + + if (_token.Type == JTokenType.Array) + return NodeType.Array; + + return NodeType.Value; + } + + /// + public NodeType? GetNodeType(MessagePath path) + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var node = GetPathNode(path); + if (node == null) + return null; + + if (node.Type == JTokenType.Object) + return NodeType.Object; + + if (node.Type == JTokenType.Array) + return NodeType.Array; + + return NodeType.Value; + } + + /// + public T? GetValue(MessagePath path) + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var value = GetPathNode(path); + if (value == null) + return default; + + if (value.Type == JTokenType.Object || value.Type == JTokenType.Array) + return default; + + return value!.Value(); + } + + /// + public List? GetValues(MessagePath path) + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var value = GetPathNode(path); + if (value == null) + return default; + + if (value.Type == JTokenType.Object) + return default; + + return value!.Values().ToList(); + } + + private JToken? GetPathNode(MessagePath path) + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var currentToken = _token; + foreach (var node in path) + { + if (node.Type == 0) + { + // Int value + var val = (int)node.Value!; + if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val) + return null; + + currentToken = currentToken[val]; + } + else if (node.Type == 1) + { + // String value + if (currentToken!.Type != JTokenType.Object) + return null; + + currentToken = currentToken[(string)node.Value!]; + } + else + { + // Property name + if (currentToken!.Type != JTokenType.Object) + return null; + + currentToken = (currentToken.First as JProperty)?.Name; + } + + if (currentToken == null) + return null; + } + + return currentToken; + } + + /// + public abstract string GetOriginalString(); + + /// + public abstract void Clear(); + } + + /// + /// Json.Net stream message accessor + /// + public class JsonNetStreamMessageAccessor : JsonNetMessageAccessor, IStreamMessageAccessor + { + private Stream? _stream; + + /// + public override bool OriginalDataAvailable => _stream?.CanSeek == true; + + /// + public bool Read(Stream stream, bool bufferStream) + { + if (bufferStream && stream is not MemoryStream) + { + _stream = new MemoryStream(); + stream.CopyTo(_stream); + _stream.Position = 0; + } + else + { + _stream = stream; + } + + var length = _stream.CanSeek ? _stream.Length : 4096; + using var reader = new StreamReader(_stream, Encoding.UTF8, false, (int)Math.Max(2, length), true); + using var jsonTextReader = new JsonTextReader(reader); + + try + { + _token = JToken.Load(jsonTextReader); + IsJson = true; + } + catch (Exception) + { + // Not a json message + IsJson = false; + } + + return IsJson; + } + /// + public override string GetOriginalString() + { + if (_stream is null) + throw new NullReferenceException("Stream not initialized"); + + _stream.Position = 0; + using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true); + return textReader.ReadToEnd(); + } + + /// + public override void Clear() + { + _stream?.Dispose(); + _stream = null; + _token = null; + } + + } + + /// + /// Json.Net byte message accessor + /// + public class JsonNetByteMessageAccessor : JsonNetMessageAccessor, IByteMessageAccessor + { + private ReadOnlyMemory _bytes; + + /// + public bool Read(ReadOnlyMemory data) + { + _bytes = data; + using var stream = new MemoryStream(data.ToArray()); + using var reader = new StreamReader(stream, Encoding.UTF8, false, (int)Math.Max(2, data.Length), true); + using var jsonTextReader = new JsonTextReader(reader); + + try + { + _token = JToken.Load(jsonTextReader); + IsJson = true; + } + catch (Exception) + { + // Not a json message + IsJson = false; + } + + return IsJson; + } + + /// + public override string GetOriginalString() => Encoding.UTF8.GetString(_bytes.ToArray()); + + /// + public override bool OriginalDataAvailable => true; + + /// + public override void Clear() + { + _bytes = null; + _token = null; + } + } +} diff --git a/CryptoExchange.Net/Sockets/MessageParsing/JsonNetSerializer.cs b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs similarity index 53% rename from CryptoExchange.Net/Sockets/MessageParsing/JsonNetSerializer.cs rename to CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs index f501cd8..79ec7f5 100644 --- a/CryptoExchange.Net/Sockets/MessageParsing/JsonNetSerializer.cs +++ b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs @@ -1,10 +1,10 @@ -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; +using CryptoExchange.Net.Interfaces; using Newtonsoft.Json; -namespace CryptoExchange.Net.Sockets.MessageParsing +namespace CryptoExchange.Net.Converters.JsonNet { /// - public class JsonNetSerializer : IMessageSerializer + public class JsonNetMessageSerializer : IMessageSerializer { /// public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None); diff --git a/CryptoExchange.Net/Converters/SerializerOptions.cs b/CryptoExchange.Net/Converters/JsonNet/SerializerOptions.cs similarity index 95% rename from CryptoExchange.Net/Converters/SerializerOptions.cs rename to CryptoExchange.Net/Converters/JsonNet/SerializerOptions.cs index 2e71a5d..96756e8 100644 --- a/CryptoExchange.Net/Converters/SerializerOptions.cs +++ b/CryptoExchange.Net/Converters/JsonNet/SerializerOptions.cs @@ -1,7 +1,7 @@ using Newtonsoft.Json; using System.Globalization; -namespace CryptoExchange.Net.Converters +namespace CryptoExchange.Net.Converters.JsonNet { /// /// Serializer options diff --git a/CryptoExchange.Net/Sockets/MessageParsing/MessageNode.cs b/CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs similarity index 95% rename from CryptoExchange.Net/Sockets/MessageParsing/MessageNode.cs rename to CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs index ecbc00a..01b23bf 100644 --- a/CryptoExchange.Net/Sockets/MessageParsing/MessageNode.cs +++ b/CryptoExchange.Net/Converters/MessageParsing/MessageNode.cs @@ -1,4 +1,4 @@ -namespace CryptoExchange.Net.Sockets.MessageParsing +namespace CryptoExchange.Net.Converters.MessageParsing { /// /// Node accessor diff --git a/CryptoExchange.Net/Sockets/MessageParsing/MessagePath.cs b/CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs similarity index 95% rename from CryptoExchange.Net/Sockets/MessageParsing/MessagePath.cs rename to CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs index 51db132..601c692 100644 --- a/CryptoExchange.Net/Sockets/MessageParsing/MessagePath.cs +++ b/CryptoExchange.Net/Converters/MessageParsing/MessagePath.cs @@ -1,7 +1,7 @@ using System.Collections; using System.Collections.Generic; -namespace CryptoExchange.Net.Sockets.MessageParsing +namespace CryptoExchange.Net.Converters.MessageParsing { /// /// Message access definition diff --git a/CryptoExchange.Net/Sockets/MessageParsing/MessagePathExtension.cs b/CryptoExchange.Net/Converters/MessageParsing/MessagePathExtension.cs similarity index 95% rename from CryptoExchange.Net/Sockets/MessageParsing/MessagePathExtension.cs rename to CryptoExchange.Net/Converters/MessageParsing/MessagePathExtension.cs index 771a992..2f5cde9 100644 --- a/CryptoExchange.Net/Sockets/MessageParsing/MessagePathExtension.cs +++ b/CryptoExchange.Net/Converters/MessageParsing/MessagePathExtension.cs @@ -1,4 +1,4 @@ -namespace CryptoExchange.Net.Sockets.MessageParsing +namespace CryptoExchange.Net.Converters.MessageParsing { /// /// Message path extension methods diff --git a/CryptoExchange.Net/Sockets/MessageParsing/NodeType.cs b/CryptoExchange.Net/Converters/MessageParsing/NodeType.cs similarity index 85% rename from CryptoExchange.Net/Sockets/MessageParsing/NodeType.cs rename to CryptoExchange.Net/Converters/MessageParsing/NodeType.cs index 309db88..21bccb8 100644 --- a/CryptoExchange.Net/Sockets/MessageParsing/NodeType.cs +++ b/CryptoExchange.Net/Converters/MessageParsing/NodeType.cs @@ -1,4 +1,4 @@ -namespace CryptoExchange.Net.Sockets.MessageParsing +namespace CryptoExchange.Net.Converters.MessageParsing { /// /// Message node type diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs new file mode 100644 index 0000000..40be613 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Concurrent; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Text.Json.Serialization; +using System.Text.Json; +using CryptoExchange.Net.Attributes; +using System.Collections.Generic; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties + /// with [ArrayProperty(x)] where x is the index of the property in the array + /// + public class ArrayConverter : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) => true; + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert); + return (JsonConverter)Activator.CreateInstance(converterType); + } + + private class ArrayPropertyInfo + { + public PropertyInfo PropertyInfo { get; set; } = null!; + public ArrayPropertyAttribute ArrayProperty { get; set; } = null!; + public Type? JsonConverterType { get; set; } + public bool DefaultDeserialization { get; set; } + } + + private class ArrayConverterInner : JsonConverter + { + private static readonly ConcurrentDictionary> _typeAttributesCache = new ConcurrentDictionary>(); + + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + // TODO + throw new NotImplementedException(); + } + + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return default; + + var result = Activator.CreateInstance(typeToConvert); + return (T)ParseObject(ref reader, result, typeToConvert); + } + + private static List CacheTypeAttributes(Type type) + { + var attributes = new List(); + var properties = type.GetProperties(); + foreach (var property in properties) + { + var att = property.GetCustomAttribute(); + if (att == null) + continue; + + attributes.Add(new ArrayPropertyInfo + { + ArrayProperty = att, + PropertyInfo = property, + DefaultDeserialization = property.GetCustomAttribute() != null, + JsonConverterType = property.GetCustomAttribute()?.ConverterType + }); + } + + _typeAttributesCache.TryAdd(type, attributes); + return attributes; + } + + private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType) + { + if (reader.TokenType != JsonTokenType.StartArray) + throw new Exception("1"); + + if (!_typeAttributesCache.TryGetValue(objectType, out var attributes)) + attributes = CacheTypeAttributes(objectType); + + int index = 0; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + break; + + var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index); + var targetType = attribute.PropertyInfo.PropertyType; + + object? value = null; + if (attribute.JsonConverterType != null) + { + // Has JsonConverter attribute + var options = new JsonSerializerOptions(); + options.Converters.Add((JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)); + value = JsonDocument.ParseValue(ref reader).Deserialize(targetType, options); + } + else if (attribute.DefaultDeserialization) + { + // Use default deserialization + value = JsonDocument.ParseValue(ref reader).Deserialize(targetType); + } + else + { + value = reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.False => false, + JsonTokenType.True => true, + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetDecimal(), + _ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"), + }; + } + + attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, attribute.PropertyInfo.PropertyType, CultureInfo.InvariantCulture)); + + index++; + } + + return result; + } + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs new file mode 100644 index 0000000..df115f3 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs @@ -0,0 +1,82 @@ +using System; +using System.Diagnostics; +using System.Runtime.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Bool converter + /// + public class BoolConverter : JsonConverterFactory + { + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?); + } + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); + return (JsonConverter)Activator.CreateInstance(converterType); + } + + private class BoolConverterInner : JsonConverter + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!; + + public bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.True) + return true; + + if (reader.TokenType == JsonTokenType.False) + return false; + + var value = reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetInt16().ToString(), + _ => null + }; + + value = value?.ToLowerInvariant().Trim(); + if (string.IsNullOrEmpty(value)) + { + if (typeToConvert == typeof(bool)) + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool"); + return default; + } + + switch (value) + { + case "true": + case "yes": + case "y": + case "1": + case "on": + return true; + case "false": + case "no": + case "n": + case "0": + case "off": + case "-1": + return false; + } + + throw new SerializationException($"Can't convert bool value {value}"); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + writer.WriteNullValue(); + } + } + + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs new file mode 100644 index 0000000..cd8fb2f --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs @@ -0,0 +1,202 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Date time converter + /// + public class DateTimeConverter : JsonConverterFactory + { + private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; + private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d; + private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000; + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?); + } + + /// + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); + return (JsonConverter)Activator.CreateInstance(converterType); + } + + private class DateTimeConverterInner : JsonConverter + { + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!; + + private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + if (typeToConvert == typeof(DateTime)) + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable"); + return default; + } + + if (reader.TokenType is JsonTokenType.Number) + { + var longValue = reader.GetDouble(); + if (longValue == 0 || longValue == -1) + return default; + if (longValue < 19999999999) + return ConvertFromSeconds(longValue); + if (longValue < 19999999999999) + return ConvertFromMilliseconds(longValue); + if (longValue < 19999999999999999) + return ConvertFromMicroseconds(longValue); + + return ConvertFromNanoseconds(longValue); + } + else if (reader.TokenType is JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (string.IsNullOrWhiteSpace(stringValue) + || stringValue == "-1" + || double.TryParse(stringValue, out var doubleVal) && doubleVal == 0) + { + return default; + } + + if (stringValue!.Length == 8) + { + // Parse 20211103 format + if (!int.TryParse(stringValue.Substring(0, 4), out var year) + || !int.TryParse(stringValue.Substring(4, 2), out var month) + || !int.TryParse(stringValue.Substring(6, 2), out var day)) + { + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue); + return default; + } + return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc); + } + + if (stringValue.Length == 6) + { + // Parse 211103 format + if (!int.TryParse(stringValue.Substring(0, 2), out var year) + || !int.TryParse(stringValue.Substring(2, 2), out var month) + || !int.TryParse(stringValue.Substring(4, 2), out var day)) + { + Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue); + return default; + } + return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc); + } + + if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue)) + { + // Parse 1637745563.000 format + if (doubleValue < 19999999999) + return ConvertFromSeconds(doubleValue); + if (doubleValue < 19999999999999) + return ConvertFromMilliseconds((long)doubleValue); + if (doubleValue < 19999999999999999) + return ConvertFromMicroseconds((long)doubleValue); + + return ConvertFromNanoseconds((long)doubleValue); + } + + if (stringValue.Length == 10) + { + // Parse 2021-11-03 format + var values = stringValue.Split('-'); + if (!int.TryParse(values[0], out var year) + || !int.TryParse(values[1], out var month) + || !int.TryParse(values[2], out var day)) + { + Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue); + return default; + } + + return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc); + } + + return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal); + } + else + { + return reader.GetDateTime(); + } + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value == null) + writer.WriteNullValue(); + else + { + var dtValue = (DateTime)(object)value; + if (dtValue == default) + writer.WriteStringValue(default(DateTime)); + else + writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds)); + } + } + } + + /// + /// Convert a seconds since epoch (01-01-1970) value to DateTime + /// + /// + /// + public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond)); + /// + /// Convert a milliseconds since epoch (01-01-1970) value to DateTime + /// + /// + /// + public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond)); + /// + /// Convert a microseconds since epoch (01-01-1970) value to DateTime + /// + /// + /// + public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond)); + /// + /// Convert a nanoseconds since epoch (01-01-1970) value to DateTime + /// + /// + /// + public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond)); + + /// + /// Convert a DateTime value to seconds since epoch (01-01-1970) value + /// + /// + /// + [return: NotNullIfNotNull("time")] + public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds); + /// + /// Convert a DateTime value to milliseconds since epoch (01-01-1970) value + /// + /// + /// + [return: NotNullIfNotNull("time")] + public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds); + /// + /// Convert a DateTime value to microseconds since epoch (01-01-1970) value + /// + /// + /// + [return: NotNullIfNotNull("time")] + public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond); + /// + /// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value + /// + /// + /// + [return: NotNullIfNotNull("time")] + public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond); + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs new file mode 100644 index 0000000..d753848 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/DecimalConverter.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Decimal converter + /// + public class DecimalConverter : JsonConverter + { + /// + public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType == JsonTokenType.String) + { + var value = reader.GetString(); + if (string.IsNullOrEmpty(value)) + return null; + + return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); + } + + return reader.GetDecimal(); + } + + /// + public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options) + { + if (value == null) + writer.WriteNullValue(); + else + writer.WriteNumberValue(value.Value); + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DecimalStringWriterConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DecimalStringWriterConverter.cs new file mode 100644 index 0000000..551030c --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/DecimalStringWriterConverter.cs @@ -0,0 +1,23 @@ +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Converter for serializing decimal values as string + /// + public class DecimalStringWriterConverter : JsonConverter + { + /// + public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + /// + public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null); + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs new file mode 100644 index 0000000..11eca74 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs @@ -0,0 +1,207 @@ +using CryptoExchange.Net.Attributes; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value + /// + public class EnumConverter : JsonConverterFactory + { + private bool _warnOnMissingEntry = true; + private bool _writeAsInt; + private static readonly ConcurrentDictionary>> _mapping = new(); + + /// + /// + public EnumConverter() { } + + /// + /// + /// + /// + public EnumConverter(bool writeAsInt, bool warnOnMissingEntry) + { + _writeAsInt = writeAsInt; + _warnOnMissingEntry = warnOnMissingEntry; + } + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true; + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + JsonConverter converter = (JsonConverter)Activator.CreateInstance( + typeof(EnumConverterInner<>).MakeGenericType( + new Type[] { typeToConvert }), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: new object[] { _writeAsInt, _warnOnMissingEntry }, + culture: null)!; + + return converter; + } + + private static List> AddMapping(Type objectType) + { + var mapping = new List>(); + var enumMembers = objectType.GetMembers(); + foreach (var member in enumMembers) + { + var maps = member.GetCustomAttributes(typeof(MapAttribute), false); + foreach (MapAttribute attribute in maps) + { + foreach (var value in attribute.Values) + mapping.Add(new KeyValuePair(Enum.Parse(objectType, member.Name), value)); + } + } + _mapping.TryAdd(objectType, mapping); + return mapping; + } + + private class EnumConverterInner : JsonConverter + { + private bool _warnOnMissingEntry = true; + private bool _writeAsInt; + + public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry) + { + _warnOnMissingEntry = warnOnMissingEntry; + _writeAsInt = writeAsInt; + } + + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; + if (!_mapping.TryGetValue(enumType, out var mapping)) + mapping = AddMapping(enumType); + + var stringValue = reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetInt16().ToString(), + _ => null + }; + + if (string.IsNullOrEmpty(stringValue)) + { + // Received null value + var emptyResult = GetDefaultValue(typeToConvert, enumType); + if (emptyResult != null) + // If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core). + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo"); + + return (T?)emptyResult; + } + + if (!GetValue(enumType, mapping, stringValue!, out var result)) + { + var defaultValue = GetDefaultValue(typeToConvert, enumType); + if (string.IsNullOrWhiteSpace(stringValue)) + { + if (defaultValue != null) + // We received an empty string and have no mapping for it, and the property isn't nullable + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo"); + } + else + { + // We received an enum value but weren't able to parse it. + if (_warnOnMissingEntry) + Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo"); + } + + return (T?)defaultValue; + } + + return (T?)result; + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + } + else + { + if (!_writeAsInt) + { + var stringValue = GetString(value.GetType(), value); + writer.WriteStringValue(stringValue); + } + else + { + writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int))); + } + } + } + + private static object? GetDefaultValue(Type objectType, Type enumType) + { + if (Nullable.GetUnderlyingType(objectType) != null) + return null; + + return Activator.CreateInstance(enumType); // return default value + } + + private static bool GetValue(Type objectType, List> enumMapping, string value, out object? result) + { + // Check for exact match first, then if not found fallback to a case insensitive match + var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); + if (mapping.Equals(default(KeyValuePair))) + mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); + + if (!mapping.Equals(default(KeyValuePair))) + { + result = mapping.Key; + return true; + } + + try + { + // If no explicit mapping is found try to parse string + result = Enum.Parse(objectType, value, true); + return true; + } + catch (Exception) + { + result = default; + return false; + } + } + } + + /// + /// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned + /// + /// + /// + /// + [return: NotNullIfNotNull("enumValue")] + public static string? GetString(T enumValue) => GetString(typeof(T), enumValue); + + + [return: NotNullIfNotNull("enumValue")] + private static string? GetString(Type objectType, object? enumValue) + { + objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; + + if (!_mapping.TryGetValue(objectType, out var mapping)) + mapping = AddMapping(objectType); + + return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs b/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs new file mode 100644 index 0000000..1605afb --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs @@ -0,0 +1,27 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Serializer options + /// + public static class SerializerOptions + { + /// + /// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter + /// + public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + Converters = + { + new DateTimeConverter(), + new EnumConverter(), + new BoolConverter(), + new DecimalConverter(), + } + }; + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs new file mode 100644 index 0000000..41f577e --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -0,0 +1,284 @@ +using CryptoExchange.Net.Converters.MessageParsing; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.Json; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// System.Text.Json message accessor + /// + public abstract class SystemTextJsonMessageAccessor : IMessageAccessor + { + /// + /// The JsonDocument loaded + /// + protected JsonDocument? _document; + + private static JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters; + + /// + public bool IsJson { get; set; } + + /// + public abstract bool OriginalDataAvailable { get; } + + /// + public object? Underlying => throw new NotImplementedException(); + + /// + public CallResult Deserialize(Type type, MessagePath? path = null) + { + if (!IsJson) + return new CallResult(GetOriginalString()); + + if (_document == null) + throw new InvalidOperationException("No json document loaded"); + + try + { + var result = _document.Deserialize(type, _serializerOptions); + return new CallResult(result!); + } + catch (JsonException ex) + { + var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } + } + + /// + public CallResult Deserialize(MessagePath? path = null) + { + if (_document == null) + throw new InvalidOperationException("No json document loaded"); + + try + { + var result = _document.Deserialize(_serializerOptions); + return new CallResult(result!); + } + catch (JsonException ex) + { + var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; + return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + } + } + + /// + public NodeType? GetNodeType() + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + if (_document == null) + throw new InvalidOperationException("No json document loaded"); + + return _document.RootElement.ValueKind switch + { + JsonValueKind.Object => NodeType.Object, + JsonValueKind.Array => NodeType.Array, + _ => NodeType.Value + }; + } + + /// + public NodeType? GetNodeType(MessagePath path) + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var node = GetPathNode(path); + if (!node.HasValue) + return null; + + return node.Value.ValueKind switch + { + JsonValueKind.Object => NodeType.Object, + JsonValueKind.Array => NodeType.Array, + _ => NodeType.Value + }; + } + + /// + public T? GetValue(MessagePath path) + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + var value = GetPathNode(path); + if (value == null) + return default; + + if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array) + return default; + + var ttype = typeof(T); + if (ttype == typeof(string)) + return (T?)(object?)value.Value.GetString(); + if (ttype == typeof(short)) + return (T)(object)value.Value.GetInt16(); + if (ttype == typeof(int)) + return (T)(object)value.Value.GetInt32(); + if (ttype == typeof(long)) + return (T)(object)value.Value.GetInt64(); + + return default; + } + + /// + public List? GetValues(MessagePath path) => throw new NotImplementedException(); + + private JsonElement? GetPathNode(MessagePath path) + { + if (!IsJson) + throw new InvalidOperationException("Can't access json data on non-json message"); + + if (_document == null) + throw new InvalidOperationException("No json document loaded"); + + JsonElement? currentToken = _document.RootElement; + foreach (var node in path) + { + if (node.Type == 0) + { + // Int value + var val = (int)node.Value!; + if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val) + return null; + + currentToken = currentToken.Value[val]; + } + else if (node.Type == 1) + { + // String value + if (currentToken!.Value.ValueKind != JsonValueKind.Object) + return null; + + if (!currentToken.Value.TryGetProperty((string)node.Value!, out var token)) + return null; + currentToken = token; + } + else + { + // Property name + if (currentToken!.Value.ValueKind != JsonValueKind.Object) + return null; + + throw new NotImplementedException(); + } + + if (currentToken == null) + return null; + } + + return currentToken; + } + + /// + public abstract string GetOriginalString(); + + /// + public abstract void Clear(); + } + + /// + /// System.Text.Json stream message accessor + /// + public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor + { + private Stream? _stream; + + /// + public override bool OriginalDataAvailable => _stream?.CanSeek == true; + + /// + public bool Read(Stream stream, bool bufferStream) + { + if (bufferStream && stream is not MemoryStream) + { + _stream = new MemoryStream(); + stream.CopyTo(_stream); + _stream.Position = 0; + } + else + { + _stream = stream; + } + + try + { + _document = JsonDocument.Parse(_stream); + IsJson = true; + } + catch (Exception) + { + // Not a json message + IsJson = false; + } + + return IsJson; + } + /// + public override string GetOriginalString() + { + if (_stream is null) + throw new NullReferenceException("Stream not initialized"); + + _stream.Position = 0; + using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true); + return textReader.ReadToEnd(); + } + + /// + public override void Clear() + { + _stream?.Dispose(); + _stream = null; + _document = null; + } + + } + + /// + /// System.Text.Json byte message accessor + /// + public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor + { + private ReadOnlyMemory _bytes; + + /// + public bool Read(ReadOnlyMemory data) + { + try + { + _document = JsonDocument.Parse(data); + IsJson = true; + } + catch (Exception) + { + // Not a json message + IsJson = false; + } + + return IsJson; + } + + /// + public override string GetOriginalString() => Encoding.UTF8.GetString(_bytes.ToArray()); + + /// + public override bool OriginalDataAvailable => true; + + /// + public override void Clear() + { + _bytes = null; + _document = null; + } + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs new file mode 100644 index 0000000..8c4cdf7 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs @@ -0,0 +1,12 @@ +using CryptoExchange.Net.Interfaces; +using System.Text.Json; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + public class SystemTextJsonMessageSerializer : IMessageSerializer + { + /// + public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters); + } +} diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index a9aba8b..5451780 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -44,7 +44,7 @@ CryptoExchange.Net.xml - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -52,11 +52,12 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - - + + + \ No newline at end of file diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index e2188bf..0ec990b 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -8,8 +8,6 @@ using System.Text; using System.Web; using CryptoExchange.Net.Objects; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace CryptoExchange.Net { @@ -29,18 +27,6 @@ namespace CryptoExchange.Net parameters.Add(key, value); } - /// - /// Add a parameter - /// - /// - /// - /// - /// - public static void AddParameter(this Dictionary parameters, string key, string value, JsonConverter converter) - { - parameters.Add(key, JsonConvert.SerializeObject(value, converter)); - } - /// /// Add a parameter /// @@ -52,18 +38,6 @@ namespace CryptoExchange.Net parameters.Add(key, value); } - /// - /// Add a parameter - /// - /// - /// - /// - /// - public static void AddParameter(this Dictionary parameters, string key, object value, JsonConverter converter) - { - parameters.Add(key, JsonConvert.SerializeObject(value, converter)); - } - /// /// Add an optional parameter. Not added if value is null /// @@ -76,19 +50,6 @@ namespace CryptoExchange.Net parameters.Add(key, value); } - /// - /// Add an optional parameter. Not added if value is null - /// - /// - /// - /// - /// - public static void AddOptionalParameter(this Dictionary parameters, string key, object? value, JsonConverter converter) - { - if (value != null) - parameters.Add(key, JsonConvert.SerializeObject(value, converter)); - } - /// /// Create a query string of the specified parameters /// @@ -230,37 +191,6 @@ namespace CryptoExchange.Net return secureString; } - /// - /// String to JToken - /// - /// - /// - /// - public static JToken? ToJToken(this string stringData, ILogger? logger = null) - { - if (string.IsNullOrEmpty(stringData)) - return null; - - try - { - return JToken.Parse(stringData); - } - catch (JsonReaderException jre) - { - var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}"; - logger?.Log(LogLevel.Error, info); - if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}"); - return null; - } - catch (JsonSerializationException jse) - { - var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}"; - logger?.Log(LogLevel.Error, info); - if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}"); - return null; - } - } - /// /// Validates an int is one of the allowed values /// diff --git a/CryptoExchange.Net/Sockets/MessageParsing/Interfaces/IMessageData.cs b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs similarity index 52% rename from CryptoExchange.Net/Sockets/MessageParsing/Interfaces/IMessageData.cs rename to CryptoExchange.Net/Interfaces/IMessageAccessor.cs index 9c76270..44d3d70 100644 --- a/CryptoExchange.Net/Sockets/MessageParsing/Interfaces/IMessageData.cs +++ b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs @@ -1,8 +1,10 @@ -using System; +using CryptoExchange.Net.Converters.MessageParsing; +using CryptoExchange.Net.Objects; +using System; using System.Collections.Generic; using System.IO; -namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces +namespace CryptoExchange.Net.Interfaces { /// /// Message accessor @@ -14,14 +16,17 @@ namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces /// bool IsJson { get; } /// + /// Is the original data available for retrieval + /// + bool OriginalDataAvailable { get; } + /// /// The underlying data object /// object? Underlying { get; } /// - /// Load a stream message + /// Clear internal data structure /// - /// - void Load(Stream stream); + void Clear(); /// /// Get the type of node /// @@ -53,6 +58,43 @@ namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces /// /// /// - object Deserialize(Type type, MessagePath? path = null); + CallResult Deserialize(Type type, MessagePath? path = null); + /// + /// Deserialize the message into this type + /// + /// + /// + CallResult Deserialize(MessagePath? path = null); + + /// + /// Get the original string value + /// + /// + string GetOriginalString(); + } + + /// + /// Stream message accessor + /// + public interface IStreamMessageAccessor : IMessageAccessor + { + /// + /// Load a stream message + /// + /// + /// + bool Read(Stream stream, bool bufferStream); + } + + /// + /// Byte message accessor + /// + public interface IByteMessageAccessor : IMessageAccessor + { + /// + /// Load a data message + /// + /// + bool Read(ReadOnlyMemory data); } } diff --git a/CryptoExchange.Net/Interfaces/IMessageProcessor.cs b/CryptoExchange.Net/Interfaces/IMessageProcessor.cs index 6f6115a..b1846e1 100644 --- a/CryptoExchange.Net/Interfaces/IMessageProcessor.cs +++ b/CryptoExchange.Net/Interfaces/IMessageProcessor.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -18,6 +17,10 @@ namespace CryptoExchange.Net.Interfaces /// public int Id { get; } /// + /// Whether this listener can handle data + /// + public bool CanHandleData { get; } + /// /// The identifiers for this processor /// public HashSet ListenerIdentifiers { get; } @@ -27,7 +30,7 @@ namespace CryptoExchange.Net.Interfaces /// /// /// - Task HandleAsync(SocketConnection connection, DataEvent message); + CallResult Handle(SocketConnection connection, DataEvent message); /// /// Get the type the message should be deserialized to /// @@ -40,6 +43,6 @@ namespace CryptoExchange.Net.Interfaces /// /// /// - object Deserialize(IMessageAccessor accessor, Type type); + CallResult Deserialize(IMessageAccessor accessor, Type type); } } diff --git a/CryptoExchange.Net/Sockets/MessageParsing/Interfaces/IMessageSerializer.cs b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs similarity index 83% rename from CryptoExchange.Net/Sockets/MessageParsing/Interfaces/IMessageSerializer.cs rename to CryptoExchange.Net/Interfaces/IMessageSerializer.cs index 8df3e5e..61ffecb 100644 --- a/CryptoExchange.Net/Sockets/MessageParsing/Interfaces/IMessageSerializer.cs +++ b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs @@ -1,4 +1,4 @@ -namespace CryptoExchange.Net.Sockets.MessageParsing.Interfaces +namespace CryptoExchange.Net.Interfaces { /// /// Serializer interface diff --git a/CryptoExchange.Net/Interfaces/IRestClient.cs b/CryptoExchange.Net/Interfaces/IRestClient.cs index b2510e2..a2592a3 100644 --- a/CryptoExchange.Net/Interfaces/IRestClient.cs +++ b/CryptoExchange.Net/Interfaces/IRestClient.cs @@ -17,5 +17,10 @@ namespace CryptoExchange.Net.Interfaces /// The total amount of requests made with this client /// int TotalRequestsMade { get; } + + /// + /// The exchange name + /// + string Exchange { get; } } } \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/ISocketApiClient.cs b/CryptoExchange.Net/Interfaces/ISocketApiClient.cs index 4d45e10..7c7a430 100644 --- a/CryptoExchange.Net/Interfaces/ISocketApiClient.cs +++ b/CryptoExchange.Net/Interfaces/ISocketApiClient.cs @@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Log the current state of connections and subscriptions /// - string GetSubscriptionsState(); + string GetSubscriptionsState(bool includeSubDetails = true); /// /// Reconnect all connections /// diff --git a/CryptoExchange.Net/Interfaces/IWebsocket.cs b/CryptoExchange.Net/Interfaces/IWebsocket.cs index 7dc9736..9a09b8c 100644 --- a/CryptoExchange.Net/Interfaces/IWebsocket.cs +++ b/CryptoExchange.Net/Interfaces/IWebsocket.cs @@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Websocket message received event /// - event Func OnStreamMessage; + event Action> OnStreamMessage; /// /// Websocket sent event, RequestId as parameter /// diff --git a/CryptoExchange.Net/Objects/ParameterCollection.cs b/CryptoExchange.Net/Objects/ParameterCollection.cs index ffcad33..7ce5959 100644 --- a/CryptoExchange.Net/Objects/ParameterCollection.cs +++ b/CryptoExchange.Net/Objects/ParameterCollection.cs @@ -1,5 +1,6 @@ using CryptoExchange.Net.Attributes; using CryptoExchange.Net.Converters; +using CryptoExchange.Net.Converters.SystemTextJson; using System; using System.Collections.Generic; using System.Globalization; @@ -158,6 +159,17 @@ namespace CryptoExchange.Net.Objects Add(key, EnumConverter.GetString(value)!); } + /// + /// Add an enum value as the string value as mapped using the + /// + /// + /// + public void AddEnumAsInt(string key, T value) + { + var stringVal = EnumConverter.GetString(value); + Add(key, EnumConverter.GetString(int.Parse(stringVal))!); + } + /// /// Add an enum value as the string value as mapped using the . Not added if value is null /// @@ -168,5 +180,19 @@ namespace CryptoExchange.Net.Objects if (value != null) Add(key, EnumConverter.GetString(value)); } + + /// + /// Add an enum value as the string value as mapped using the . Not added if value is null + /// + /// + /// + public void AddOptionalEnumAsInt(string key, T? value) + { + if (value != null) + { + var stringVal = EnumConverter.GetString(value); + Add(key, int.Parse(stringVal)); + } + } } } diff --git a/CryptoExchange.Net/Objects/TraceLogger.cs b/CryptoExchange.Net/Objects/TraceLogger.cs index 3404cfb..87c51fd 100644 --- a/CryptoExchange.Net/Objects/TraceLogger.cs +++ b/CryptoExchange.Net/Objects/TraceLogger.cs @@ -31,8 +31,8 @@ namespace CryptoExchange.Net.Objects /// public class TraceLogger : ILogger { - private string? _categoryName; - private LogLevel _logLevel; + private readonly string? _categoryName; + private readonly LogLevel _logLevel; /// /// ctor @@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Objects } /// - public IDisposable BeginScope(TState state) => null!; + public IDisposable? BeginScope(TState state) where TState : notnull => null!; /// public bool IsEnabled(LogLevel logLevel) => (int)logLevel < (int)_logLevel; diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index d2936f1..86096e6 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -45,6 +45,9 @@ namespace CryptoExchange.Net.Sockets private ProcessState _processState; private DateTime _lastReconnectTime; + private const int _receiveBufferSize = 1048576; + private const int _sendBufferSize = 4096; + /// /// Received messages, the size and the timstamp /// @@ -101,7 +104,7 @@ namespace CryptoExchange.Net.Sockets public event Func? OnClose; /// - public event Func? OnStreamMessage; + public event Action>? OnStreamMessage; /// public event Func? OnRequestSent; @@ -168,7 +171,10 @@ namespace CryptoExchange.Net.Sockets foreach (var header in Parameters.Headers) socket.Options.SetRequestHeader(header.Key, header.Value); socket.Options.KeepAliveInterval = Parameters.KeepAliveInterval ?? TimeSpan.Zero; - socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework + if (System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) + socket.Options.SetBuffer(65536, 65536); // Setting it to anything bigger than 65536 throws an exception in .net framework + else + socket.Options.SetBuffer(_receiveBufferSize, _sendBufferSize); if (Parameters.Proxy != null) SetProxy(socket, Parameters.Proxy); } @@ -463,7 +469,7 @@ namespace CryptoExchange.Net.Sockets /// private async Task ReceiveLoopAsync() { - var buffer = new ArraySegment(new byte[65536]); + var buffer = new ArraySegment(new byte[_receiveBufferSize]); var received = 0; try { @@ -472,7 +478,7 @@ namespace CryptoExchange.Net.Sockets if (_ctsSource.IsCancellationRequested) break; - MemoryStream? memoryStream = null; + MemoryStream? multipartStream = null; WebSocketReceiveResult? receiveResult = null; bool multiPartMessage = false; while (true) @@ -501,7 +507,7 @@ namespace CryptoExchange.Net.Sockets if (receiveResult.MessageType == WebSocketMessageType.Close) { // Connection closed unexpectedly - _logger.Log(LogLevel.Debug, $"[Sckt {Id}] received `Close` message"); + _logger.Log(LogLevel.Debug, "[Sckt {Id}] received `Close` message, CloseStatus: {Status}, CloseStatusDescription: {CloseStatusDescription}", Id, receiveResult.CloseStatus, receiveResult.CloseStatusDescription); if (_closeTask?.IsCompleted != false) _closeTask = CloseInternalAsync(); break; @@ -511,9 +517,12 @@ namespace CryptoExchange.Net.Sockets { // We received data, but it is not complete, write it to a memory stream for reassembling multiPartMessage = true; - memoryStream ??= new MemoryStream(); _logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message"); - await memoryStream.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false); + + // Add the buffer to the multipart buffers list, create new buffer for next message part + if (multipartStream == null) + multipartStream = new MemoryStream(); + multipartStream.Write(buffer.Array, buffer.Offset, receiveResult.Count); } else { @@ -521,14 +530,15 @@ namespace CryptoExchange.Net.Sockets { // Received a complete message and it's not multi part _logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in single message"); - await ProcessData(receiveResult.MessageType, new MemoryStream(buffer.Array, buffer.Offset, receiveResult.Count)).ConfigureAwait(false); + ProcessData(receiveResult.MessageType, new ReadOnlyMemory(buffer.Array, buffer.Offset, receiveResult.Count)); } else { // Received the end of a multipart message, write to memory stream for reassembling _logger.Log(LogLevel.Trace, $"[Sckt {Id}] received {receiveResult.Count} bytes in partial message"); - await memoryStream!.WriteAsync(buffer.Array, buffer.Offset, receiveResult.Count).ConfigureAwait(false); + multipartStream!.Write(buffer.Array, buffer.Offset, receiveResult.Count); } + break; } } @@ -553,14 +563,13 @@ namespace CryptoExchange.Net.Sockets // When the connection gets interupted we might not have received a full message if (receiveResult?.EndOfMessage == true) { - // Reassemble complete message from memory stream - _logger.Log(LogLevel.Trace, $"[Sckt {Id}] reassembled message of {memoryStream!.Length} bytes"); - await ProcessData(receiveResult.MessageType, memoryStream).ConfigureAwait(false); - memoryStream.Dispose(); + _logger.Log(LogLevel.Trace, $"[Sckt {Id}] reassembled message of {multipartStream!.Length} bytes"); + // Get the underlying buffer of the memorystream holding the written data and delimit it (GetBuffer return the full array, not only the written part) + ProcessData(receiveResult.MessageType, new ReadOnlyMemory(multipartStream.GetBuffer(), 0, (int)multipartStream.Length)); } else { - _logger.Log(LogLevel.Trace, $"[Sckt {Id}] discarding incomplete message of {memoryStream!.Length} bytes"); + _logger.Log(LogLevel.Trace, $"[Sckt {Id}] discarding incomplete message of {multipartStream!.Length} bytes"); } } } @@ -584,15 +593,12 @@ namespace CryptoExchange.Net.Sockets /// Proccess a stream message /// /// - /// + /// /// - protected async Task ProcessData(WebSocketMessageType type, Stream stream) + protected void ProcessData(WebSocketMessageType type, ReadOnlyMemory data) { LastActionTime = DateTime.UtcNow; - stream.Position = 0; - - if (OnStreamMessage != null) - await OnStreamMessage.Invoke(type, stream).ConfigureAwait(false); + OnStreamMessage?.Invoke(type, data); } /// diff --git a/CryptoExchange.Net/Sockets/MessageParsing/JsonNetMessageData.cs b/CryptoExchange.Net/Sockets/MessageParsing/JsonNetMessageData.cs deleted file mode 100644 index 93e4c93..0000000 --- a/CryptoExchange.Net/Sockets/MessageParsing/JsonNetMessageData.cs +++ /dev/null @@ -1,173 +0,0 @@ -using CryptoExchange.Net.Converters; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace CryptoExchange.Net.Sockets.MessageParsing -{ - /// - /// Json.Net message accessor - /// - public class JsonNetMessageAccessor : IMessageAccessor - { - private JToken? _token; - private Stream? _stream; - private static JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters); - - /// - public bool IsJson { get; private set; } - - /// - public object? Underlying => _token; - - /// - public void Load(Stream stream) - { - _stream = stream; - using var reader = new StreamReader(stream, Encoding.UTF8, false, (int)stream.Length, true); - using var jsonTextReader = new JsonTextReader(reader); - - try - { - _token = JToken.Load(jsonTextReader); - IsJson = true; - } - catch (Exception) - { - // Not a json message - IsJson = false; - } - } - - /// - public object Deserialize(Type type, MessagePath? path = null) - { - if (!IsJson) - { - var sr = new StreamReader(_stream); - return sr.ReadToEnd(); - } - - var source = _token; - if (path != null) - source = GetPathNode(path.Value); - - return source!.ToObject(type, _serializer)!; - } - - /// - public NodeType? GetNodeType() - { - if (!IsJson) - throw new InvalidOperationException("Can't access json data on non-json message"); - - if (_token == null) - return null; - - if (_token.Type == JTokenType.Object) - return NodeType.Object; - - if (_token.Type == JTokenType.Array) - return NodeType.Array; - - return NodeType.Value; - } - - /// - public NodeType? GetNodeType(MessagePath path) - { - if (!IsJson) - throw new InvalidOperationException("Can't access json data on non-json message"); - - var node = GetPathNode(path); - if (node == null) - return null; - - if (node.Type == JTokenType.Object) - return NodeType.Object; - - if (node.Type == JTokenType.Array) - return NodeType.Array; - - return NodeType.Value; - } - - /// - public T? GetValue(MessagePath path) - { - if (!IsJson) - throw new InvalidOperationException("Can't access json data on non-json message"); - - var value = GetPathNode(path); - if (value == null) - return default; - - if (value.Type == JTokenType.Object || value.Type == JTokenType.Array) - return default; - - return value!.Value(); - } - - /// - public List? GetValues(MessagePath path) - { - if (!IsJson) - throw new InvalidOperationException("Can't access json data on non-json message"); - - var value = GetPathNode(path); - if (value == null) - return default; - - if (value.Type == JTokenType.Object) - return default; - - return value!.Values().ToList(); - } - - private JToken? GetPathNode(MessagePath path) - { - if (!IsJson) - throw new InvalidOperationException("Can't access json data on non-json message"); - - var currentToken = _token; - foreach (var node in path) - { - if (node.Type == 0) - { - // Int value - var val = (int)node.Value!; - if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val) - return null; - - currentToken = currentToken[val]; - } - else if (node.Type == 1) - { - // String value - if (currentToken!.Type != JTokenType.Object) - return null; - - currentToken = currentToken[(string)node.Value!]; - } - else - { - // Property name - if (currentToken!.Type != JTokenType.Object) - return null; - - currentToken = (currentToken.First as JProperty)?.Name; - } - - if (currentToken == null) - return null; - } - - return currentToken; - } - } -} diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index 56d3ea4..a33cd67 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using System; using System.Collections.Generic; using System.Diagnostics; @@ -20,6 +19,11 @@ namespace CryptoExchange.Net.Sockets /// public int Id { get; } = ExchangeHelpers.NextId(); + /// + /// Can handle data + /// + public bool CanHandleData => true; + /// /// Has this query been completed /// @@ -43,7 +47,7 @@ namespace CryptoExchange.Net.Sockets /// /// Wait event for the calling message processing thread /// - public AsyncResetEvent? ContinueAwaiter { get; set; } + public ManualResetEvent? ContinueAwaiter { get; set; } /// /// Strings to match this query to a received message @@ -116,7 +120,7 @@ namespace CryptoExchange.Net.Sockets public async Task WaitAsync(TimeSpan timeout) => await _event.WaitAsync(timeout).ConfigureAwait(false); /// - public virtual object Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type); + public virtual CallResult Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type); /// /// Mark request as timeout @@ -127,7 +131,7 @@ namespace CryptoExchange.Net.Sockets /// Mark request as failed /// /// - public abstract void Fail(string error); + public abstract void Fail(Error error); /// /// Handle a response message @@ -135,7 +139,7 @@ namespace CryptoExchange.Net.Sockets /// /// /// - public abstract Task HandleAsync(SocketConnection connection, DataEvent message); + public abstract CallResult Handle(SocketConnection connection, DataEvent message); } @@ -164,13 +168,13 @@ namespace CryptoExchange.Net.Sockets } /// - public override async Task HandleAsync(SocketConnection connection, DataEvent message) + public override CallResult Handle(SocketConnection connection, DataEvent message) { Completed = true; Response = message.Data; - Result = await HandleMessageAsync(connection, message.As((TResponse)message.Data)).ConfigureAwait(false); + Result = HandleMessage(connection, message.As((TResponse)message.Data)); _event.Set(); - await (ContinueAwaiter?.WaitAsync() ?? Task.CompletedTask).ConfigureAwait(false); + ContinueAwaiter?.WaitOne(); return Result; } @@ -180,7 +184,7 @@ namespace CryptoExchange.Net.Sockets /// /// /// - public virtual Task> HandleMessageAsync(SocketConnection connection, DataEvent message) => Task.FromResult(new CallResult(message.Data, message.OriginalData, null)); + public virtual CallResult HandleMessage(SocketConnection connection, DataEvent message) => new CallResult(message.Data, message.OriginalData, null); /// public override void Timeout() @@ -195,9 +199,9 @@ namespace CryptoExchange.Net.Sockets } /// - public override void Fail(string error) + public override void Fail(Error error) { - Result = new CallResult(new ServerError(error)); + Result = new CallResult(error); Completed = true; ContinueAwaiter?.Set(); _event.Set(); diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 0d686c8..4d18ecb 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -8,10 +8,10 @@ using CryptoExchange.Net.Objects; using System.Net.WebSockets; using System.IO; using CryptoExchange.Net.Objects.Sockets; -using System.Text; using System.Diagnostics; -using CryptoExchange.Net.Sockets.MessageParsing; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Converters.JsonNet; +using System.Threading; namespace CryptoExchange.Net.Sockets { @@ -160,8 +160,8 @@ namespace CryptoExchange.Net.Sockets private readonly ILogger _logger; private SocketStatus _status; - private IMessageSerializer _serializer; - private IMessageAccessor _accessor; + private readonly IMessageSerializer _serializer; + private readonly IByteMessageAccessor _accessor; /// /// The task that is sending periodic data on the websocket. Can be used for sending Ping messages every x seconds or similair. Not necesarry. @@ -205,8 +205,8 @@ namespace CryptoExchange.Net.Sockets _listenersLock = new object(); _listeners = new List(); - _serializer = new JsonNetSerializer(); - _accessor = new JsonNetMessageAccessor(); + _serializer = apiClient.CreateSerializer(); + _accessor = apiClient.CreateAccessor(); } /// @@ -234,7 +234,7 @@ namespace CryptoExchange.Net.Sockets foreach (var query in _listeners.OfType().ToList()) { - query.Fail("Connection interupted"); + query.Fail(new WebError("Connection interupted")); _listeners.Remove(query); } } @@ -259,7 +259,7 @@ namespace CryptoExchange.Net.Sockets foreach (var query in _listeners.OfType().ToList()) { - query.Fail("Connection interupted"); + query.Fail(new WebError("Connection interupted")); _listeners.Remove(query); } } @@ -288,7 +288,7 @@ namespace CryptoExchange.Net.Sockets { foreach (var query in _listeners.OfType().ToList()) { - query.Fail("Connection interupted"); + query.Fail(new WebError("Connection interupted")); _listeners.Remove(query); } } @@ -363,115 +363,113 @@ namespace CryptoExchange.Net.Sockets /// /// Handle a message /// - /// + /// /// /// - protected virtual async Task HandleStreamMessage(WebSocketMessageType type, Stream stream) + protected virtual void HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory data) { var sw = Stopwatch.StartNew(); var receiveTime = DateTime.UtcNow; string? originalData = null; // 1. Decrypt/Preprocess if necessary - stream = ApiClient.PreprocessStreamMessage(type, stream); + data = ApiClient.PreprocessStreamMessage(type, data); // 2. Read data into accessor - _accessor.Load(stream); - if (ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData) - { - stream.Position = 0; - using var textReader = new StreamReader(stream, Encoding.UTF8, false, 1024, true); - originalData = textReader.ReadToEnd(); - - _logger.LogTrace("[Sckt {SocketId}] received {Data}", SocketId, originalData); - } - - // 3. Determine the identifying properties of this message - var listenId = ApiClient.GetListenerIdentifier(_accessor); - if (listenId == null) - { - if (!ApiClient.UnhandledMessageExpected) - _logger.LogWarning("[Sckt {SocketId}] failed to evaluate message", SocketId); - - UnhandledMessage?.Invoke(_accessor); - stream.Dispose(); - return; - } - - // 4. Get the listeners interested in this message - List processors; - lock(_listenersLock) - processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList(); - - if (!processors.Any()) - { - if (!ApiClient.UnhandledMessageExpected) + _accessor.Read(data); + try { + if (ApiClient.ApiOptions.OutputOriginalData ?? ApiClient.ClientOptions.OutputOriginalData) { - List listenerIds; - lock (_listenersLock) - listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList(); - _logger.LogWarning("[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: {ListenIds}", SocketId, listenId, listenerIds); + originalData = _accessor.GetOriginalString(); + _logger.LogTrace("[Sckt {SocketId}] received {Data}", SocketId, originalData); + } + + // 3. Determine the identifying properties of this message + var listenId = ApiClient.GetListenerIdentifier(_accessor); + if (listenId == null) + { + if (!ApiClient.UnhandledMessageExpected) + _logger.LogWarning("[Sckt {SocketId}] failed to evaluate message", SocketId); + UnhandledMessage?.Invoke(_accessor); + return; } - stream.Dispose(); - return; - } + // 4. Get the listeners interested in this message + List processors; + lock (_listenersLock) + processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId) && s.CanHandleData).ToList(); - _logger.LogTrace("[Sckt {SocketId}] {Count} processor(s) matched to message with listener identifier {ListenerId}", SocketId, processors.Count, listenId); - var totalUserTime = 0; - Dictionary? desCache = null; - if (processors.Count > 1) - { - // Only instantiate a cache if there are multiple processors - desCache = new Dictionary(); - } - - foreach (var processor in processors) - { - // 5. Determine the type to deserialize to for this processor - var messageType = processor.GetMessageType(_accessor); - if (messageType == null) + if (!processors.Any()) { - _logger.LogWarning("[Sckt {SocketId}] received message not recognized by handler {Id}", SocketId, processor.Id); - continue; + if (!ApiClient.UnhandledMessageExpected) + { + List listenerIds; + lock (_listenersLock) + listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList(); + _logger.LogWarning("[Sckt {SocketId}] received message not matched to any listener. ListenId: {ListenId}, current listeners: {ListenIds}", SocketId, listenId, listenerIds); + UnhandledMessage?.Invoke(_accessor); + } + + return; } - // 6. Deserialize the message - object? deserialized = null; - desCache?.TryGetValue(messageType, out deserialized); - - if (deserialized == null) + _logger.LogTrace("[Sckt {SocketId}] {Count} processor(s) matched to message with listener identifier {ListenerId}", SocketId, processors.Count, listenId); + var totalUserTime = 0; + Dictionary? desCache = null; + if (processors.Count > 1) { + // Only instantiate a cache if there are multiple processors + desCache = new Dictionary(); + } + + foreach (var processor in processors) + { + // 5. Determine the type to deserialize to for this processor + var messageType = processor.GetMessageType(_accessor); + if (messageType == null) + { + _logger.LogWarning("[Sckt {SocketId}] received message not recognized by handler {Id}", SocketId, processor.Id); + continue; + } + + // 6. Deserialize the message + object? deserialized = null; + desCache?.TryGetValue(messageType, out deserialized); + + if (deserialized == null) + { + var desResult = processor.Deserialize(_accessor, messageType); + if (!desResult) + { + _logger.LogWarning("[Sckt {SocketId}] deserialization failed: {Error}", SocketId, desResult.Error); + continue; + } + deserialized = desResult.Data; + desCache?.Add(messageType, deserialized); + } + + // 7. Hand of the message to the subscription try { - deserialized = processor.Deserialize(_accessor, messageType); - desCache?.Add(messageType, deserialized); + var innerSw = Stopwatch.StartNew(); + processor.Handle(this, new DataEvent(deserialized, null, originalData, receiveTime, null)); + totalUserTime += (int)innerSw.ElapsedMilliseconds; } catch (Exception ex) { - _logger.LogWarning("[Sckt {SocketId}] failed to deserialize message to type {Type}: {Exception}", SocketId, messageType.Name, ex.ToLogString()); - continue; + _logger.LogWarning("[Sckt {SocketId}] user message processing failed: {Exception}", SocketId, ex.ToLogString()); + if (processor is Subscription subscription) + subscription.InvokeExceptionHandler(ex); } } - // 7. Hand of the message to the subscription - try - { - var innerSw = Stopwatch.StartNew(); - await processor.HandleAsync(this, new DataEvent(deserialized, null, originalData, receiveTime, null)).ConfigureAwait(false); - totalUserTime += (int)innerSw.ElapsedMilliseconds; - } - catch (Exception ex) - { - _logger.LogWarning("[Sckt {SocketId}] user message processing failed: {Exception}", SocketId, ex.ToLogString()); - if (processor is Subscription subscription) - subscription.InvokeExceptionHandler(ex); - } + _logger.LogTrace($"[Sckt {SocketId}] message processed in {(int)sw.ElapsedMilliseconds}ms ({sw.ElapsedMilliseconds - totalUserTime}ms parsing)"); + } + finally + { + _accessor.Clear(); } - - stream.Dispose(); - _logger.LogTrace($"[Sckt {SocketId}] message processed in {(int)sw.ElapsedMilliseconds}ms ({sw.ElapsedMilliseconds - totalUserTime}ms parsing)"); } /// @@ -626,7 +624,7 @@ namespace CryptoExchange.Net.Sockets /// Query to send /// Wait event for when the socket message handler can continue /// - public virtual async Task SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null) + public virtual async Task SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null) { await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false); return query.Result ?? new CallResult(new ServerError("Timeout")); @@ -639,22 +637,22 @@ namespace CryptoExchange.Net.Sockets /// Query to send /// Wait event for when the socket message handler can continue /// - public virtual async Task> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null) + public virtual async Task> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null) { await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false); return query.TypedResult ?? new CallResult(new ServerError("Timeout")); } - private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent) + private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent) { lock(_listenersLock) _listeners.Add(query); query.ContinueAwaiter = continueEvent; - var sendOk = Send(query.Id, query.Request, query.Weight); - if (!sendOk) + var sendResult = Send(query.Id, query.Request, query.Weight); + if (!sendResult) { - query.Fail("Failed to send"); + query.Fail(sendResult.Error!); lock (_listenersLock) _listeners.Remove(query); return; @@ -666,7 +664,7 @@ namespace CryptoExchange.Net.Sockets { if (!_socket.IsOpen) { - query.Fail("Socket not open"); + query.Fail(new WebError("Socket not open")); return; } @@ -693,12 +691,10 @@ namespace CryptoExchange.Net.Sockets /// The request id /// The object to send /// The weight of the message - public virtual bool Send(int requestId, T obj, int weight) + public virtual CallResult Send(int requestId, T obj, int weight) { - if(obj is string str) - return Send(requestId, str, weight); - else - return Send(requestId, _serializer.Serialize(obj!), weight); + var data = obj is string str ? str : _serializer.Serialize(obj!); + return Send(requestId, data, weight); } /// @@ -707,17 +703,24 @@ namespace CryptoExchange.Net.Sockets /// The data to send /// The weight of the message /// The id of the request - public virtual bool Send(int requestId, string data, int weight) + public virtual CallResult Send(int requestId, string data, int weight) { + if (ApiClient.MessageSendSizeLimit != null && data.Length > ApiClient.MessageSendSizeLimit.Value) + { + var info = $"Message to send exceeds the max server message size ({ApiClient.MessageSendSizeLimit.Value} bytes). Split the request into batches to keep below this limit"; + _logger.LogWarning("[Sckt {SocketId}] msg {RequestId} - {Info}", SocketId, requestId, info); + return new CallResult(new InvalidOperationError(info)); + } + _logger.Log(LogLevel.Trace, $"[Sckt {SocketId}] msg {requestId} - sending messsage: {data}"); try { _socket.Send(requestId, data, weight); - return true; + return new CallResult(null); } - catch(Exception) + catch(Exception ex) { - return false; + return new CallResult(new WebError("Failed to send message: " + ex.Message)); } } @@ -783,7 +786,7 @@ namespace CryptoExchange.Net.Sockets if (subQuery == null) continue; - var waitEvent = new AsyncResetEvent(false); + var waitEvent = new ManualResetEvent(false); taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) => { subscription.HandleSubQueryResponse(subQuery.Response!); diff --git a/CryptoExchange.Net/Sockets/Subscription.cs b/CryptoExchange.Net/Sockets/Subscription.cs index 353c9b7..9fd1f21 100644 --- a/CryptoExchange.Net/Sockets/Subscription.cs +++ b/CryptoExchange.Net/Sockets/Subscription.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -20,6 +19,11 @@ namespace CryptoExchange.Net.Sockets /// public int Id { get; set; } + /// + /// Can handle data + /// + public bool CanHandleData => Confirmed || HandleUpdatesBeforeConfirmation; + /// /// Total amount of invocations /// @@ -40,6 +44,11 @@ namespace CryptoExchange.Net.Sockets /// public bool Confirmed { get; set; } + /// + /// Whether this subscription should handle update messages before confirmation + /// + public bool HandleUpdatesBeforeConfirmation { get; set; } + /// /// Is the subscription closed /// @@ -116,7 +125,7 @@ namespace CryptoExchange.Net.Sockets public abstract Query? GetUnsubQuery(); /// - public virtual object Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type); + public virtual CallResult Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type); /// /// Handle an update message @@ -124,11 +133,11 @@ namespace CryptoExchange.Net.Sockets /// /// /// - public async Task HandleAsync(SocketConnection connection, DataEvent message) + public CallResult Handle(SocketConnection connection, DataEvent message) { ConnectionInvocations++; TotalInvocations++; - return await DoHandleMessageAsync(connection, message).ConfigureAwait(false); + return DoHandleMessage(connection, message); } /// @@ -137,7 +146,7 @@ namespace CryptoExchange.Net.Sockets /// /// /// - public abstract Task DoHandleMessageAsync(SocketConnection connection, DataEvent message); + public abstract CallResult DoHandleMessage(SocketConnection connection, DataEvent message); /// /// Invoke the exception event diff --git a/CryptoExchange.Net/Sockets/SystemSubscription.cs b/CryptoExchange.Net/Sockets/SystemSubscription.cs index 3fb883b..a58a80d 100644 --- a/CryptoExchange.Net/Sockets/SystemSubscription.cs +++ b/CryptoExchange.Net/Sockets/SystemSubscription.cs @@ -1,6 +1,6 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; -using CryptoExchange.Net.Sockets.MessageParsing.Interfaces; using Microsoft.Extensions.Logging; using System; using System.Threading.Tasks; @@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Sockets /// public SystemSubscription(ILogger logger, bool authenticated = false) : base(logger, authenticated, false) { + Confirmed = true; } /// @@ -35,8 +36,8 @@ namespace CryptoExchange.Net.Sockets public override Type GetMessageType(IMessageAccessor message) => typeof(T); /// - public override Task DoHandleMessageAsync(SocketConnection connection, DataEvent message) - => HandleMessageAsync(connection, message.As((T)message.Data)); + public override CallResult DoHandleMessage(SocketConnection connection, DataEvent message) + => HandleMessage(connection, message.As((T)message.Data)); /// /// ctor @@ -53,6 +54,6 @@ namespace CryptoExchange.Net.Sockets /// /// /// - public abstract Task HandleMessageAsync(SocketConnection connection, DataEvent message); + public abstract CallResult HandleMessage(SocketConnection connection, DataEvent message); } }