diff --git a/CryptoExchange.Net.UnitTests/CallResultTests.cs b/CryptoExchange.Net.UnitTests/CallResultTests.cs index dd52d41..ab75100 100644 --- a/CryptoExchange.Net.UnitTests/CallResultTests.cs +++ b/CryptoExchange.Net.UnitTests/CallResultTests.cs @@ -112,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests { var result = new WebCallResult( System.Net.HttpStatusCode.OK, - new List>>(), + new KeyValuePair[0], TimeSpan.FromSeconds(1), null, "{}", @@ -120,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests "https://test.com/api", null, HttpMethod.Get, - new List>>(), + new KeyValuePair[0], ResultDataSource.Server, new TestObjectResult(), null); @@ -142,7 +142,7 @@ namespace CryptoExchange.Net.UnitTests { var result = new WebCallResult( System.Net.HttpStatusCode.OK, - new List>>(), + new KeyValuePair[0], TimeSpan.FromSeconds(1), null, "{}", @@ -150,7 +150,7 @@ namespace CryptoExchange.Net.UnitTests "https://test.com/api", null, HttpMethod.Get, - new List>>(), + new KeyValuePair[0], ResultDataSource.Server, new TestObjectResult(), null); diff --git a/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs b/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs deleted file mode 100644 index d6d56e0..0000000 --- a/CryptoExchange.Net.UnitTests/JsonNetConverterTests.cs +++ /dev/null @@ -1,248 +0,0 @@ -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; -using System.Text; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.UnitTests -{ - [TestFixture()] - public class JsonNetConverterTests - { - [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 = JsonConvert.DeserializeObject($"{{ \"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 = JsonConvert.DeserializeObject($"{{ \"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 = JsonConvert.DeserializeObject($"{{ \"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 = JsonConvert.DeserializeObject($"{{ \"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 = JsonConvert.DeserializeObject($"{{ \"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 = JsonConvert.DeserializeObject($"{{ \"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 = JsonConvert.DeserializeObject($"{{ \"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 = JsonConvert.DeserializeObject($"{{ \"Value\": {val} }}"); - Assert.That(output.Value == expected); - } - } - - public class TimeObject - { - [JsonConverter(typeof(DateTimeConverter))] - public DateTime? Time { get; set; } - } - - public class EnumObject - { - public TestEnum? Value { get; set; } - } - - public class NotNullableEnumObject - { - public TestEnum Value { get; set; } - } - - public class BoolObject - { - [JsonConverter(typeof(BoolConverter))] - public bool? Value { get; set; } - } - - public class NotNullableBoolObject - { - [JsonConverter(typeof(BoolConverter))] - public bool Value { get; set; } - } - - [JsonConverter(typeof(EnumConverter))] - public enum TestEnum - { - [Map("1")] - One, - [Map("2")] - Two, - [Map("three", "3")] - Three, - Four - } -} diff --git a/CryptoExchange.Net.UnitTests/RestClientTests.cs b/CryptoExchange.Net.UnitTests/RestClientTests.cs index 6f1abfa..ded742f 100644 --- a/CryptoExchange.Net.UnitTests/RestClientTests.cs +++ b/CryptoExchange.Net.UnitTests/RestClientTests.cs @@ -1,14 +1,9 @@ -using CryptoExchange.Net.Authentication; -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects; using CryptoExchange.Net.UnitTests.TestImplementations; -using Newtonsoft.Json; using NUnit.Framework; using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using CryptoExchange.Net.Interfaces; -using Microsoft.Extensions.Logging; using System.Net.Http; using System.Threading.Tasks; using System.Threading; @@ -18,6 +13,7 @@ using System.Net; using CryptoExchange.Net.RateLimiting.Guards; using CryptoExchange.Net.RateLimiting.Filters; using CryptoExchange.Net.RateLimiting.Interfaces; +using System.Text.Json; namespace CryptoExchange.Net.UnitTests { @@ -30,7 +26,7 @@ namespace CryptoExchange.Net.UnitTests // arrange var client = new TestRestClient(); var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" }; - client.SetResponse(JsonConvert.SerializeObject(expected), out _); + client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _); // act var result = client.Api1.Request().Result; @@ -84,8 +80,6 @@ namespace CryptoExchange.Net.UnitTests 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] @@ -140,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests client.SetResponse("{}", out var request); - await client.Api1.RequestWithParams(new HttpMethod(method), new Dictionary + await client.Api1.RequestWithParams(new HttpMethod(method), new ParameterCollection { { "TestParam1", "Value1" }, { "TestParam2", 2 }, diff --git a/CryptoExchange.Net.UnitTests/SocketClientTests.cs b/CryptoExchange.Net.UnitTests/SocketClientTests.cs index 51c157a..7f85384 100644 --- a/CryptoExchange.Net.UnitTests/SocketClientTests.cs +++ b/CryptoExchange.Net.UnitTests/SocketClientTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Objects; @@ -9,7 +10,6 @@ using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.TestImplementations.Sockets; using Microsoft.Extensions.Logging; using Moq; -using Newtonsoft.Json; using NUnit.Framework; using NUnit.Framework.Legacy; @@ -103,7 +103,7 @@ namespace CryptoExchange.Net.UnitTests rstEvent.Set(); }); sub.AddSubscription(subObj); - var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", action = "update", property = 123 }); + var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" }); // act socket.InvokeMessage(msgToSend); @@ -198,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests // act var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); - socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "error" })); + socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" })); await sub; // assert @@ -221,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests // act var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default); - socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "confirmed" })); + socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" })); await sub; // assert diff --git a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs index ac390a5..68891d3 100644 --- a/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs +++ b/CryptoExchange.Net.UnitTests/SystemTextJsonConverterTests.cs @@ -146,7 +146,7 @@ namespace CryptoExchange.Net.UnitTests public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected) { var val = value == null ? "null" : $"\"{value}\""; - var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext())); Assert.That(output.Value == expected); } @@ -171,8 +171,8 @@ namespace CryptoExchange.Net.UnitTests [TestCase("three", TestEnum.Three)] [TestCase("Four", TestEnum.Four)] [TestCase("four", TestEnum.Four)] - [TestCase("Four1", TestEnum.One)] - [TestCase(null, TestEnum.One)] + [TestCase("Four1", null)] + [TestCase(null, null)] public void TestEnumConverterParseStringTests(string value, TestEnum? expected) { var result = EnumConverter.ParseString(value); @@ -194,7 +194,7 @@ namespace CryptoExchange.Net.UnitTests public void TestBoolConverter(string value, bool? expected) { var val = value == null ? "null" : $"\"{value}\""; - var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext())); Assert.That(output.Value == expected); } @@ -213,7 +213,7 @@ namespace CryptoExchange.Net.UnitTests public void TestBoolConverterNotNullable(string value, bool expected) { var val = value == null ? "null" : $"\"{value}\""; - var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}"); + var output = JsonSerializer.Deserialize($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext())); Assert.That(output.Value == expected); } @@ -265,9 +265,22 @@ namespace CryptoExchange.Net.UnitTests Prop31 = 4, Prop32 = "789" }, - Prop7 = TestEnum.Two + Prop7 = TestEnum.Two, + TestInternal = new Test + { + Prop1 = 10 + }, + Prop8 = new Test3 + { + Prop31 = 5, + Prop32 = "101" + }, }; + var options = new JsonSerializerOptions() + { + TypeInfoResolver = new SerializationContext() + }; var serialized = JsonSerializer.Serialize(data); var deserialized = JsonSerializer.Deserialize(serialized); @@ -281,6 +294,9 @@ namespace CryptoExchange.Net.UnitTests Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4)); Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789")); Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two)); + Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10)); + Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5)); + Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101")); } } @@ -300,29 +316,25 @@ namespace CryptoExchange.Net.UnitTests 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; } } - [JsonConverter(typeof(ArrayConverter))] + [JsonConverter(typeof(ArrayConverter))] record Test { [ArrayProperty(0)] @@ -339,11 +351,15 @@ namespace CryptoExchange.Net.UnitTests public Test2 Prop5 { get; set; } [ArrayProperty(5)] public Test3 Prop6 { get; set; } - [ArrayProperty(6), JsonConverter(typeof(EnumConverter))] + [ArrayProperty(6), JsonConverter(typeof(EnumConverter))] public TestEnum? Prop7 { get; set; } + [ArrayProperty(7)] + public Test TestInternal { get; set; } + [ArrayProperty(8), JsonConversion] + public Test3 Prop8 { get; set; } } - [JsonConverter(typeof(ArrayConverter))] + [JsonConverter(typeof(ArrayConverter))] record Test2 { [ArrayProperty(0)] @@ -359,4 +375,29 @@ namespace CryptoExchange.Net.UnitTests [JsonPropertyName("prop32")] public string Prop32 { get; set; } } + + [JsonConverter(typeof(EnumConverter))] + public enum TestEnum + { + [Map("1")] + One, + [Map("2")] + Two, + [Map("three", "3")] + Three, + Four + } + + [JsonSerializable(typeof(Test))] + [JsonSerializable(typeof(Test2))] + [JsonSerializable(typeof(Test3))] + [JsonSerializable(typeof(NotNullableSTJBoolObject))] + [JsonSerializable(typeof(STJBoolObject))] + [JsonSerializable(typeof(NotNullableSTJEnumObject))] + [JsonSerializable(typeof(STJEnumObject))] + [JsonSerializable(typeof(STJDecimalObject))] + [JsonSerializable(typeof(STJTimeObject))] + internal partial class SerializationContext : JsonSerializerContext + { + } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs index 12ff600..0af9b97 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/Sockets/TestChannelQuery.cs @@ -1,32 +1,31 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Sockets; -using Newtonsoft.Json; using System; using System.Collections.Generic; -using System.Threading.Tasks; +using System.Text.Json.Serialization; namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets { internal class SubResponse { - [JsonProperty("action")] + [JsonPropertyName("action")] public string Action { get; set; } = null!; - [JsonProperty("channel")] + [JsonPropertyName("channel")] public string Channel { get; set; } = null!; - [JsonProperty("status")] + [JsonPropertyName("status")] public string Status { get; set; } = null!; } internal class UnsubResponse { - [JsonProperty("action")] + [JsonPropertyName("action")] public string Action { get; set; } = null!; - [JsonProperty("status")] + [JsonPropertyName("status")] public string Status { get; set; } = null!; } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs index 57abfe8..52dd6a4 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestBaseClient.cs @@ -3,14 +3,18 @@ using System.Collections.Generic; using System.IO; using System.Net.Http; using System.Text; +using System.Text.Json.Serialization; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Converters.SystemTextJson; +using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.UnitTests.TestImplementations; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace CryptoExchange.Net.UnitTests { @@ -21,12 +25,14 @@ namespace CryptoExchange.Net.UnitTests public TestBaseClient(): base(null, "Test") { var options = new TestClientOptions(); + _logger = NullLogger.Instance; Initialize(options); SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions())); } public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test") { + _logger = NullLogger.Instance; Initialize(exchangeOptions); SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions())); } @@ -59,6 +65,8 @@ namespace CryptoExchange.Net.UnitTests public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; public override TimeSpan? GetTimeOffset() => null; public override TimeSyncInfo GetTimeSyncInfo() => null; + protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions()); + protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions()); protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException(); protected override Task> GetServerTimestampAsync() => throw new NotImplementedException(); } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs index 0ad099d..7827d0c 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestObjects.cs @@ -1,12 +1,14 @@ -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace CryptoExchange.Net.UnitTests.TestImplementations { public class TestObject { - [JsonProperty("other")] + [JsonPropertyName("other")] public string StringData { get; set; } + [JsonPropertyName("intData")] public int IntData { get; set; } + [JsonPropertyName("decimalData")] public decimal DecimalData { get; set; } } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs index 8d1ef36..f909ed9 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestRestClient.cs @@ -1,7 +1,6 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using Moq; -using Newtonsoft.Json.Linq; using System; using System.IO; using System.Net; @@ -12,11 +11,13 @@ using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Authentication; using System.Collections.Generic; -using CryptoExchange.Net.Objects.Options; using Microsoft.Extensions.Logging; using CryptoExchange.Net.Clients; using CryptoExchange.Net.SharedApis; using Microsoft.Extensions.Options; +using System.Linq; +using CryptoExchange.Net.Converters.SystemTextJson; +using System.Text.Json.Serialization; namespace CryptoExchange.Net.UnitTests.TestImplementations { @@ -49,13 +50,13 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations response.Setup(c => c.IsSuccessStatusCode).Returns(true); response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream)); - var headers = new Dictionary>(); + var headers = new Dictionary(); var request = new Mock(); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.GetResponseAsync(It.IsAny())).Returns(Task.FromResult(response.Object)); request.Setup(c => c.SetContent(It.IsAny(), It.IsAny())).Callback(new Action((content, type) => { request.Setup(r => r.Content).Returns(content); })); - request.Setup(c => c.AddHeader(It.IsAny(), It.IsAny())).Callback((key, val) => headers.Add(key, new List { val })); - request.Setup(c => c.GetHeaders()).Returns(() => headers); + request.Setup(c => c.AddHeader(It.IsAny(), It.IsAny())).Callback((key, val) => headers.Add(key, new string[] { val })); + request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray()); var factory = Mock.Get(Api1.RequestFactory); factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) @@ -84,7 +85,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations var request = new Mock(); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); - request.Setup(c => c.GetHeaders()).Returns(new Dictionary>()); + request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair[0]); request.Setup(c => c.GetResponseAsync(It.IsAny())).Throws(we); var factory = Mock.Get(Api1.RequestFactory); @@ -108,12 +109,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations response.Setup(c => c.IsSuccessStatusCode).Returns(false); response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream)); - var headers = new Dictionary>(); + var headers = new List>(); var request = new Mock(); request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com")); request.Setup(c => c.GetResponseAsync(It.IsAny())).Returns(Task.FromResult(response.Object)); - request.Setup(c => c.AddHeader(It.IsAny(), It.IsAny())).Callback((key, val) => headers.Add(key, new List { val })); - request.Setup(c => c.GetHeaders()).Returns(headers); + request.Setup(c => c.AddHeader(It.IsAny(), It.IsAny())).Callback((key, val) => headers.Add(new KeyValuePair(key, new string[] { val }))); + request.Setup(c => c.GetHeaders()).Returns(headers.ToArray()); var factory = Mock.Get(Api1.RequestFactory); factory.Setup(c => c.Create(It.IsAny(), It.IsAny(), It.IsAny())) @@ -137,14 +138,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations /// public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; + protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() }); + protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions()); + public async Task> Request(CancellationToken ct = default) where T : class { - return await SendRequestAsync(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0); + return await SendAsync("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct); } - public async Task> RequestWithParams(HttpMethod method, Dictionary parameters, Dictionary headers) where T : class + public async Task> RequestWithParams(HttpMethod method, ParameterCollection parameters, Dictionary headers) where T : class { - return await SendRequestAsync(new Uri("http://www.test.com"), method, default, parameters, requestWeight: 0, additionalHeaders: headers); + return await SendAsync("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers); } public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position) @@ -178,15 +182,18 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations RequestFactory = new Mock().Object; } + protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions()); + protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions()); + /// public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; public async Task> Request(CancellationToken ct = default) where T : class { - return await SendRequestAsync(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0); + return await SendAsync("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct); } - protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable>> responseHeaders, IMessageAccessor accessor) + protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception exception) { var errorData = accessor.Deserialize(); @@ -214,7 +221,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations public class TestError { + [JsonPropertyName("errorCode")] public int ErrorCode { get; set; } + [JsonPropertyName("errorMessage")] public string ErrorMessage { get; set; } } diff --git a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs index d74060e..11080ec 100644 --- a/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs +++ b/CryptoExchange.Net.UnitTests/TestImplementations/TestSocketClient.cs @@ -16,6 +16,7 @@ using Moq; using CryptoExchange.Net.Testing.Implementations; using CryptoExchange.Net.SharedApis; using Microsoft.Extensions.Options; +using CryptoExchange.Net.Converters.SystemTextJson; namespace CryptoExchange.Net.UnitTests.TestImplementations { @@ -97,6 +98,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations } + protected internal override IByteMessageAccessor CreateAccessor() => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions()); + protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions()); + /// public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}"; diff --git a/CryptoExchange.Net.UnitTests/TestSerializerContext.cs b/CryptoExchange.Net.UnitTests/TestSerializerContext.cs new file mode 100644 index 0000000..e3dfe5b --- /dev/null +++ b/CryptoExchange.Net.UnitTests/TestSerializerContext.cs @@ -0,0 +1,21 @@ +using CryptoExchange.Net.UnitTests.TestImplementations; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.UnitTests +{ + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(IDictionary))] + [JsonSerializable(typeof(TestObject))] + internal partial class TestSerializerContext : JsonSerializerContext + { + } +} diff --git a/CryptoExchange.Net/Authentication/ApiCredentials.cs b/CryptoExchange.Net/Authentication/ApiCredentials.cs index 8f16d26..3542b3b 100644 --- a/CryptoExchange.Net/Authentication/ApiCredentials.cs +++ b/CryptoExchange.Net/Authentication/ApiCredentials.cs @@ -20,6 +20,11 @@ namespace CryptoExchange.Net.Authentication /// public string Secret { get; set; } + /// + /// The api passphrase. Not needed on all exchanges + /// + public string? Pass { get; set; } + /// /// Type of the credentials /// @@ -30,8 +35,9 @@ namespace CryptoExchange.Net.Authentication /// /// The api key / label used for identification /// The api secret or private key used for signing + /// The api pass for the key. Not always needed /// The type of credentials - public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac) + public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac) { if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret)) throw new ArgumentException("Key and secret can't be null/empty"); @@ -39,6 +45,7 @@ namespace CryptoExchange.Net.Authentication CredentialType = credentialType; Key = key; Secret = secret; + Pass = pass; } /// @@ -47,28 +54,7 @@ namespace CryptoExchange.Net.Authentication /// public virtual ApiCredentials Copy() { - return new ApiCredentials(Key, Secret, CredentialType); - } - - /// - /// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret - /// - /// The stream containing the json data - /// A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'. - /// 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 static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null) - { - var accessor = new SystemTextJsonStreamMessageAccessor(); - if (!accessor.Read(inputStream, false).Result) - throw new ArgumentException("Input stream not valid json data"); - - 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"); - - inputStream.Seek(0, SeekOrigin.Begin); - return new ApiCredentials(key, secret); + return new ApiCredentials(Key, Secret, Pass, CredentialType); } } } diff --git a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs index 6920e98..ede3cf0 100644 --- a/CryptoExchange.Net/Authentication/AuthenticationProvider.cs +++ b/CryptoExchange.Net/Authentication/AuthenticationProvider.cs @@ -32,6 +32,10 @@ namespace CryptoExchange.Net.Authentication /// Get the API key of the current credentials /// public string ApiKey => _credentials.Key!; + /// + /// Get the Passphrase of the current credentials + /// + public string? Pass => _credentials.Pass; /// /// ctor diff --git a/CryptoExchange.Net/Caching/MemoryCache.cs b/CryptoExchange.Net/Caching/MemoryCache.cs index 143e07d..ca2c3c4 100644 --- a/CryptoExchange.Net/Caching/MemoryCache.cs +++ b/CryptoExchange.Net/Caching/MemoryCache.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Concurrent; +using System.Linq; namespace CryptoExchange.Net.Caching { internal class MemoryCache { private readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + private readonly object _lock = new object(); /// /// Add a new cache entry. Will override an existing entry if it already exists @@ -26,16 +28,13 @@ namespace CryptoExchange.Net.Caching /// Cached value if it was in cache public object? Get(string key, TimeSpan maxAge) { + foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList()) + _cache.TryRemove(item.Key, out _); + _cache.TryGetValue(key, out CacheItem? value); if (value == null) return null; - if (DateTime.UtcNow - value.CacheTime > maxAge) - { - _cache.TryRemove(key, out _); - return null; - } - return value.Value; } diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs index 50b03c4..a4f64c0 100644 --- a/CryptoExchange.Net/Clients/BaseApiClient.cs +++ b/CryptoExchange.Net/Clients/BaseApiClient.cs @@ -39,7 +39,10 @@ namespace CryptoExchange.Net.Clients public bool OutputOriginalData { get; } /// - public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null; + public bool Authenticated => ApiCredentials != null; + + /// + public ApiCredentials? ApiCredentials { get; set; } /// /// Api options @@ -68,9 +71,10 @@ namespace CryptoExchange.Net.Clients ApiOptions = apiOptions; OutputOriginalData = outputOriginalData; BaseAddress = baseAddress; + ApiCredentials = apiCredentials?.Copy(); - if (apiCredentials != null) - AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy()); + if (ApiCredentials != null) + AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); } /// @@ -86,9 +90,9 @@ namespace CryptoExchange.Net.Clients /// public void SetApiCredentials(T credentials) where T : ApiCredentials { - ApiOptions.ApiCredentials = credentials; - if (credentials != null) - AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy()); + ApiCredentials = credentials?.Copy(); + if (ApiCredentials != null) + AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); } /// @@ -97,9 +101,9 @@ namespace CryptoExchange.Net.Clients ClientOptions.Proxy = options.Proxy; ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout; - ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials; - if (options.ApiCredentials != null) - AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy()); + ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials; + if (ApiCredentials != null) + AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials); } /// diff --git a/CryptoExchange.Net/Clients/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs index 842c77c..7523423 100644 --- a/CryptoExchange.Net/Clients/BaseClient.cs +++ b/CryptoExchange.Net/Clients/BaseClient.cs @@ -66,8 +66,6 @@ namespace CryptoExchange.Net.Clients 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(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange); - Exchange = exchange; } diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs index 6ff8b03..bc5464b 100644 --- a/CryptoExchange.Net/Clients/BaseRestClient.cs +++ b/CryptoExchange.Net/Clients/BaseRestClient.cs @@ -1,6 +1,7 @@ using System.Linq; using CryptoExchange.Net.Interfaces; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace CryptoExchange.Net.Clients { @@ -19,6 +20,7 @@ namespace CryptoExchange.Net.Clients /// The name of the API this client is for protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name) { + _logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name); } } } diff --git a/CryptoExchange.Net/Clients/BaseSocketClient.cs b/CryptoExchange.Net/Clients/BaseSocketClient.cs index e376a41..38f34e7 100644 --- a/CryptoExchange.Net/Clients/BaseSocketClient.cs +++ b/CryptoExchange.Net/Clients/BaseSocketClient.cs @@ -3,10 +3,12 @@ using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; +using System.Xml.Linq; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects.Sockets; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace CryptoExchange.Net.Clients { @@ -33,10 +35,11 @@ namespace CryptoExchange.Net.Clients /// /// ctor /// - /// Logger - /// The name of the exchange this client is for - protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange) + /// Logger factory + /// The name of the exchange this client is for + protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name) { + _logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name); } /// diff --git a/CryptoExchange.Net/Clients/CryptoRestClient.cs b/CryptoExchange.Net/Clients/CryptoRestClient.cs index c1255aa..d4ee0bb 100644 --- a/CryptoExchange.Net/Clients/CryptoRestClient.cs +++ b/CryptoExchange.Net/Clients/CryptoRestClient.cs @@ -1,5 +1,4 @@ -using CryptoExchange.Net.Interfaces; -using CryptoExchange.Net.Interfaces.CommonClients; +using CryptoExchange.Net.Interfaces; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; @@ -24,24 +23,5 @@ namespace CryptoExchange.Net.Clients public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider) { } - - /// - /// Get a list of the registered ISpotClient implementations - /// - /// - public IEnumerable GetSpotClients() - { - if (_serviceProvider == null) - return new List(); - - return _serviceProvider.GetServices().ToList(); - } - - /// - /// Get an ISpotClient implementation by exchange name - /// - /// - /// - public ISpotClient? SpotClient(string exchangeName) => _serviceProvider?.GetServices()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase)); } -} +} \ No newline at end of file diff --git a/CryptoExchange.Net/Clients/RestApiClient.cs b/CryptoExchange.Net/Clients/RestApiClient.cs index 0ee0af1..0c1b199 100644 --- a/CryptoExchange.Net/Clients/RestApiClient.cs +++ b/CryptoExchange.Net/Clients/RestApiClient.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Net; @@ -9,7 +8,6 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using CryptoExchange.Net.Caching; -using CryptoExchange.Net.Converters.JsonNet; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Objects; @@ -114,13 +112,13 @@ namespace CryptoExchange.Net.Clients /// Create a message accessor instance /// /// - protected virtual IStreamMessageAccessor CreateAccessor() => new JsonNetStreamMessageAccessor(); + protected abstract IStreamMessageAccessor CreateAccessor(); /// /// Create a serializer instance /// /// - protected virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer(); + protected abstract IMessageSerializer CreateSerializer(); /// /// Send a request to the base address based on the request definition @@ -243,10 +241,11 @@ namespace CryptoExchange.Net.Clients var result = await GetResponseAsync(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); if (result.Error is not CancellationRequestedError) { + var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"; if (!result) - _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString()); + _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception); else - _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"); + _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData); } else { @@ -343,7 +342,7 @@ namespace CryptoExchange.Net.Clients } } - return new CallResult(null); + return CallResult.SuccessResult; } /// @@ -437,215 +436,6 @@ namespace CryptoExchange.Net.Clients return request; } - /// - /// Execute a request to the uri and returns if it was successful - /// - /// The uri to send the request to - /// The method of the request - /// Cancellation token - /// The parameters of the request - /// Whether or not the request should be authenticated - /// The format of the body content - /// 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 - /// Additional headers to send with the request - /// The ratelimit gate to use - /// - [return: NotNull] - protected virtual async Task SendRequestAsync( - Uri uri, - HttpMethod method, - CancellationToken cancellationToken, - Dictionary? parameters = null, - bool signed = false, - RequestBodyFormat? requestBodyFormat = null, - HttpMethodParameterPosition? parameterPosition = null, - ArrayParametersSerialization? arraySerialization = null, - int requestWeight = 1, - Dictionary? additionalHeaders = null, - IRateLimitGate? gate = null) - { - int currentTry = 0; - while (true) - { - currentTry++; - var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false); - if (!request) - return new WebCallResult(request.Error!); - - var result = await GetResponseAsync(request.Data, gate, cancellationToken).ConfigureAwait(false); - if (!result) - _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString()); - else - _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"); - - if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false)) - continue; - - return result.AsDataless(); - } - } - - /// - /// Execute a request to the uri and deserialize the response into the provided type parameter - /// - /// The type to deserialize into - /// The uri to send the request to - /// The method of the request - /// Cancellation token - /// The parameters of the request - /// Whether or not the request should be authenticated - /// The format of the body content - /// 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 - /// Additional headers to send with the request - /// The ratelimit gate to use - /// Whether caching should be prevented for this request - /// - [return: NotNull] - protected virtual async Task> SendRequestAsync( - Uri uri, - HttpMethod method, - CancellationToken cancellationToken, - Dictionary? parameters = null, - bool signed = false, - RequestBodyFormat? requestBodyFormat = null, - HttpMethodParameterPosition? parameterPosition = null, - ArrayParametersSerialization? arraySerialization = null, - int requestWeight = 1, - Dictionary? additionalHeaders = null, - IRateLimitGate? gate = null, - bool preventCaching = false - ) where T : class - { - var key = uri.ToString() + method + signed + parameters?.ToFormData(); - if (ShouldCache(method) && !preventCaching) - { - _logger.CheckingCache(key); - var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge); - if (cachedValue != null) - { - _logger.CacheHit(key); - var original = (WebCallResult)cachedValue; - return original.Cached(); - } - - _logger.CacheNotHit(key); - } - - int currentTry = 0; - while (true) - { - currentTry++; - var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, requestBodyFormat, parameterPosition, arraySerialization, requestWeight, additionalHeaders, gate).ConfigureAwait(false); - if (!request) - return new WebCallResult(request.Error!); - - var result = await GetResponseAsync(request.Data, gate, cancellationToken).ConfigureAwait(false); - if (!result) - _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString()); - else - _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"); - - if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false)) - continue; - - if (result.Success && - ShouldCache(method) && - !preventCaching) - { - _cache.Add(key, result); - } - - return result; - } - } - - /// - /// Prepares a request to be sent to the server - /// - /// The uri to send the request to - /// The method of the request - /// Cancellation token - /// The parameters of the request - /// Whether or not the request should be authenticated - /// The format of the body content - /// 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 - /// Additional headers to send with the request - /// The rate limit gate to use - /// - protected virtual async Task> PrepareRequestAsync( - Uri uri, - HttpMethod method, - CancellationToken cancellationToken, - Dictionary? parameters = null, - bool signed = false, - RequestBodyFormat? requestBodyFormat = null, - HttpMethodParameterPosition? parameterPosition = null, - ArrayParametersSerialization? arraySerialization = null, - int requestWeight = 1, - Dictionary? additionalHeaders = null, - IRateLimitGate? gate = null) - { - var requestId = ExchangeHelpers.NextId(); - - if (signed) - { - if (AuthenticationProvider == null) - { - _logger.RestApiNoApiCredentials(requestId, uri.AbsolutePath); - return new CallResult(new NoApiCredentialsError()); - } - - var syncTask = SyncTimeAsync(); - var timeSyncInfo = GetTimeSyncInfo(); - - if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default) - { - // Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue - var syncTimeResult = await syncTask.ConfigureAwait(false); - if (!syncTimeResult) - { - _logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString()); - return syncTimeResult.As(default); - } - } - } - - if (requestWeight != 0) - { - if (gate == null) - throw new Exception("Ratelimit gate not set when request weight is not 0"); - - if (ClientOptions.RateLimiterEnabled) - { - var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, null, cancellationToken).ConfigureAwait(false); - if (!limitResult) - return new CallResult(limitResult.Error!); - } - } - - _logger.RestApiCreatingRequest(requestId, 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 ?? ArraySerialization, requestBodyFormat ?? RequestBodyFormat, requestId, additionalHeaders); - - string? paramString = ""; - if (paramsPosition == HttpMethodParameterPosition.InBody) - paramString = $" with request body '{request.Content}'"; - - var headers = request.GetHeaders(); - if (headers.Count != 0) - paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")); - - TotalRequestsMade++; - _logger.RestApiSendingRequest(requestId, method, signed ? "signed": "", request.Uri, paramString); - return new CallResult(request); - } - /// /// Executes the request and returns the result deserialized into the type parameter class /// @@ -676,7 +466,7 @@ namespace CryptoExchange.Net.Clients if (!response.IsSuccessStatusCode) { // Error response - await accessor.Read(responseStream, true).ConfigureAwait(false); + var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false); Error error; if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) @@ -692,7 +482,7 @@ namespace CryptoExchange.Net.Clients } else { - error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor); + error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception); } if (error.Code == null || error.Code == 0) @@ -701,15 +491,15 @@ namespace CryptoExchange.Net.Clients 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(), ResultDataSource.Server, default, error!); } + var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); 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(), ResultDataSource.Server, default, null); + return new WebCallResult(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); - var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false); if (!valid) { // Invalid json - var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"); + var error = new DeserializeError("Failed to parse response: " + valid.Error!.Message, valid.Error.Exception); 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(), ResultDataSource.Server, default, error); } @@ -736,20 +526,19 @@ namespace CryptoExchange.Net.Clients catch (HttpRequestException requestException) { // Request exception, can't reach server for instance - var exceptionInfo = requestException.ToLogString(); - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo)); + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(requestException.Message, exception: requestException)); } catch (OperationCanceledException canceledException) { if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) { // Cancellation token canceled by caller - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError()); + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); } else { // Request timed out - return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out")); + return new WebCallResult(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out", exception: canceledException)); } } finally @@ -768,7 +557,7 @@ namespace CryptoExchange.Net.Clients /// Data accessor /// The response headers /// Null if not an error, Error otherwise - protected virtual Error? TryParseError(IEnumerable>> responseHeaders, IMessageAccessor accessor) => null; + protected virtual Error? TryParseError(KeyValuePair[] responseHeaders, 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. @@ -804,112 +593,6 @@ namespace CryptoExchange.Net.Clients return false; } - /// - /// Creates a request object - /// - /// The uri to send the request to - /// The method of the request - /// The parameters of the request - /// Whether or not the request should be authenticated - /// Where the parameters should be placed - /// How array parameters should be serialized - /// Format of the body content - /// Unique id of a request - /// Additional headers to send with the request - /// - protected virtual IRequest ConstructRequest( - Uri uri, - HttpMethod method, - Dictionary? parameters, - bool signed, - HttpMethodParameterPosition parameterPosition, - ArrayParametersSerialization arraySerialization, - RequestBodyFormat bodyFormat, - int requestId, - Dictionary? additionalHeaders) - { - parameters ??= new Dictionary(); - - for (var i = 0; i < parameters.Count; i++) - { - var kvp = parameters.ElementAt(i); - if (kvp.Value is Func delegateValue) - parameters[kvp.Key] = delegateValue(); - } - - if (parameterPosition == HttpMethodParameterPosition.InUri) - { - foreach (var parameter in parameters) - uri = uri.AddQueryParameter(parameter.Key, parameter.Value.ToString()!); - } - - var headers = new Dictionary(); - var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : null; - var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : null; - if (AuthenticationProvider != null) - { - try - { - AuthenticationProvider.AuthenticateRequest( - this, - uri, - method, - ref uriParameters, - ref bodyParameters, - ref headers, - signed, - arraySerialization, - parameterPosition, - bodyFormat - ); - } - catch (Exception ex) - { - throw new Exception("Failed to authenticate request, make sure your API credentials are correct", ex); - } - } - - // Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters - if (uriParameters != null) - uri = uri.SetParameters(uriParameters, arraySerialization); - - var request = RequestFactory.Create(method, uri, requestId); - request.Accept = Constants.JsonContentHeader; - - if (headers != null) - { - foreach (var header in headers) - request.AddHeader(header.Key, header.Value); - } - - if (additionalHeaders != null) - { - foreach (var header in additionalHeaders) - request.AddHeader(header.Key, header.Value); - } - - if (StandardRequestHeaders != null) - { - foreach (var header in StandardRequestHeaders) - { - // Only add it if it isn't overwritten - if (additionalHeaders?.ContainsKey(header.Key) != true) - request.AddHeader(header.Key, header.Value); - } - } - - if (parameterPosition == HttpMethodParameterPosition.InBody) - { - var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; - if (bodyParameters?.Any() == true) - WriteParamBody(request, bodyParameters, contentType); - else - request.SetContent(RequestBodyEmptyContent, contentType); - } - - return request; - } - /// /// Writes the parameters of the request to the request object body /// @@ -942,11 +625,11 @@ namespace CryptoExchange.Net.Clients /// The response status code /// The response headers /// Data accessor + /// Exception /// - protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable>> responseHeaders, IMessageAccessor accessor) + protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair[] responseHeaders, IMessageAccessor accessor, Exception? exception) { - var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]"; - return new ServerError(message); + return new ServerError(null, "Unknown request error", exception); } /// @@ -956,23 +639,21 @@ namespace CryptoExchange.Net.Clients /// The response headers /// Data accessor /// - protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, IEnumerable>> responseHeaders, IMessageAccessor accessor) + protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair[] 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(message); + return new ServerRateLimitError(); var value = retryAfterHeader.Value.First(); if (int.TryParse(value, out var seconds)) - return new ServerRateLimitError(message) { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; + return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) }; if (DateTime.TryParse(value, out var datetime)) - return new ServerRateLimitError(message) { RetryAfter = datetime }; + return new ServerRateLimitError() { RetryAfter = datetime }; - return new ServerRateLimitError(message); + return new ServerRateLimitError(); } /// @@ -1049,9 +730,5 @@ namespace CryptoExchange.Net.Clients => ClientOptions.CachingEnabled && definition.Method == HttpMethod.Get && !definition.PreventCaching; - - private bool ShouldCache(HttpMethod method) - => ClientOptions.CachingEnabled - && method == HttpMethod.Get; } } diff --git a/CryptoExchange.Net/Clients/SocketApiClient.cs b/CryptoExchange.Net/Clients/SocketApiClient.cs index 5098b7b..ac0cf25 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.Logging.Extensions; using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.Sockets; using Microsoft.Extensions.Logging; @@ -42,6 +42,11 @@ namespace CryptoExchange.Net.Clients /// protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10); + /// + /// Keep alive timeout for websocket connection + /// + protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10); + /// /// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example. /// @@ -133,13 +138,13 @@ namespace CryptoExchange.Net.Clients /// Create a message accessor instance /// /// - protected internal virtual IByteMessageAccessor CreateAccessor() => new JsonNetByteMessageAccessor(); + protected internal abstract IByteMessageAccessor CreateAccessor(); /// /// Create a serializer instance /// /// - protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer(); + protected internal abstract IMessageSerializer CreateSerializer(); /// /// Keep an open connection to this url @@ -206,9 +211,9 @@ namespace CryptoExchange.Net.Clients { await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false); } - catch (OperationCanceledException) + catch (OperationCanceledException tce) { - return new CallResult(new CancellationRequestedError()); + return new CallResult(new CancellationRequestedError(tce)); } try @@ -378,7 +383,7 @@ namespace CryptoExchange.Net.Clients protected virtual async Task ConnectIfNeededAsync(SocketConnection socket, bool authenticated) { if (socket.Connected) - return new CallResult(null); + return CallResult.SuccessResult; var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false); if (!connectResult) @@ -388,7 +393,7 @@ namespace CryptoExchange.Net.Clients await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false); if (!authenticated || socket.Authenticated) - return new CallResult(null); + return CallResult.SuccessResult; var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false); if (!result) @@ -427,7 +432,7 @@ namespace CryptoExchange.Net.Clients } socket.Authenticated = true; - return new CallResult(null); + return CallResult.SuccessResult; } /// @@ -475,7 +480,7 @@ namespace CryptoExchange.Net.Clients /// protected internal virtual Task RevitalizeRequestAsync(Subscription subscription) { - return Task.FromResult(new CallResult(null)); + return Task.FromResult(CallResult.SuccessResult); } /// @@ -561,11 +566,12 @@ namespace CryptoExchange.Net.Clients /// protected async virtual Task HandleConnectRateLimitedAsync() { - if (ClientOptions.RateLimiterEnabled && RateLimiter is not null && ClientOptions.ConnectDelayAfterRateLimited is not null) + if (ClientOptions.RateLimiterEnabled && ClientOptions.ConnectDelayAfterRateLimited.HasValue) { var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value); _logger.AddingRetryAfterGuard(retryAfter); - await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimiting.RateLimitItemType.Connection).ConfigureAwait(false); + RateLimiter ??= new RateLimitGate("Connection"); + await RateLimiter.SetRetryAfterGuardAsync(retryAfter, RateLimitItemType.Connection).ConfigureAwait(false); } } @@ -596,6 +602,7 @@ namespace CryptoExchange.Net.Clients => new(new Uri(address), ClientOptions.ReconnectPolicy) { KeepAliveInterval = KeepAliveInterval, + KeepAliveTimeout = KeepAliveTimeout, ReconnectInterval = ClientOptions.ReconnectInterval, RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null, RateLimitingBehavior = ClientOptions.RateLimitingBehaviour, @@ -712,7 +719,7 @@ namespace CryptoExchange.Net.Clients return new CallResult(connectResult.Error!); } - return new CallResult(null); + return CallResult.SuccessResult; } /// diff --git a/CryptoExchange.Net/CommonObjects/Balance.cs b/CryptoExchange.Net/CommonObjects/Balance.cs deleted file mode 100644 index 696a35c..0000000 --- a/CryptoExchange.Net/CommonObjects/Balance.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Balance data - /// - public class Balance: BaseCommonObject - { - /// - /// The asset name - /// - public string Asset { get; set; } = string.Empty; - /// - /// Quantity available - /// - public decimal? Available { get; set; } - /// - /// Total quantity - /// - public decimal? Total { get; set; } - } -} diff --git a/CryptoExchange.Net/CommonObjects/BaseComonObject.cs b/CryptoExchange.Net/CommonObjects/BaseComonObject.cs deleted file mode 100644 index 88bf356..0000000 --- a/CryptoExchange.Net/CommonObjects/BaseComonObject.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Base class for common objects - /// - public class BaseCommonObject - { - /// - /// The source object the data is derived from - /// - public object SourceObject { get; set; } = null!; - } -} diff --git a/CryptoExchange.Net/CommonObjects/Enums.cs b/CryptoExchange.Net/CommonObjects/Enums.cs deleted file mode 100644 index 1d73e03..0000000 --- a/CryptoExchange.Net/CommonObjects/Enums.cs +++ /dev/null @@ -1,73 +0,0 @@ -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Order type - /// - public enum CommonOrderType - { - /// - /// Limit type - /// - Limit, - /// - /// Market type - /// - Market, - /// - /// Other order type - /// - Other - } - - /// - /// Order side - /// - public enum CommonOrderSide - { - /// - /// Buy order - /// - Buy, - /// - /// Sell order - /// - Sell - } - /// - /// Order status - /// - public enum CommonOrderStatus - { - /// - /// placed and not fully filled order - /// - Active, - /// - /// canceled order - /// - Canceled, - /// - /// filled order - /// - Filled - } - - /// - /// Position side - /// - public enum CommonPositionSide - { - /// - /// Long position - /// - Long, - /// - /// Short position - /// - Short, - /// - /// Both - /// - Both - } -} diff --git a/CryptoExchange.Net/CommonObjects/Kline.cs b/CryptoExchange.Net/CommonObjects/Kline.cs deleted file mode 100644 index ae01746..0000000 --- a/CryptoExchange.Net/CommonObjects/Kline.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Kline data - /// - public class Kline: BaseCommonObject - { - /// - /// Opening time of the kline - /// - public DateTime OpenTime { get; set; } - /// - /// Price at the open time - /// - public decimal? OpenPrice { get; set; } - /// - /// Highest price of the kline - /// - public decimal? HighPrice { get; set; } - /// - /// Lowest price of the kline - /// - public decimal? LowPrice { get; set; } - /// - /// Close price of the kline - /// - public decimal? ClosePrice { get; set; } - /// - /// Volume of the kline - /// - public decimal? Volume { get; set; } - } -} diff --git a/CryptoExchange.Net/CommonObjects/Order.cs b/CryptoExchange.Net/CommonObjects/Order.cs deleted file mode 100644 index 9d630c0..0000000 --- a/CryptoExchange.Net/CommonObjects/Order.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Order data - /// - public class Order: BaseCommonObject - { - /// - /// Id of the order - /// - public string Id { get; set; } = string.Empty; - /// - /// Symbol of the order - /// - public string Symbol { get; set; } = string.Empty; - /// - /// Price of the order - /// - public decimal? Price { get; set; } - /// - /// Quantity of the order - /// - public decimal? Quantity { get; set; } - /// - /// The quantity of the order which has been filled - /// - public decimal? QuantityFilled { get; set; } - /// - /// Status of the order - /// - public CommonOrderStatus Status { get; set; } - /// - /// Side of the order - /// - public CommonOrderSide Side { get; set; } - /// - /// Type of the order - /// - public CommonOrderType Type { get; set; } - /// - /// Order time - /// - public DateTime Timestamp { get; set; } - } -} diff --git a/CryptoExchange.Net/CommonObjects/OrderBook.cs b/CryptoExchange.Net/CommonObjects/OrderBook.cs deleted file mode 100644 index 2703675..0000000 --- a/CryptoExchange.Net/CommonObjects/OrderBook.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Order book data - /// - public class OrderBook: BaseCommonObject - { - /// - /// List of bids - /// - public IEnumerable Bids { get; set; } = Array.Empty(); - /// - /// List of asks - /// - public IEnumerable Asks { get; set; } = Array.Empty(); - } -} diff --git a/CryptoExchange.Net/CommonObjects/OrderBookEntry.cs b/CryptoExchange.Net/CommonObjects/OrderBookEntry.cs deleted file mode 100644 index 3384f41..0000000 --- a/CryptoExchange.Net/CommonObjects/OrderBookEntry.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Order book entry - /// - public class OrderBookEntry - { - /// - /// Quantity of the entry - /// - public decimal Quantity { get; set; } - /// - /// Price of the entry - /// - public decimal Price { get; set; } - } -} diff --git a/CryptoExchange.Net/CommonObjects/OrderId.cs b/CryptoExchange.Net/CommonObjects/OrderId.cs deleted file mode 100644 index 333e742..0000000 --- a/CryptoExchange.Net/CommonObjects/OrderId.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Id of an order - /// - public class OrderId: BaseCommonObject - { - /// - /// Id of an order - /// - public string Id { get; set; } = string.Empty; - } -} diff --git a/CryptoExchange.Net/CommonObjects/Position.cs b/CryptoExchange.Net/CommonObjects/Position.cs deleted file mode 100644 index 4319a9f..0000000 --- a/CryptoExchange.Net/CommonObjects/Position.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Position data - /// - public class Position: BaseCommonObject - { - /// - /// Id of the position - /// - public string? Id { get; set; } - /// - /// Symbol of the position - /// - public string Symbol { get; set; } = string.Empty; - /// - /// Leverage - /// - public decimal Leverage { get; set; } - /// - /// Position quantity - /// - public decimal Quantity { get; set; } - /// - /// Entry price - /// - public decimal? EntryPrice { get; set; } - /// - /// Liquidation price - /// - public decimal? LiquidationPrice { get; set; } - /// - /// Unrealized profit and loss - /// - public decimal? UnrealizedPnl { get; set; } - /// - /// Realized profit and loss - /// - public decimal? RealizedPnl { get; set; } - /// - /// Mark price - /// - public decimal? MarkPrice { get; set; } - /// - /// Auto adding margin - /// - public bool? AutoMargin { get; set; } - /// - /// Position margin - /// - public decimal? PositionMargin { get; set; } - /// - /// Position side - /// - public CommonPositionSide? Side { get; set; } - /// - /// Is isolated - /// - public bool? Isolated { get; set; } - /// - /// Maintenance margin - /// - public decimal? MaintananceMargin { get; set; } - } -} diff --git a/CryptoExchange.Net/CommonObjects/Symbol.cs b/CryptoExchange.Net/CommonObjects/Symbol.cs deleted file mode 100644 index 21fcea6..0000000 --- a/CryptoExchange.Net/CommonObjects/Symbol.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Symbol data - /// - public class Symbol: BaseCommonObject - { - /// - /// Name of the symbol - /// - public string Name { get; set; } = string.Empty; - /// - /// Minimal quantity of an order - /// - public decimal? MinTradeQuantity { get; set; } - /// - /// Step with which the quantity should increase - /// - public decimal? QuantityStep { get; set; } - /// - /// step with which the price should increase - /// - public decimal? PriceStep { get; set; } - /// - /// The max amount of decimals for quantity - /// - public int? QuantityDecimals { get; set; } - /// - /// The max amount of decimal for price - /// - public int? PriceDecimals { get; set; } - } -} diff --git a/CryptoExchange.Net/CommonObjects/Ticker.cs b/CryptoExchange.Net/CommonObjects/Ticker.cs deleted file mode 100644 index 4513bf5..0000000 --- a/CryptoExchange.Net/CommonObjects/Ticker.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Ticker data - /// - public class Ticker: BaseCommonObject - { - /// - /// Symbol - /// - public string Symbol { get; set; } = string.Empty; - /// - /// Price 24 hours ago - /// - public decimal? Price24H { get; set; } - /// - /// Last trade price - /// - public decimal? LastPrice { get; set; } - /// - /// 24 hour low price - /// - public decimal? LowPrice { get; set; } - /// - /// 24 hour high price - /// - public decimal? HighPrice { get; set; } - /// - /// 24 hour volume - /// - public decimal? Volume { get; set; } - } -} diff --git a/CryptoExchange.Net/CommonObjects/Trade.cs b/CryptoExchange.Net/CommonObjects/Trade.cs deleted file mode 100644 index ea15d9c..0000000 --- a/CryptoExchange.Net/CommonObjects/Trade.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; - -namespace CryptoExchange.Net.CommonObjects -{ - /// - /// Trade data - /// - public class Trade: BaseCommonObject - { - /// - /// Symbol of the trade - /// - public string Symbol { get; set; } = string.Empty; - /// - /// Price of the trade - /// - public decimal Price { get; set; } - /// - /// Quantity of the trade - /// - public decimal Quantity { get; set; } - /// - /// Timestamp of the trade - /// - public DateTime Timestamp { get; set; } - } - - /// - /// User trade info - /// - public class UserTrade: Trade - { - /// - /// Id of the trade - /// - public string Id { get; set; } = string.Empty; - /// - /// Order id of the trade - /// - public string? OrderId { get; set; } - /// - /// Fee of the trade - /// - public decimal? Fee { get; set; } - /// - /// The asset the fee is paid in - /// - public string? FeeAsset { get; set; } - } -} diff --git a/CryptoExchange.Net/Converters/JsonNet/ArrayConverter.cs b/CryptoExchange.Net/Converters/JsonNet/ArrayConverter.cs deleted file mode 100644 index 4a2180e..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/ArrayConverter.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Concurrent; -using System.Globalization; -using System.Linq; -using System.Reflection; -using CryptoExchange.Net.Attributes; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -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 - /// with [ArrayProperty(x)] where x is the index of the property in the array - /// - public class ArrayConverter : JsonConverter - { - private static readonly ConcurrentDictionary<(MemberInfo, Type), Attribute> _attributeByMemberInfoAndTypeCache = new ConcurrentDictionary<(MemberInfo, Type), Attribute>(); - private static readonly ConcurrentDictionary<(Type, Type), Attribute> _attributeByTypeAndTypeCache = new ConcurrentDictionary<(Type, Type), Attribute>(); - - /// - public override bool CanConvert(Type objectType) - { - return true; - } - - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return null; - - if (objectType == typeof(JToken)) - return JToken.Load(reader); - - var result = Activator.CreateInstance(objectType); - var arr = JArray.Load(reader); - return ParseObject(arr, result!, objectType); - } - - private static object ParseObject(JArray arr, object result, Type objectType) - { - foreach (var property in objectType.GetProperties()) - { - var attribute = GetCustomAttribute(property); - - if (attribute == null) - continue; - - if (attribute.Index >= arr.Count) - continue; - - if (property.PropertyType.BaseType == typeof(Array)) - { - var objType = property.PropertyType.GetElementType(); - var innerArray = (JArray)arr[attribute.Index]; - var count = 0; - if (innerArray.Count == 0) - { - var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!; - property.SetValue(result, arrayResult); - } - else if (innerArray[0].Type == JTokenType.Array) - { - var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!; - foreach (var obj in innerArray) - { - var innerObj = Activator.CreateInstance(objType!); - arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!); - count++; - } - property.SetValue(result, arrayResult); - } - else - { - var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!; - var innerObj = Activator.CreateInstance(objType!); - arrayResult[0] = ParseObject(innerArray, innerObj!, objType!); - property.SetValue(result, arrayResult); - } - continue; - } - - var converterAttribute = GetCustomAttribute(property) ?? GetCustomAttribute(property.PropertyType); - var conversionAttribute = GetCustomAttribute(property) ?? GetCustomAttribute(property.PropertyType); - - object? value; - if (converterAttribute != null) - { - value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}}); - } - else if (conversionAttribute != null) - { - value = arr[attribute.Index].ToObject(property.PropertyType); - } - else - { - value = arr[attribute.Index]; - } - - if (value != null && property.PropertyType.IsInstanceOfType(value)) - { - property.SetValue(result, value); - } - else - { - if (value is JToken token) - { - if (token.Type == JTokenType.Null) - value = null; - - if (token.Type == JTokenType.Float) - value = token.Value(); - } - - if (value is decimal) - { - property.SetValue(result, value); - } - else if ((property.PropertyType == typeof(decimal) - || property.PropertyType == typeof(decimal?)) - && (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0)) - { - var v = value.ToString(); - if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec)) - property.SetValue(result, dec); - } - else - { - property.SetValue(result, value == null ? null : Convert.ChangeType(value, property.PropertyType)); - } - } - } - return result; - } - - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - if (value == null) - return; - - writer.WriteStartArray(); - var props = value.GetType().GetProperties(); - var ordered = props.OrderBy(p => GetCustomAttribute(p)?.Index); - - var last = -1; - foreach (var prop in ordered) - { - var arrayProp = GetCustomAttribute(prop); - if (arrayProp == null) - continue; - - if (arrayProp.Index == last) - continue; - - while (arrayProp.Index != last + 1) - { - writer.WriteValue((string?)null); - last += 1; - } - - last = arrayProp.Index; - var converterAttribute = GetCustomAttribute(prop); - if (converterAttribute != null) - writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!)); - else if (!IsSimple(prop.PropertyType)) - serializer.Serialize(writer, prop.GetValue(value)); - else - writer.WriteValue(prop.GetValue(value)); - } - writer.WriteEndArray(); - } - - private static bool IsSimple(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - // nullable type, check if the nested type is simple. - return IsSimple(type.GetGenericArguments()[0]); - } - return type.IsPrimitive - || type.IsEnum - || type == typeof(string) - || type == typeof(decimal); - } - - private static T? GetCustomAttribute(MemberInfo memberInfo) where T : Attribute => - (T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!); - - private static T? GetCustomAttribute(Type type) where T : Attribute => - (T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!); - } -} diff --git a/CryptoExchange.Net/Converters/JsonNet/BaseConverter.cs b/CryptoExchange.Net/Converters/JsonNet/BaseConverter.cs deleted file mode 100644 index 9422d62..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/BaseConverter.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Newtonsoft.Json; - -namespace CryptoExchange.Net.Converters.JsonNet -{ - /// - /// Base class for enum converters - /// - /// Type of enum to convert - public abstract class BaseConverter: JsonConverter where T: struct - { - /// - /// The enum->string mapping - /// - protected abstract List> Mapping { get; } - private readonly bool _quotes; - - /// - /// ctor - /// - /// - protected BaseConverter(bool useQuotes) - { - _quotes = useQuotes; - } - - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - var stringValue = value == null? null: GetValue((T) value); - if (_quotes) - writer.WriteValue(stringValue); - else - writer.WriteRawValue(stringValue); - } - - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.Value == null) - return null; - - var stringValue = reader.Value.ToString(); - if (string.IsNullOrWhiteSpace(stringValue)) - return null; - - if (!GetValue(stringValue, out var result)) - { - Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {typeof(T)}, Value: {reader.Value}, Known values: {string.Join(", ", Mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo"); - return null; - } - - return result; - } - - /// - /// Convert a string value - /// - /// - /// - public T ReadString(string data) - { - return Mapping.FirstOrDefault(v => v.Value == data).Key; - } - - /// - public override bool CanConvert(Type objectType) - { - // Check if it is type, or nullable of type - return objectType == typeof(T) || Nullable.GetUnderlyingType(objectType) == typeof(T); - } - - private bool GetValue(string value, out T result) - { - // Check for exact match first, then if not found fallback to a case insensitive match - var mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); - if(mapping.Equals(default(KeyValuePair))) - mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); - - if (!mapping.Equals(default(KeyValuePair))) - { - result = mapping.Key; - return true; - } - - result = default; - return false; - } - - private string GetValue(T value) - { - return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value; - } - } -} diff --git a/CryptoExchange.Net/Converters/JsonNet/BigDecimalConverter.cs b/CryptoExchange.Net/Converters/JsonNet/BigDecimalConverter.cs deleted file mode 100644 index 80777c1..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/BigDecimalConverter.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; - -namespace CryptoExchange.Net.Converters.JsonNet -{ - /// - /// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue) - /// - public class BigDecimalConverter : JsonConverter - { - /// - public override bool CanConvert(Type objectType) - { - if (Nullable.GetUnderlyingType(objectType) != null) - return Nullable.GetUnderlyingType(objectType) == typeof(decimal); - return objectType == typeof(decimal); - } - - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.TokenType == JsonToken.Null) - return null; - - if (reader.TokenType == JsonToken.Float || reader.TokenType == JsonToken.Integer) - { - try - { - return decimal.Parse(reader.Value!.ToString()!, NumberStyles.Float, CultureInfo.InvariantCulture); - } - catch (OverflowException) - { - // Value doesn't fit decimal; set it to max value - return decimal.MaxValue; - } - } - - if (reader.TokenType == JsonToken.String) - { - try - { - var value = reader.Value!.ToString()!; - return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture); - } - catch (OverflowException) - { - // Value doesn't fit decimal; set it to max value - return decimal.MaxValue; - } - } - - return null; - } - - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - writer.WriteValue(value); - } - } -} \ No newline at end of file diff --git a/CryptoExchange.Net/Converters/JsonNet/BoolConverter.cs b/CryptoExchange.Net/Converters/JsonNet/BoolConverter.cs deleted file mode 100644 index 4f519dd..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/BoolConverter.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace CryptoExchange.Net.Converters.JsonNet -{ - /// - /// Boolean converter with support for "0"/"1" (strings) - /// - public class BoolConverter : JsonConverter - { - /// - /// Determines whether this instance can convert the specified object type. - /// - /// Type of the object. - /// - /// true if this instance can convert the specified object type; otherwise, false. - /// - public override bool CanConvert(Type objectType) - { - if (Nullable.GetUnderlyingType(objectType) != null) - return Nullable.GetUnderlyingType(objectType) == typeof(bool); - return objectType == typeof(bool); - } - - /// - /// Reads the JSON representation of the object. - /// - /// The to read from. - /// Type of the object. - /// The existing value of object being read. - /// The calling serializer. - /// - /// The object value. - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - var value = reader.Value?.ToString()!.ToLower().Trim(); - if (value == null || value == "") - { - if (Nullable.GetUnderlyingType(objectType) != null) - return null; - - return false; - } - - 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; - } - - // If we reach here, we're pretty much going to throw an error so let's let Json.NET throw it's pretty-fied error message. - return new JsonSerializer().Deserialize(reader, objectType); - } - - /// - /// Specifies that this converter will not participate in writing results. - /// - public override bool CanWrite { get { return false; } } - - /// - /// Writes the JSON representation of the object. - /// - /// The to write to.The value.The calling serializer. - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - } - } -} \ No newline at end of file diff --git a/CryptoExchange.Net/Converters/JsonNet/DateTimeConverter.cs b/CryptoExchange.Net/Converters/JsonNet/DateTimeConverter.cs deleted file mode 100644 index e2f686a..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/DateTimeConverter.cs +++ /dev/null @@ -1,240 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; - -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. - /// - public class DateTimeConverter: JsonConverter - { - private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; - private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000; - private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000; - - /// - public override bool CanConvert(Type objectType) - { - return objectType == typeof(DateTime) || objectType == typeof(DateTime?); - } - - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - if (reader.Value == null) - { - if (objectType == typeof(DateTime)) - return default(DateTime); - - return null; - } - - if(reader.TokenType is JsonToken.Integer) - { - var longValue = (long)reader.Value; - if (longValue == 0 || longValue == -1) - return objectType == typeof(DateTime) ? default(DateTime): null; - - return ParseFromLong(longValue); - } - else if (reader.TokenType is JsonToken.Float) - { - var doubleValue = (double)reader.Value; - if (doubleValue == 0 || doubleValue == -1) - return objectType == typeof(DateTime) ? default(DateTime) : null; - - if (doubleValue < 19999999999) - return ConvertFromSeconds(doubleValue); - - return ConvertFromMilliseconds(doubleValue); - } - else if(reader.TokenType is JsonToken.String) - { - var stringValue = (string)reader.Value; - if (string.IsNullOrWhiteSpace(stringValue) - || stringValue == "-1" - || (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)) - { - return objectType == typeof(DateTime) ? default(DateTime) : null; - } - - return ParseFromString(stringValue); - } - else if(reader.TokenType == JsonToken.Date) - { - return (DateTime)reader.Value; - } - else - { - Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value); - return default; - } - } - - /// - /// Parse a long value to datetime - /// - /// - /// - public static DateTime ParseFromLong(long longValue) - { - if (longValue < 19999999999) - return ConvertFromSeconds(longValue); - if (longValue < 19999999999999) - return ConvertFromMilliseconds(longValue); - if (longValue < 19999999999999999) - return ConvertFromMicroseconds(longValue); - - return ConvertFromNanoseconds(longValue); - } - - /// - /// Parse a string value to datetime - /// - /// - /// - public static DateTime ParseFromString(string stringValue) - { - if (stringValue.Length == 12 && stringValue.StartsWith("202")) - { - // Parse 202303261200 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) - || !int.TryParse(stringValue.Substring(8, 2), out var hour) - || !int.TryParse(stringValue.Substring(10, 2), out var minute)) - { - Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue); - return default; - } - return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc); - } - - 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); - } - - /// - /// 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(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond)); - /// - /// Convert a nanoseconds since epoch (01-01-1970) value to DateTime - /// - /// - /// - public static DateTime ConvertFromNanoseconds(long 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); - - - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - var datetimeValue = (DateTime?)value; - if (datetimeValue == null) - writer.WriteValue((DateTime?)null); - if(datetimeValue == default(DateTime)) - writer.WriteValue((DateTime?)null); - else - writer.WriteValue((long)Math.Round(((DateTime)value! - new DateTime(1970, 1, 1)).TotalMilliseconds)); - } - } -} diff --git a/CryptoExchange.Net/Converters/JsonNet/DecimalStringWriterConverter.cs b/CryptoExchange.Net/Converters/JsonNet/DecimalStringWriterConverter.cs deleted file mode 100644 index 694b3a9..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/DecimalStringWriterConverter.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Newtonsoft.Json; -using System; -using System.Globalization; - -namespace CryptoExchange.Net.Converters.JsonNet -{ - /// - /// Converter for serializing decimal values as string - /// - public class DecimalStringWriterConverter : JsonConverter - { - /// - public override bool CanRead => false; - - /// - public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?); - - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - throw new NotImplementedException(); - } - - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? null); - } -} diff --git a/CryptoExchange.Net/Converters/JsonNet/EnumConverter.cs b/CryptoExchange.Net/Converters/JsonNet/EnumConverter.cs deleted file mode 100644 index edd97a0..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/EnumConverter.cs +++ /dev/null @@ -1,176 +0,0 @@ -using CryptoExchange.Net.Attributes; -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Linq; - -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 - /// - public class EnumConverter : JsonConverter - { - private bool _warnOnMissingEntry = true; - private bool _writeAsInt; - - /// - /// - public EnumConverter() { } - - /// - /// - /// - /// - public EnumConverter(bool writeAsInt, bool warnOnMissingEntry) - { - _writeAsInt = writeAsInt; - _warnOnMissingEntry = warnOnMissingEntry; - } - - private static readonly ConcurrentDictionary>> _mapping = new(); - - /// - public override bool CanConvert(Type objectType) - { - return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true; - } - - /// - public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) - { - var enumType = Nullable.GetUnderlyingType(objectType) ?? objectType; - if (!_mapping.TryGetValue(enumType, out var mapping)) - mapping = AddMapping(enumType); - - var stringValue = reader.Value?.ToString(); - if (stringValue == null || stringValue == "") - { - // Received null value - var emptyResult = GetDefaultValue(objectType, 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 emptyResult; - } - - if (!GetValue(enumType, mapping, stringValue!, out var result)) - { - var defaultValue = GetDefaultValue(objectType, 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: {reader.Value}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {reader.Value} should added please open an issue on the Github repo"); - } - - return defaultValue; - } - - return result; - } - - private static object? GetDefaultValue(Type objectType, Type enumType) - { - if (Nullable.GetUnderlyingType(objectType) != null) - return null; - - return Activator.CreateInstance(enumType); // return default value - } - - 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 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()); - } - - /// - public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) - { - if (value == null) - { - writer.WriteNull(); - } - else - { - if (!_writeAsInt) - { - var stringValue = GetString(value.GetType(), value); - writer.WriteValue(stringValue); - } - else - { - writer.WriteValue((int)value); - } - } - } - } -} diff --git a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs deleted file mode 100644 index 2938ee4..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageAccessor.cs +++ /dev/null @@ -1,338 +0,0 @@ -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.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.Converters.JsonNet -{ - /// - /// Json.Net message accessor - /// - public abstract class JsonNetMessageAccessor : IMessageAccessor - { - /// - /// The json token loaded - /// - protected JToken? _token; - private static readonly 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 = node.Index!.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[node.Property!]; - } - 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 async Task Read(Stream stream, bool bufferStream) - { - if (bufferStream && stream is not MemoryStream) - { - // We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream - _stream = new MemoryStream(); - stream.CopyTo(_stream); - _stream.Position = 0; - } - else if (bufferStream) - { - // We need to buffer the stream, and the current stream is seekable, store as is - _stream = stream; - } - else - { - // We don't need to buffer the stream, so don't bother keeping the reference - } - - var readStream = _stream ?? stream; - var length = readStream.CanSeek ? readStream.Length : 4096; - using var reader = new StreamReader(readStream, Encoding.UTF8, false, (int)Math.Max(2, length), true); - using var jsonTextReader = new JsonTextReader(reader); - - try - { - _token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false); - IsJson = true; - return new CallResult(null); - } - catch (Exception ex) - { - // Not a json message - IsJson = false; - return new CallResult(new ServerError("JsonError: " + ex.Message)); - } - - } - /// - 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 CallResult Read(ReadOnlyMemory data) - { - _bytes = data; - - // Try getting the underlying byte[] instead of the ToArray to prevent creating a copy - using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment) - ? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count) - : new MemoryStream(data.ToArray()); - using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true); - using var jsonTextReader = new JsonTextReader(reader); - - try - { - _token = JToken.Load(jsonTextReader); - IsJson = true; - return new CallResult(null); - } - catch (Exception ex) - { - // Not a json message - IsJson = false; - return new CallResult(new ServerError("JsonError: " + ex.Message)); - } - } - - /// - public override string GetOriginalString() => - // Netstandard 2.0 doesn't support GetString from a ReadonlySpan, so use ToArray there instead -#if NETSTANDARD2_0 - Encoding.UTF8.GetString(_bytes.ToArray()); -#else - Encoding.UTF8.GetString(_bytes.Span); -#endif - - /// - public override bool OriginalDataAvailable => true; - - /// - public override void Clear() - { - _bytes = null; - _token = null; - } - } -} diff --git a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs b/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs deleted file mode 100644 index 79ec7f5..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/JsonNetMessageSerializer.cs +++ /dev/null @@ -1,12 +0,0 @@ -using CryptoExchange.Net.Interfaces; -using Newtonsoft.Json; - -namespace CryptoExchange.Net.Converters.JsonNet -{ - /// - public class JsonNetMessageSerializer : IMessageSerializer - { - /// - public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None); - } -} diff --git a/CryptoExchange.Net/Converters/JsonNet/SerializerOptions.cs b/CryptoExchange.Net/Converters/JsonNet/SerializerOptions.cs deleted file mode 100644 index 96756e8..0000000 --- a/CryptoExchange.Net/Converters/JsonNet/SerializerOptions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Newtonsoft.Json; -using System.Globalization; - -namespace CryptoExchange.Net.Converters.JsonNet -{ - /// - /// Serializer options - /// - public static class SerializerOptions - { - /// - /// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter - /// - public static JsonSerializerSettings WithConverters => new JsonSerializerSettings - { - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - Culture = CultureInfo.InvariantCulture, - Converters = - { - new EnumConverter(), - new DateTimeConverter(), - new BoolConverter() - } - }; - - /// - /// Default json serializer settings - /// - public static JsonSerializerSettings Default => new JsonSerializerSettings - { - DateTimeZoneHandling = DateTimeZoneHandling.Utc, - Culture = CultureInfo.InvariantCulture - }; - } -} diff --git a/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs b/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs new file mode 100644 index 0000000..55cb48f --- /dev/null +++ b/CryptoExchange.Net/Converters/JsonSerializerContextCache.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters +{ + /// + /// Caching for JsonSerializerContext instances + /// + public static class JsonSerializerContextCache + { + private static ConcurrentDictionary _cache = new ConcurrentDictionary(); + + /// + /// Get the instance of the provided type T. It will be created if it doesn't exist yet. + /// + /// Implementation type of the JsonSerializerContext + public static JsonSerializerContext GetOrCreate() where T: JsonSerializerContext, new() + { + var contextType = typeof(T); + if (_cache.TryGetValue(contextType, out var context)) + return context; + + var instance = new T(); + _cache[contextType] = instance; + return instance; + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs index 5fe381b..0be5896 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ArrayConverter.cs @@ -7,6 +7,9 @@ using System.Text.Json.Serialization; using System.Text.Json; using CryptoExchange.Net.Attributes; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Diagnostics; namespace CryptoExchange.Net.Converters.SystemTextJson { @@ -14,215 +17,220 @@ 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 +#if NET5_0_OR_GREATER + public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter where T : new() +#else + public class ArrayConverter : JsonConverter where T : new() +#endif { - /// - public override bool CanConvert(Type typeToConvert) => true; + private static readonly Lazy> _typePropertyInfo = new Lazy>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly); + + private static readonly ConcurrentDictionary _converterOptionsCache = new ConcurrentDictionary(); /// - public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) { - Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert); - return (JsonConverter)Activator.CreateInstance(converterType)!; + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartArray(); + + var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index); + var last = -1; + foreach (var prop in ordered) + { + if (prop.ArrayProperty.Index == last) + continue; + + while (prop.ArrayProperty.Index != last + 1) + { + writer.WriteNullValue(); + last += 1; + } + + last = prop.ArrayProperty.Index; + + var objValue = prop.PropertyInfo.GetValue(value); + if (objValue == null) + { + writer.WriteNullValue(); + continue; + } + + JsonSerializerOptions? typeOptions = null; + if (prop.JsonConverter != null) + { + typeOptions = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + TypeInfoResolver = options.TypeInfoResolver, + }; + typeOptions.Converters.Add(prop.JsonConverter); + } + + if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType)) + { + if (prop.TargetType == typeof(string)) + writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); + else if (prop.TargetType == typeof(bool)) + writer.WriteBooleanValue((bool)objValue); + else + writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!); + } + else + { + JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); + } + } + + writer.WriteEndArray(); + } + + /// + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return default; + + var result = Activator.CreateInstance(typeof(T))!; + return (T)ParseObject(ref reader, result, typeof(T), options); + } + + +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + private static object ParseObject(ref Utf8JsonReader reader, object result, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type objectType, JsonSerializerOptions options) +#else + private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options) +#endif + { + if (reader.TokenType != JsonTokenType.StartArray) + throw new Exception("Not an array"); + + int index = 0; + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndArray) + break; + + var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index); + if (!indexAttributes.Any()) + { + index++; + continue; + } + + foreach (var attribute in indexAttributes) + { + var targetType = attribute.TargetType; + object? value = null; + if (attribute.JsonConverter != null) + { + if (!_converterOptionsCache.TryGetValue(attribute.JsonConverter, out var newOptions)) + { + newOptions = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + Converters = { attribute.JsonConverter }, + TypeInfoResolver = options.TypeInfoResolver, + }; + _converterOptionsCache.TryAdd(attribute.JsonConverter, newOptions); + } + + var doc = JsonDocument.ParseValue(ref reader); + value = doc.Deserialize(attribute.PropertyInfo.PropertyType, newOptions); + } + else if (attribute.DefaultDeserialization) + { + value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType)); + } + else + { + value = reader.TokenType switch + { + JsonTokenType.Null => null, + JsonTokenType.False => false, + JsonTokenType.True => true, + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetDecimal(), + JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options), + _ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"), + }; + } + + if (targetType.IsAssignableFrom(value?.GetType())) + attribute.PropertyInfo.SetValue(result, value); + else + attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); + } + + index++; + } + + return result; + } + + private static bool IsSimple(Type type) + { + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + // nullable type, check if the nested type is simple. + return IsSimple(type.GetGenericArguments()[0]); + } + return type.IsPrimitive + || type.IsEnum + || type == typeof(string) + || type == typeof(decimal); + } + +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + private static List CacheTypeAttributes() +#else + private static List CacheTypeAttributes() +#endif + { + var attributes = new List(); + var properties = typeof(T).GetProperties(); + foreach (var property in properties) + { + var att = property.GetCustomAttribute(); + if (att == null) + continue; + + var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + var converterType = property.GetCustomAttribute()?.ConverterType ?? targetType.GetCustomAttribute()?.ConverterType; + attributes.Add(new ArrayPropertyInfo + { + ArrayProperty = att, + PropertyInfo = property, + DefaultDeserialization = property.GetCustomAttribute() != null, + JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!, + TargetType = targetType + }); + } + + return attributes; } private class ArrayPropertyInfo { public PropertyInfo PropertyInfo { get; set; } = null!; public ArrayPropertyAttribute ArrayProperty { get; set; } = null!; - public Type? JsonConverterType { get; set; } + public JsonConverter? JsonConverter { get; set; } public bool DefaultDeserialization { get; set; } public Type TargetType { get; set; } = null!; } - - private class ArrayConverterInner : JsonConverter - { - private static readonly ConcurrentDictionary> _typeAttributesCache = new ConcurrentDictionary>(); - private static readonly ConcurrentDictionary _converterOptionsCache = new ConcurrentDictionary(); - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - if (value == null) - { - writer.WriteNullValue(); - return; - } - - writer.WriteStartArray(); - - var valueType = value.GetType(); - if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes)) - typeAttributes = CacheTypeAttributes(valueType); - - var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index); - var last = -1; - foreach (var prop in ordered) - { - if (prop.ArrayProperty.Index == last) - continue; - - while (prop.ArrayProperty.Index != last + 1) - { - writer.WriteNullValue(); - last += 1; - } - - last = prop.ArrayProperty.Index; - - var objValue = prop.PropertyInfo.GetValue(value); - if (objValue == null) - { - writer.WriteNullValue(); - continue; - } - - JsonSerializerOptions? typeOptions = null; - if (prop.JsonConverterType != null) - { - var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!; - typeOptions = new JsonSerializerOptions(); - typeOptions.Converters.Clear(); - typeOptions.Converters.Add(converter); - } - - if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType)) - { - if (prop.TargetType == typeof(string)) - writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)); - else if(prop.TargetType.IsEnum) - writer.WriteStringValue(EnumConverter.GetString(objValue)); - else if (prop.TargetType == typeof(bool)) - writer.WriteBooleanValue((bool)objValue); - else - writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!); - } - else - { - JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); - } - } - - writer.WriteEndArray(); - } - - /// - 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, options); - } - - private static bool IsSimple(Type type) - { - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - // nullable type, check if the nested type is simple. - return IsSimple(type.GetGenericArguments()[0]); - } - return type.IsPrimitive - || type.IsEnum - || type == typeof(string) - || type == typeof(decimal); - } - - 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 ?? property.PropertyType.GetCustomAttribute()?.ConverterType, - TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType - }); - } - - _typeAttributesCache.TryAdd(type, attributes); - return attributes; - } - - private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartArray) - throw new Exception("Not an array"); - - if (!_typeAttributesCache.TryGetValue(objectType, out var attributes)) - attributes = CacheTypeAttributes(objectType); - - int index = 0; - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndArray) - break; - - var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index); - if (!indexAttributes.Any()) - { - index++; - continue; - } - - foreach (var attribute in indexAttributes) - { - var targetType = attribute.TargetType; - object? value = null; - if (attribute.JsonConverterType != null) - { - if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions)) - { - var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!; - newOptions = new JsonSerializerOptions - { - NumberHandling = SerializerOptions.WithConverters.NumberHandling, - PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive, - Converters = { converter }, - }; - _converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions); - } - - value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions); - } - else if (attribute.DefaultDeserialization) - { - // Use default deserialization - value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters); - } - else - { - value = reader.TokenType switch - { - JsonTokenType.Null => null, - JsonTokenType.False => false, - JsonTokenType.True => true, - JsonTokenType.String => reader.GetString(), - JsonTokenType.Number => reader.GetDecimal(), - JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options), - _ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"), - }; - } - - if (targetType.IsAssignableFrom(value?.GetType())) - attribute.PropertyInfo.SetValue(result, value); - else - attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture)); - } - - index++; - } - - return result; - } - } } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs index 2f45135..9ada30b 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/BoolConverter.cs @@ -20,8 +20,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert); - return (JsonConverter)Activator.CreateInstance(converterType)!; + return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInner(); } private class BoolConverterInner : JsonConverter diff --git a/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs index f74af7a..c327079 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/CommaSplitEnumConverter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; using System.Text.Json; @@ -8,18 +9,27 @@ using System.Text.Json.Serialization; namespace CryptoExchange.Net.Converters.SystemTextJson { /// - /// Converter for comma seperated enum values + /// Converter for comma separated enum values /// - public class CommaSplitEnumConverter : JsonConverter> where T : Enum +#if NET5_0_OR_GREATER + public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter where T : struct, Enum +#else + public class CommaSplitEnumConverter : JsonConverter where T : struct, Enum +#endif + { /// - public override IEnumerable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString(x)).ToArray() ?? new T[0])!; + var str = reader.GetString(); + if (string.IsNullOrEmpty(str)) + return []; + + return str!.Split(',').Select(x => (T)EnumConverter.ParseString(x)!).ToArray() ?? []; } /// - public override void Write(Utf8JsonWriter writer, IEnumerable value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options) { writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x)))); } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs index 9f3f7af..e491add 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/DateTimeConverter.cs @@ -26,8 +26,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert); - return (JsonConverter)Activator.CreateInstance(converterType)!; + return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new DateTimeConverterInner(); } private class DateTimeConverterInner : JsonConverter diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EmptyArrayObjectConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EmptyArrayObjectConverter.cs deleted file mode 100644 index b3486f3..0000000 --- a/CryptoExchange.Net/Converters/SystemTextJson/EmptyArrayObjectConverter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace CryptoExchange.Net.Converters.SystemTextJson -{ - /// - /// Converter mapping to an object but also handles when an empty array is send - /// - /// - public class EmptyArrayObjectConverter : JsonConverter - { - private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters; - - /// - public override T? Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options) - { - switch (reader.TokenType) - { - case JsonTokenType.StartArray: - _ = JsonSerializer.Deserialize(ref reader, options); - return default; - case JsonTokenType.StartObject: - return JsonSerializer.Deserialize(ref reader, _defaultConverter); - }; - - return default; - } - - /// - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - => JsonSerializer.Serialize(writer, (object?)value, options); - } -} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs index 46c3ee0..ff5dfb8 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumConverter.cs @@ -11,127 +11,78 @@ using System.Text.Json.Serialization; namespace CryptoExchange.Net.Converters.SystemTextJson { + /// + /// Static EnumConverter methods + /// + public static class EnumConverter + { + /// + /// Get the enum value from a string + /// + /// String value + /// +#if NET5_0_OR_GREATER + public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum +#else + public static T? ParseString(string value) where T : struct, Enum +#endif + => EnumConverter.ParseString(value); + + /// + /// 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 + /// + /// + /// +#if NET5_0_OR_GREATER + public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum +#else + public static string GetString(T enumValue) where T : struct, Enum +#endif + => EnumConverter.GetString(enumValue); + + /// + /// 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")] +#if NET5_0_OR_GREATER + public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum +#else + public static string? GetString(T? enumValue) where T : struct, Enum +#endif + => EnumConverter.GetString(enumValue); + } + /// /// 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 +#if NET5_0_OR_GREATER + public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> +#else + public class EnumConverter +#endif + : JsonConverter, INullableConverterFactory where T : struct, Enum { - private bool _warnOnMissingEntry = true; - private bool _writeAsInt; - private static readonly ConcurrentDictionary>> _mapping = new(); + private static List>? _mapping = null; + private NullableEnumConverter? _nullableEnumConverter = null; - /// - /// - public EnumConverter() { } + private static ConcurrentBag _unknownValuesWarned = new ConcurrentBag(); - /// - /// - /// - /// - public EnumConverter(bool writeAsInt, bool warnOnMissingEntry) + internal class NullableEnumConverter : JsonConverter { - _writeAsInt = writeAsInt; - _warnOnMissingEntry = warnOnMissingEntry; - } + private readonly EnumConverter _enumConverter; - /// - 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) + public NullableEnumConverter(EnumConverter enumConverter) { - 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)); - } + _enumConverter = enumConverter; } - _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(), - JsonTokenType.True => reader.GetBoolean().ToString(), - JsonTokenType.False => reader.GetBoolean().ToString(), - JsonTokenType.Null => null, - _ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType) - }; - - 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; + return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn); } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { if (value == null) { @@ -139,106 +90,183 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } else { - if (!_writeAsInt) + _enumConverter.Write(writer, value.Value, options); + } + } + } + + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn); + if (t == null) + { + if (warn) + { + if (isEmptyString) { - var stringValue = GetString(value.GetType(), value); - writer.WriteStringValue(stringValue); + // 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: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo"); } else { - writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int))); + 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: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo"); } } - } - private static object? GetDefaultValue(Type objectType, Type enumType) + return new T(); // return default value + } + else { - if (Nullable.GetUnderlyingType(objectType) != null) - return null; + return t.Value; + } + } - return Activator.CreateInstance(enumType); // return default value + private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn) + { + isEmptyString = false; + warn = false; + var enumType = typeof(T); + if (_mapping == null) + _mapping = AddMapping(); + + var stringValue = reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.GetInt32().ToString(), + JsonTokenType.True => reader.GetBoolean().ToString(), + JsonTokenType.False => reader.GetBoolean().ToString(), + JsonTokenType.Null => null, + _ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType) + }; + + if (string.IsNullOrEmpty(stringValue)) + return null; + + if (!GetValue(enumType, stringValue!, out var result)) + { + if (string.IsNullOrWhiteSpace(stringValue)) + { + isEmptyString = true; + } + else + { + // We received an enum value but weren't able to parse it. + if (!_unknownValuesWarned.Contains(stringValue)) + { + warn = true; + _unknownValuesWarned.Add(stringValue!); + 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 null; } - private static bool GetValue(Type objectType, List> enumMapping, string value, out object? result) + return result; + } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + var stringValue = GetString(value); + writer.WriteStringValue(stringValue); + } + + private static bool GetValue(Type objectType, string value, out T? result) + { + if (_mapping != null) { // 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)); + var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); + if (mapping.Equals(default(KeyValuePair))) + mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); - if (!mapping.Equals(default(KeyValuePair))) + if (!mapping.Equals(default(KeyValuePair))) { result = mapping.Key; return true; } + } - if (objectType.IsDefined(typeof(FlagsAttribute))) - { - var intValue = int.Parse(value); - result = Enum.ToObject(objectType, intValue); - return true; - } + if (objectType.IsDefined(typeof(FlagsAttribute))) + { + var intValue = int.Parse(value); + result = (T)Enum.ToObject(objectType, intValue); + return true; + } - try + if (_unknownValuesWarned.Contains(value)) + { + // Check if it is an known unknown value + // Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing + result = default; + return false; + } + + try + { + // If no explicit mapping is found try to parse string + result = (T)Enum.Parse(objectType, value, true); + return true; + } + catch (Exception) + { + result = default; + return false; + } + } + + private static List> AddMapping() + { + var mapping = new List>(); + var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + var enumMembers = enumType.GetFields(); + foreach (var member in enumMembers) + { + var maps = member.GetCustomAttributes(typeof(MapAttribute), false); + foreach (MapAttribute attribute in maps) { - // If no explicit mapping is found try to parse string - result = Enum.Parse(objectType, value, true); - return true; - } - catch (Exception) - { - result = default; - return false; + foreach (var value in attribute.Values) + mapping.Add(new KeyValuePair((T)Enum.Parse(enumType, member.Name), value)); } } + + _mapping = mapping; + return mapping; } /// /// 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); - - /// - /// 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(Type objectType, object? enumValue) + public static string? GetString(T? enumValue) { - objectType = Nullable.GetUnderlyingType(objectType) ?? objectType; + if (_mapping == null) + _mapping = AddMapping(); - if (!_mapping.TryGetValue(objectType, out var mapping)) - mapping = AddMapping(objectType); - - return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); + return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString()); } /// /// Get the enum value from a string /// - /// Enum type /// String value /// - public static T? ParseString(string value) where T : Enum + public static T? ParseString(string value) { - var type = typeof(T); - if (!_mapping.TryGetValue(type, out var enumMapping)) - enumMapping = AddMapping(type); + var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); + if (_mapping == null) + _mapping = AddMapping(); - 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)); + var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture)); + if (mapping.Equals(default(KeyValuePair))) + mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase)); - if (!mapping.Equals(default(KeyValuePair))) - { - return (T)mapping.Key; - } + if (!mapping.Equals(default(KeyValuePair))) + return mapping.Key; try { @@ -250,5 +278,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson return default; } } + + /// + public JsonConverter CreateNullableConverter() + { + _nullableEnumConverter ??= new NullableEnumConverter(this); + return _nullableEnumConverter; + } } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.cs new file mode 100644 index 0000000..9a0c9a7 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/EnumIntWriterConverter.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 enum values as int + /// + public class EnumIntWriterConverter : JsonConverter where T: struct, Enum + { + /// + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + throw new NotImplementedException(); + } + + /// + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + => writer.WriteNumberValue((int)(object)value); + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs b/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs new file mode 100644 index 0000000..ea01b5a --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/INullableConverterFactory.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + internal interface INullableConverterFactory + { + JsonConverter CreateNullableConverter(); + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/JsonConverterCtorAttribute.cs b/CryptoExchange.Net/Converters/SystemTextJson/JsonConverterCtorAttribute.cs deleted file mode 100644 index c58c0e3..0000000 --- a/CryptoExchange.Net/Converters/SystemTextJson/JsonConverterCtorAttribute.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Text.Json.Serialization; - -namespace CryptoExchange.Net.Converters.SystemTextJson -{ - /// - /// Attribute for allowing specifying a JsonConverter with constructor parameters - /// - [AttributeUsage(AttributeTargets.Property)] - public class JsonConverterCtorAttribute : JsonConverterAttribute - { - private readonly object[] _parameters; - private readonly Type _type; - - /// - /// ctor - /// - public JsonConverterCtorAttribute(Type type, params object[] parameters) - { - _type = type; - _parameters = parameters; - } - - /// - public override JsonConverter CreateConverter(Type typeToConvert) - { - return (JsonConverter)Activator.CreateInstance(_type, _parameters)!; - } - } - -} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs b/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs new file mode 100644 index 0000000..4cd7e68 --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/NullableEnumConverterFactory.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + internal class NullableEnumConverterFactory : JsonConverterFactory + { + private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver; + private static readonly JsonSerializerOptions _options = new JsonSerializerOptions(); + + public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver) + { + _jsonTypeInfoResolver = jsonTypeInfoResolver; + } + + public override bool CanConvert(Type typeToConvert) + { + var b = Nullable.GetUnderlyingType(typeToConvert); + if (b == null) + return false; + + var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options); + if (typeInfo == null) + return false; + + return typeInfo.Converter is INullableConverterFactory; + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}"); + var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}"); + if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory) + throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}"); + + return nullConverterFactory.CreateNullableConverter(); + } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs index 542defa..53a77ee 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ObjectStringConverter.cs @@ -1,16 +1,20 @@ using System; using System.Text.Json.Serialization; using System.Text.Json; +using System.Diagnostics.CodeAnalysis; namespace CryptoExchange.Net.Converters.SystemTextJson { /// - /// + /// Converter for values which contain a nested json value /// - /// public class ObjectStringConverter : JsonConverter { /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) @@ -20,10 +24,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson if (string.IsNullOrEmpty(value)) return default; - return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T)); + return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options); } /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { if (value is null) diff --git a/CryptoExchange.Net/Converters/SystemTextJson/ReplaceConverter.cs b/CryptoExchange.Net/Converters/SystemTextJson/ReplaceConverter.cs index 5976387..6006c80 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/ReplaceConverter.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/ReplaceConverter.cs @@ -8,7 +8,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// /// Replace a value on a string property /// - public class ReplaceConverter : JsonConverter + public abstract class ReplaceConverter : JsonConverter { private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets; diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs b/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs new file mode 100644 index 0000000..3c958ec --- /dev/null +++ b/CryptoExchange.Net/Converters/SystemTextJson/SerializationModel.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Converters.SystemTextJson +{ + /// + /// Attribute to mark a model as json serializable. Used for AOT compilation. + /// + [AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)] + public class SerializationModelAttribute : Attribute + { + /// + /// ctor + /// + public SerializationModelAttribute() { } + /// + /// ctor + /// + /// + public SerializationModelAttribute(Type type) { } + } +} diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs b/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs index 71c867f..39ef682 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SerializerOptions.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Collections.Concurrent; +using System.Text.Json; using System.Text.Json.Serialization; namespace CryptoExchange.Net.Converters.SystemTextJson @@ -8,22 +9,39 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public static class SerializerOptions { + private static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); + /// - /// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter + /// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types /// - public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions + public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters) { - NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, - PropertyNameCaseInsensitive = false, - Converters = + if (!_cache.TryGetValue(typeResolver, out var options)) + { + options = new JsonSerializerOptions + { + NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals, + PropertyNameCaseInsensitive = false, + Converters = { new DateTimeConverter(), - new EnumConverter(), new BoolConverter(), new DecimalConverter(), new IntConverter(), - new LongConverter() - } - }; + new LongConverter(), + new NullableEnumConverterFactory(typeResolver) + }, + TypeInfoResolver = typeResolver, + }; + + foreach (var converter in additionalConverters) + options.Converters.Add(converter); + + options.TypeInfoResolver = typeResolver; + _cache.TryAdd(typeResolver, options); + } + + return options; + } } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs index 45c8a86..dc2f3cb 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageAccessor.cs @@ -3,6 +3,7 @@ using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text; using System.Text.Json; @@ -20,7 +21,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// protected JsonDocument? _document; - private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters; private readonly JsonSerializerOptions? _customSerializerOptions; /// @@ -32,13 +32,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public object? Underlying => throw new NotImplementedException(); - /// - /// ctor - /// - public SystemTextJsonMessageAccessor() - { - } - /// /// ctor /// @@ -48,6 +41,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif public CallResult Deserialize(Type type, MessagePath? path = null) { if (!IsJson) @@ -58,22 +55,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson try { - var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions); + var result = _document.Deserialize(type, _customSerializerOptions); 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]")); + return new CallResult(new DeserializeError(info, ex)); } catch (Exception ex) { var info = $"Deserialize unknown Exception: {ex.Message}"; - return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + return new CallResult(new DeserializeError(info, ex)); } } /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif public CallResult Deserialize(MessagePath? path = null) { if (_document == null) @@ -81,18 +82,18 @@ namespace CryptoExchange.Net.Converters.SystemTextJson try { - var result = _document.Deserialize(_customSerializerOptions ?? _serializerOptions); + var result = _document.Deserialize(_customSerializerOptions); 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]")); + return new CallResult(new DeserializeError(info, ex)); } catch (Exception ex) { var info = $"Unknown exception: {ex.Message}"; - return new CallResult(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); + return new CallResult(new DeserializeError(info, ex)); } } @@ -132,6 +133,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson } /// +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif public T? GetValue(MessagePath path) { if (!IsJson) @@ -145,7 +150,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { try { - return value.Value.Deserialize(_customSerializerOptions ?? _serializerOptions); + return value.Value.Deserialize(_customSerializerOptions); } catch { } @@ -158,11 +163,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson return (T)(object)value.Value.GetInt64().ToString(); } - return value.Value.Deserialize(); + return value.Value.Deserialize(_customSerializerOptions); } /// - public List? GetValues(MessagePath path) +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")] +#endif + public T?[]? GetValues(MessagePath path) { if (!IsJson) throw new InvalidOperationException("Can't access json data on non-json message"); @@ -174,7 +183,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson if (value.Value.ValueKind != JsonValueKind.Array) return default; - return value.Value.Deserialize>()!; + return value.Value.Deserialize(_customSerializerOptions)!; } private JsonElement? GetPathNode(MessagePath path) @@ -240,13 +249,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson /// public override bool OriginalDataAvailable => _stream?.CanSeek == true; - /// - /// ctor - /// - public SystemTextJsonStreamMessageAccessor(): base() - { - } - /// /// ctor /// @@ -278,13 +280,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { _document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false); IsJson = true; - return new CallResult(null); + return CallResult.SuccessResult; } catch (Exception ex) { // Not a json message IsJson = false; - return new CallResult(new ServerError("JsonError: " + ex.Message)); + return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex)); } } @@ -317,13 +319,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson { private ReadOnlyMemory _bytes; - /// - /// ctor - /// - public SystemTextJsonByteMessageAccessor() : base() - { - } - /// /// ctor /// @@ -348,13 +343,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson _document = JsonDocument.Parse(data); IsJson = true; - return new CallResult(null); + return CallResult.SuccessResult; } catch (Exception ex) { // Not a json message IsJson = false; - return new CallResult(new ServerError("JsonError: " + ex.Message)); + return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex)); } } diff --git a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs index 8c4cdf7..cd654c2 100644 --- a/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs +++ b/CryptoExchange.Net/Converters/SystemTextJson/SystemTextJsonMessageSerializer.cs @@ -1,12 +1,29 @@ using CryptoExchange.Net.Interfaces; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace CryptoExchange.Net.Converters.SystemTextJson { /// public class SystemTextJsonMessageSerializer : IMessageSerializer { + private readonly JsonSerializerOptions _options; + + /// + /// ctor + /// + public SystemTextJsonMessageSerializer(JsonSerializerOptions options) + { + _options = options; + } + /// - public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters); +#if NET5_0_OR_GREATER + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] + [UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")] +#endif + public string Serialize(T message) => JsonSerializer.Serialize(message, _options); } } diff --git a/CryptoExchange.Net/CryptoExchange.Net.csproj b/CryptoExchange.Net/CryptoExchange.Net.csproj index 3f55424..8a49149 100644 --- a/CryptoExchange.Net/CryptoExchange.Net.csproj +++ b/CryptoExchange.Net/CryptoExchange.Net.csproj @@ -1,14 +1,14 @@ - netstandard2.0;netstandard2.1;net9.0 + netstandard2.0;netstandard2.1;net8.0;net9.0 CryptoExchange.Net JKorf CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations. - 8.8.0 - 8.8.0 - 8.8.0 + 9.0.0-beta7 + 9.0.0 + 9.0.0 false OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange git @@ -27,6 +27,9 @@ + + true + true true @@ -52,10 +55,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + diff --git a/CryptoExchange.Net/ExchangeHelpers.cs b/CryptoExchange.Net/ExchangeHelpers.cs index 95411ff..aeb0737 100644 --- a/CryptoExchange.Net/ExchangeHelpers.cs +++ b/CryptoExchange.Net/ExchangeHelpers.cs @@ -15,6 +15,7 @@ namespace CryptoExchange.Net public static class ExchangeHelpers { private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789"; + private const string _allowedRandomHexChars = "0123456789ABCDEF"; private static readonly Dictionary _monthSymbols = new Dictionary() { @@ -111,6 +112,34 @@ namespace CryptoExchange.Net return RoundToSignificantDigits(value, precision.Value, roundingType); } + /// + /// Apply the provided rules to the value + /// + /// Value to be adjusted + /// Max decimal places + /// The value step for increase/decrease value + /// + public static decimal ApplyRules( + decimal value, + int? decimals = null, + decimal? valueStep = null) + { + if (valueStep.HasValue) + { + var offset = value % valueStep.Value; + if (offset != 0) + { + if (offset < valueStep.Value / 2) + value -= offset; + else value += (valueStep.Value - offset); + } + } + if (decimals.HasValue) + value = Math.Round(value, decimals.Value); + + return value; + } + /// /// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12 /// @@ -192,6 +221,44 @@ namespace CryptoExchange.Net return new string(randomChars); } + /// + /// Generate a random string of specified length + /// + /// Length of the random string + /// + public static string RandomHexString(int length) + { +#if NET9_0_OR_GREATER + return "0x" + RandomNumberGenerator.GetHexString(length * 2); +#else + var randomChars = new char[length * 2]; + var random = new Random(); + for (int i = 0; i < length * 2; i++) + randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)]; + return "0x" + new string(randomChars); +#endif + } + + /// + /// Generate a long value + /// + /// Max character length + /// + public static long RandomLong(int maxLength) + { +#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER + var value = RandomNumberGenerator.GetInt32(0, int.MaxValue); +#else + var random = new Random(); + var value = random.Next(0, int.MaxValue); +#endif + var val = value.ToString(); + if (val.Length > maxLength) + return int.Parse(val.Substring(0, maxLength)); + else + return value; + } + /// /// Generate a random string of specified length /// @@ -225,10 +292,10 @@ namespace CryptoExchange.Net /// The request parameters /// Cancellation token /// - public static async IAsyncEnumerable>> ExecutePages(Func>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) + public static async IAsyncEnumerable> ExecutePages(Func>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default) { var result = new List(); - ExchangeWebResult> batch; + ExchangeWebResult batch; INextPageToken? nextPageToken = null; while (true) { diff --git a/CryptoExchange.Net/ExchangeSymbolCache.cs b/CryptoExchange.Net/ExchangeSymbolCache.cs new file mode 100644 index 0000000..74c4dc1 --- /dev/null +++ b/CryptoExchange.Net/ExchangeSymbolCache.cs @@ -0,0 +1,70 @@ +using CryptoExchange.Net.SharedApis; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CryptoExchange.Net +{ + /// + /// Cache for symbol parsing + /// + public static class ExchangeSymbolCache + { + private static ConcurrentDictionary _symbolInfos = new ConcurrentDictionary(); + + /// + /// Update the cached symbol data for an exchange + /// + /// Id for the provided data + /// Symbol data + public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData) + { + if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) + { + exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name })); + _symbolInfos.TryAdd(topicId, exchangeInfo); + } + + if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60)) + return; + + _symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => new SharedSymbol(x.TradingMode, x.BaseAsset.ToUpperInvariant(), x.QuoteAsset.ToUpperInvariant(), (x as SharedFuturesSymbol)?.DeliveryTime) { SymbolName = x.Name })); + } + + /// + /// Parse a symbol name to a SharedSymbol + /// + /// Id for the provided data + /// Symbol name + public static SharedSymbol? ParseSymbol(string topicId, string? symbolName) + { + if (symbolName == null) + return null; + + if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo)) + return null; + + if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo)) + return null; + + return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName) + { + DeliverTime = symbolInfo.DeliverTime + }; + } + + class ExchangeInfo + { + public DateTime UpdateTime { get; set; } + public Dictionary Symbols { get; set; } + + public ExchangeInfo(DateTime updateTime, Dictionary symbols) + { + UpdateTime = updateTime; + Symbols = symbols; + } + } + } +} diff --git a/CryptoExchange.Net/ExtensionMethods.cs b/CryptoExchange.Net/ExtensionMethods.cs index 4f72a1d..f1a7b44 100644 --- a/CryptoExchange.Net/ExtensionMethods.cs +++ b/CryptoExchange.Net/ExtensionMethods.cs @@ -10,6 +10,9 @@ using CryptoExchange.Net.Objects; using System.Globalization; using Microsoft.Extensions.DependencyInjection; using CryptoExchange.Net.SharedApis; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json; +using System.Text.Json.Serialization; namespace CryptoExchange.Net { @@ -440,6 +443,8 @@ namespace CryptoExchange.Net services.AddTransient(x => (IWithdrawRestClient)client(x)!); if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T))) services.AddTransient(x => (IFeeRestClient)client(x)!); + if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T))) + services.AddTransient(x => (IBookTickerRestClient)client(x)!); if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T))) services.AddTransient(x => (ISpotOrderRestClient)client(x)!); @@ -447,6 +452,10 @@ namespace CryptoExchange.Net services.AddTransient(x => (ISpotSymbolRestClient)client(x)!); if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T))) services.AddTransient(x => (ISpotTickerRestClient)client(x)!); + if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T))) + services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!); + if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T))) + services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!); if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T))) services.AddTransient(x => (IFundingRateRestClient)client(x)!); @@ -468,6 +477,12 @@ namespace CryptoExchange.Net services.AddTransient(x => (IPositionHistoryRestClient)client(x)!); if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T))) services.AddTransient(x => (IPositionModeRestClient)client(x)!); + if (typeof(IFuturesTpSlRestClient).IsAssignableFrom(typeof(T))) + services.AddTransient(x => (IFuturesTpSlRestClient)client(x)!); + if (typeof(IFuturesTriggerOrderRestClient).IsAssignableFrom(typeof(T))) + services.AddTransient(x => (IFuturesTriggerOrderRestClient)client(x)!); + if (typeof(IFuturesOrderClientIdRestClient).IsAssignableFrom(typeof(T))) + services.AddTransient(x => (IFuturesOrderClientIdRestClient)client(x)!); return services; } diff --git a/CryptoExchange.Net/Interfaces/CommonClients/IBaseRestClient.cs b/CryptoExchange.Net/Interfaces/CommonClients/IBaseRestClient.cs deleted file mode 100644 index ebd2bdb..0000000 --- a/CryptoExchange.Net/Interfaces/CommonClients/IBaseRestClient.cs +++ /dev/null @@ -1,94 +0,0 @@ -using CryptoExchange.Net.CommonObjects; -using CryptoExchange.Net.Objects; -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.Interfaces.CommonClients -{ - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - public interface IBaseRestClient - { - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - string ExchangeName { get; } - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - event Action OnOrderPlaced; - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - event Action OnOrderCanceled; - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - string GetSymbolName(string baseAsset, string quoteAsset); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetSymbolsAsync(CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task> GetTickerAsync(string symbol, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetTickersAsync(CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task> GetOrderBookAsync(string symbol, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetRecentTradesAsync(string symbol, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default); - } -} diff --git a/CryptoExchange.Net/Interfaces/CommonClients/IFuturesClient.cs b/CryptoExchange.Net/Interfaces/CommonClients/IFuturesClient.cs deleted file mode 100644 index 8a6feb3..0000000 --- a/CryptoExchange.Net/Interfaces/CommonClients/IFuturesClient.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using CryptoExchange.Net.CommonObjects; -using CryptoExchange.Net.Objects; - -namespace CryptoExchange.Net.Interfaces.CommonClients -{ - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - public interface IFuturesClient : IBaseRestClient - { - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default); - - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task>> GetPositionsAsync(CancellationToken ct = default); - } -} diff --git a/CryptoExchange.Net/Interfaces/CommonClients/ISpotClient.cs b/CryptoExchange.Net/Interfaces/CommonClients/ISpotClient.cs deleted file mode 100644 index 8ca7ec4..0000000 --- a/CryptoExchange.Net/Interfaces/CommonClients/ISpotClient.cs +++ /dev/null @@ -1,18 +0,0 @@ -using CryptoExchange.Net.CommonObjects; -using CryptoExchange.Net.Objects; -using System.Threading; -using System.Threading.Tasks; - -namespace CryptoExchange.Net.Interfaces.CommonClients -{ - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - public interface ISpotClient: IBaseRestClient - { - /// - /// DEPRECATED; use instead for common/shared functionality. See for more info. - /// - Task> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default); - } -} diff --git a/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs b/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs index f072ebc..c3966eb 100644 --- a/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs +++ b/CryptoExchange.Net/Interfaces/ICryptoRestClient.cs @@ -1,6 +1,4 @@ -using CryptoExchange.Net.Interfaces.CommonClients; -using System; -using System.Collections.Generic; +using System; namespace CryptoExchange.Net.Interfaces { @@ -9,19 +7,6 @@ namespace CryptoExchange.Net.Interfaces /// public interface ICryptoRestClient { - /// - /// Get a list of all registered common ISpotClient types - /// - /// - IEnumerable GetSpotClients(); - - /// - /// Get an ISpotClient implementation by exchange name - /// - /// - /// - ISpotClient? SpotClient(string exchangeName); - /// /// Try get /// @@ -29,4 +14,4 @@ namespace CryptoExchange.Net.Interfaces /// T TryGet(Func createFunc); } -} +} \ No newline at end of file diff --git a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs index f65d066..12c6460 100644 --- a/CryptoExchange.Net/Interfaces/IMessageAccessor.cs +++ b/CryptoExchange.Net/Interfaces/IMessageAccessor.cs @@ -52,7 +52,7 @@ namespace CryptoExchange.Net.Interfaces /// /// /// - List? GetValues(MessagePath path); + T?[]? GetValues(MessagePath path); /// /// Deserialize the message into this type /// diff --git a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs index 61ffecb..544cf93 100644 --- a/CryptoExchange.Net/Interfaces/IMessageSerializer.cs +++ b/CryptoExchange.Net/Interfaces/IMessageSerializer.cs @@ -10,6 +10,6 @@ /// /// /// - string Serialize(object message); + string Serialize(T message); } } diff --git a/CryptoExchange.Net/Interfaces/IRequest.cs b/CryptoExchange.Net/Interfaces/IRequest.cs index ad6c591..72ebe59 100644 --- a/CryptoExchange.Net/Interfaces/IRequest.cs +++ b/CryptoExchange.Net/Interfaces/IRequest.cs @@ -54,7 +54,7 @@ namespace CryptoExchange.Net.Interfaces /// Get all headers /// /// - Dictionary> GetHeaders(); + KeyValuePair[] GetHeaders(); /// /// Get the response diff --git a/CryptoExchange.Net/Interfaces/IResponse.cs b/CryptoExchange.Net/Interfaces/IResponse.cs index 2d3c487..55f9921 100644 --- a/CryptoExchange.Net/Interfaces/IResponse.cs +++ b/CryptoExchange.Net/Interfaces/IResponse.cs @@ -28,7 +28,7 @@ namespace CryptoExchange.Net.Interfaces /// /// The response headers /// - IEnumerable>> ResponseHeaders { get; } + KeyValuePair[] ResponseHeaders { get; } /// /// Get the response stream diff --git a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs index baf9df3..21c2bb7 100644 --- a/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs +++ b/CryptoExchange.Net/Interfaces/ISymbolOrderBook.cs @@ -42,7 +42,7 @@ namespace CryptoExchange.Net.Interfaces /// /// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets /// - event Action<(IEnumerable Bids, IEnumerable Asks)> OnOrderBookUpdate; + event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)> OnOrderBookUpdate; /// /// Event when the BestBid or BestAsk changes ie a Pricing Tick /// @@ -64,17 +64,17 @@ namespace CryptoExchange.Net.Interfaces /// /// Get a snapshot of the book at this moment /// - (IEnumerable bids, IEnumerable asks) Book { get; } + (ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book { get; } /// /// The list of asks /// - IEnumerable Asks { get; } + ISymbolOrderBookEntry[] Asks { get; } /// /// The list of bids /// - IEnumerable Bids { get; } + ISymbolOrderBookEntry[] Bids { get; } /// /// The best bid currently in the order book diff --git a/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs index a8c5b50..f5078d3 100644 --- a/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/RestApiClientLoggingExtensions.cs @@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Logging.Extensions #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public static class RestApiClientLoggingExtensions { - private static readonly Action _restApiErrorReceived; + private static readonly Action _restApiErrorReceived; private static readonly Action _restApiResponseReceived; private static readonly Action _restApiFailedToSyncTime; private static readonly Action _restApiNoApiCredentials; @@ -25,10 +25,10 @@ namespace CryptoExchange.Net.Logging.Extensions static RestApiClientLoggingExtensions() { - _restApiErrorReceived = LoggerMessage.Define( + _restApiErrorReceived = LoggerMessage.Define( LogLevel.Warning, new EventId(4000, "RestApiErrorReceived"), - "[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}"); + "[Req {RequestId}] {ResponseStatusCode} - Error received in {ResponseTime}ms: {ErrorMessage}, Data: {OriginalData}"); _restApiResponseReceived = LoggerMessage.Define( LogLevel.Debug, @@ -92,9 +92,9 @@ namespace CryptoExchange.Net.Logging.Extensions } - public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error) + public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception) { - _restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, null); + _restApiErrorReceived(logger, requestId, (int?)responseStatusCode, responseTime, error, originalData, exception); } public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData) diff --git a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs index d4c3725..dfd9f7c 100644 --- a/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs +++ b/CryptoExchange.Net/Logging/Extensions/SocketConnectionLoggingExtension.cs @@ -246,9 +246,9 @@ namespace CryptoExchange.Net.Logging.Extensions { _receivedMessageNotRecognized(logger, socketId, id, null); } - public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage) + public static void FailedToDeserializeMessage(this ILogger logger, int socketId, string? errorMessage, Exception? ex) { - _failedToDeserializeMessage(logger, socketId, errorMessage, null); + _failedToDeserializeMessage(logger, socketId, errorMessage, ex); } public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e) { diff --git a/CryptoExchange.Net/Logging/Extensions/SymbolOrderBookLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/SymbolOrderBookLoggingExtensions.cs index 464859c..413f12b 100644 --- a/CryptoExchange.Net/Logging/Extensions/SymbolOrderBookLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/SymbolOrderBookLoggingExtensions.cs @@ -53,7 +53,7 @@ namespace CryptoExchange.Net.Logging.Extensions "{Api} order book {Symbol} connection lost"); _orderBookDisconnected = LoggerMessage.Define( - LogLevel.Warning, + LogLevel.Debug, new EventId(5004, "OrderBookDisconnected"), "{Api} order book {Symbol} disconnected"); diff --git a/CryptoExchange.Net/Logging/Extensions/TrackerLoggingExtensions.cs b/CryptoExchange.Net/Logging/Extensions/TrackerLoggingExtensions.cs index 1090a8b..8015f5b 100644 --- a/CryptoExchange.Net/Logging/Extensions/TrackerLoggingExtensions.cs +++ b/CryptoExchange.Net/Logging/Extensions/TrackerLoggingExtensions.cs @@ -173,9 +173,9 @@ namespace CryptoExchange.Net.Logging.Extensions _klineTrackerStarting(logger, symbol, null); } - public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error) + public static void KlineTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? exception) { - _klineTrackerStartFailed(logger, symbol, error, null); + _klineTrackerStartFailed(logger, symbol, error, exception); } public static void KlineTrackerStarted(this ILogger logger, string symbol) @@ -233,9 +233,9 @@ namespace CryptoExchange.Net.Logging.Extensions _tradeTrackerStarting(logger, symbol, null); } - public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error) + public static void TradeTrackerStartFailed(this ILogger logger, string symbol, string error, Exception? ex) { - _tradeTrackerStartFailed(logger, symbol, error, null); + _tradeTrackerStartFailed(logger, symbol, error, ex); } public static void TradeTrackerStarted(this ILogger logger, string symbol) diff --git a/CryptoExchange.Net/Objects/AssetAlias.cs b/CryptoExchange.Net/Objects/AssetAlias.cs new file mode 100644 index 0000000..5829d09 --- /dev/null +++ b/CryptoExchange.Net/Objects/AssetAlias.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.Objects +{ + /// + /// An alias used by the exchange for an asset commonly known by another name + /// + public class AssetAlias + { + /// + /// The name of the asset on the exchange + /// + public string ExchangeAssetName { get; set; } + /// + /// The name of the asset as it's commonly known + /// + public string CommonAssetName { get; set; } + + /// + /// ctor + /// + public AssetAlias(string exchangeName, string commonName) + { + ExchangeAssetName = exchangeName; + CommonAssetName = commonName; + } + } +} diff --git a/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs b/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs new file mode 100644 index 0000000..ab5622f --- /dev/null +++ b/CryptoExchange.Net/Objects/AssetAliasConfiguration.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace CryptoExchange.Net.Objects +{ + /// + /// Exchange configuration for asset aliases + /// + public class AssetAliasConfiguration + { + /// + /// Defined aliases + /// + public AssetAlias[] Aliases { get; set; } = []; + + /// + /// Auto convert asset names when using the Shared interfaces. Defaults to true + /// + public bool AutoConvertEnabled { get; set; } = true; + + /// + /// Map the common name to an exchange name for an asset. If there is no alias the input name is returned + /// + public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.SingleOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName; + + /// + /// Map the exchange name to a common name for an asset. If there is no alias the input name is returned + /// + public string ExchangeToCommonName(string exchangeName) => !AutoConvertEnabled ? exchangeName : Aliases.SingleOrDefault(x => x.ExchangeAssetName == exchangeName)?.CommonAssetName ?? exchangeName; + + } +} diff --git a/CryptoExchange.Net/Objects/CallResult.cs b/CryptoExchange.Net/Objects/CallResult.cs index 6dbded1..e847bfc 100644 --- a/CryptoExchange.Net/Objects/CallResult.cs +++ b/CryptoExchange.Net/Objects/CallResult.cs @@ -13,6 +13,11 @@ namespace CryptoExchange.Net.Objects /// public class CallResult { + /// + /// Static success result + /// + public static CallResult SuccessResult { get; } = new CallResult(null); + /// /// An error if the call didn't succeed, will always be filled if Success = false /// @@ -149,7 +154,7 @@ namespace CryptoExchange.Net.Objects /// public CallResult AsDataless() { - return new CallResult(null); + return SuccessResult; } /// @@ -161,6 +166,18 @@ namespace CryptoExchange.Net.Objects return new CallResult(error); } + /// + /// Copy the CallResult to a new data type + /// + /// The new type + /// The data + /// The error returned + /// + public CallResult AsErrorWithData(Error error, K data) + { + return new CallResult(data, OriginalData, error); + } + /// /// Copy the WebCallResult to a new data type /// @@ -192,7 +209,7 @@ namespace CryptoExchange.Net.Objects /// /// The headers sent with the request /// - public IEnumerable>>? RequestHeaders { get; set; } + public KeyValuePair[]? RequestHeaders { get; set; } /// /// The request id @@ -209,6 +226,11 @@ namespace CryptoExchange.Net.Objects /// public string? RequestBody { get; set; } + /// + /// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options + /// + public string? OriginalData { get; internal set; } + /// /// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this. /// @@ -217,7 +239,7 @@ namespace CryptoExchange.Net.Objects /// /// The response headers /// - public IEnumerable>>? ResponseHeaders { get; set; } + public KeyValuePair[]? ResponseHeaders { get; set; } /// /// The time between sending the request and receiving the response @@ -227,30 +249,23 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - /// - /// - /// - /// - /// - /// - /// - /// public WebCallResult( HttpStatusCode? code, - IEnumerable>>? responseHeaders, + KeyValuePair[]? responseHeaders, TimeSpan? responseTime, + string? originalData, int? requestId, string? requestUrl, string? requestBody, HttpMethod? requestMethod, - IEnumerable>>? requestHeaders, + KeyValuePair[]? requestHeaders, Error? error) : base(error) { ResponseStatusCode = code; ResponseHeaders = responseHeaders; ResponseTime = responseTime; RequestId = requestId; + OriginalData = originalData; RequestUrl = requestUrl; RequestBody = requestBody; @@ -271,7 +286,7 @@ namespace CryptoExchange.Net.Objects /// public WebCallResult AsError(Error error) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); } /// @@ -343,7 +358,7 @@ namespace CryptoExchange.Net.Objects /// /// The headers sent with the request /// - public IEnumerable>>? RequestHeaders { get; set; } + public KeyValuePair[]? RequestHeaders { get; set; } /// /// The request id @@ -373,7 +388,7 @@ namespace CryptoExchange.Net.Objects /// /// The response headers /// - public IEnumerable>>? ResponseHeaders { get; set; } + public KeyValuePair[]? ResponseHeaders { get; set; } /// /// The time between sending the request and receiving the response @@ -403,7 +418,7 @@ namespace CryptoExchange.Net.Objects /// public WebCallResult( HttpStatusCode? code, - IEnumerable>>? responseHeaders, + KeyValuePair[]? responseHeaders, TimeSpan? responseTime, long? responseLength, string? originalData, @@ -411,7 +426,7 @@ namespace CryptoExchange.Net.Objects string? requestUrl, string? requestBody, HttpMethod? requestMethod, - IEnumerable>>? requestHeaders, + KeyValuePair[]? requestHeaders, ResultDataSource dataSource, [AllowNull] T data, Error? error) : base(data, originalData, error) @@ -435,7 +450,7 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult AsDataless() { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, Error); } /// /// Copy as a dataless result @@ -443,7 +458,7 @@ namespace CryptoExchange.Net.Objects /// public new WebCallResult AsDatalessError(Error error) { - return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error); } /// @@ -474,6 +489,18 @@ namespace CryptoExchange.Net.Objects return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error); } + /// + /// Copy the WebCallResult to a new data type + /// + /// The new type + /// The data + /// The error returned + /// + public new WebCallResult AsErrorWithData(Error error, K data) + { + return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error); + } + /// /// Copy the WebCallResult to an ExchangeWebResult of a new data type /// @@ -553,7 +580,7 @@ namespace CryptoExchange.Net.Objects if (ResponseLength != null) sb.Append($", {ResponseLength} bytes"); if (ResponseTime != null) - sb.Append($" received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms"); + sb.Append($", received in {Math.Round(ResponseTime?.TotalMilliseconds ?? 0)}ms"); return sb.ToString(); } diff --git a/CryptoExchange.Net/Objects/Error.cs b/CryptoExchange.Net/Objects/Error.cs index 74232dd..61a012b 100644 --- a/CryptoExchange.Net/Objects/Error.cs +++ b/CryptoExchange.Net/Objects/Error.cs @@ -18,21 +18,18 @@ namespace CryptoExchange.Net.Objects public string Message { get; set; } /// - /// The data which caused the error + /// Underlying exception /// - public object? Data { get; set; } + public Exception? Exception { get; set; } /// /// ctor /// - /// - /// - /// - protected Error(int? code, string message, object? data) + protected Error (int? code, string message, Exception? exception) { Code = code; Message = message; - Data = data; + Exception = exception; } /// @@ -41,7 +38,7 @@ namespace CryptoExchange.Net.Objects /// public override string ToString() { - return Code != null ? $"[{GetType().Name}] {Code}: {Message} {Data}" : $"[{GetType().Name}] {Message} {Data}"; + return Code != null ? $"[{GetType().Name}] {Code}: {Message}" : $"[{GetType().Name}] {Message}"; } } @@ -58,10 +55,12 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - /// - /// - protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { } + public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { } + + /// + /// ctor + /// + protected CantConnectError(int? code, string message, Exception? exception) : base(code, message, exception) { } } /// @@ -77,10 +76,7 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - /// - /// - protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { } + protected NoApiCredentialsError(int? code, string message, Exception? exception) : base(code, message, exception) { } } /// @@ -91,25 +87,12 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - /// - public ServerError(string message, object? data = null) : base(null, message, data) { } + public ServerError(string message) : base(null, message, null) { } /// /// ctor /// - /// - /// - /// - public ServerError(int code, string message, object? data = null) : base(code, message, data) { } - - /// - /// ctor - /// - /// - /// - /// - protected ServerError(int? code, string message, object? data) : base(code, message, data) { } + public ServerError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } } /// @@ -120,25 +103,12 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - /// - public WebError(string message, object? data = null) : base(null, message, data) { } + public WebError(string message, Exception? exception = null) : base(null, message, exception) { } /// /// ctor /// - /// - /// - /// - public WebError(int code, string message, object? data = null) : base(code, message, data) { } - - /// - /// ctor - /// - /// - /// - /// - protected WebError(int? code, string message, object? data): base(code, message, data) { } + public WebError(int code, string message, Exception? exception = null) : base(code, message, exception) { } } /// @@ -149,17 +119,12 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// The error message - /// The data which caused the error - public DeserializeError(string message, object? data) : base(null, message, data) { } + public DeserializeError(string message, Exception? exception = null) : base(null, message, exception) { } /// /// ctor /// - /// - /// - /// - protected DeserializeError(int? code, string message, object? data): base(code, message, data) { } + protected DeserializeError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } } /// @@ -170,17 +135,12 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// Error message - /// Error data - public UnknownError(string message, object? data = null) : base(null, message, data) { } + public UnknownError(string message, Exception? exception = null) : base(null, message, exception) { } /// /// ctor /// - /// - /// - /// - protected UnknownError(int? code, string message, object? data): base(code, message, data) { } + protected UnknownError(int? code, string message, Exception? exception = null): base(code, message, exception) { } } /// @@ -191,16 +151,12 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { } /// /// ctor /// - /// - /// - /// - protected ArgumentError(int? code, string message, object? data): base(code, message, data) { } + protected ArgumentError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } } /// @@ -216,10 +172,7 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - /// - /// - protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { } + protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { } } /// @@ -236,10 +189,7 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - /// - /// - protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { } + protected ClientRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } } /// @@ -250,16 +200,12 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { } + public ServerRateLimitError(string? message = null, Exception? exception = null) : base(null, "Server rate limit exceeded" + (message?.Length > 0 ? " : " + message : null), exception) { } /// /// ctor /// - /// - /// - /// - protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { } + protected ServerRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } } /// @@ -270,15 +216,12 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - public CancellationRequestedError() : base(null, "Cancellation requested", null) { } + public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { } /// /// ctor /// - /// - /// - /// - public CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { } + public CancellationRequestedError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } } /// @@ -289,15 +232,11 @@ namespace CryptoExchange.Net.Objects /// /// ctor /// - /// - public InvalidOperationError(string message) : base(null, message, null) { } + public InvalidOperationError(string message, Exception? exception = null) : base(null, message, exception) { } /// /// ctor /// - /// - /// - /// - protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { } + protected InvalidOperationError(int? code, string message, Exception? exception = null) : base(code, message, exception) { } } } diff --git a/CryptoExchange.Net/Objects/Options/LibraryOptions.cs b/CryptoExchange.Net/Objects/Options/LibraryOptions.cs index 1c9400f..7cb441e 100644 --- a/CryptoExchange.Net/Objects/Options/LibraryOptions.cs +++ b/CryptoExchange.Net/Objects/Options/LibraryOptions.cs @@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Objects.Options /// public T Set(T targetOptions) where T: LibraryOptions { - targetOptions.ApiCredentials = ApiCredentials; + targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy(); targetOptions.Environment = Environment; targetOptions.SocketClientLifeTime = SocketClientLifeTime; targetOptions.Rest = Rest.Set(targetOptions.Rest); diff --git a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs index e643f39..40e8ea0 100644 --- a/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs +++ b/CryptoExchange.Net/Objects/Options/SocketExchangeOptions.cs @@ -94,7 +94,7 @@ namespace CryptoExchange.Net.Objects.Options { /// /// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for - /// the exhange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` + /// the exchange's environment list or create a custom environment using either `[Exchange]Environment.CreateCustom()` or `[Exchange]Environment.[Environment]`, for example `KucoinEnvironment.TestNet` or `BinanceEnvironment.Live` /// #pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable. public TEnvironment Environment { get; set; } diff --git a/CryptoExchange.Net/Objects/ParameterCollection.cs b/CryptoExchange.Net/Objects/ParameterCollection.cs index 599067e..1512995 100644 --- a/CryptoExchange.Net/Objects/ParameterCollection.cs +++ b/CryptoExchange.Net/Objects/ParameterCollection.cs @@ -2,6 +2,7 @@ using CryptoExchange.Net.Converters.SystemTextJson; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; @@ -173,11 +174,14 @@ namespace CryptoExchange.Net.Objects /// /// Add an enum value as the string value as mapped using the /// - /// - /// +#if NET5_0_OR_GREATER + public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) +#else public void AddEnum(string key, T value) +#endif + where T : struct, Enum { - Add(key, EnumConverter.GetString(value)!); + Add(key, EnumConverter.GetString(value)!); } /// @@ -185,9 +189,14 @@ namespace CryptoExchange.Net.Objects /// /// /// +#if NET5_0_OR_GREATER + public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value) +#else public void AddEnumAsInt(string key, T value) +#endif + where T : struct, Enum { - var stringVal = EnumConverter.GetString(value)!; + var stringVal = EnumConverter.GetString(value)!; Add(key, int.Parse(stringVal)!); } @@ -196,22 +205,30 @@ namespace CryptoExchange.Net.Objects /// /// /// +#if NET5_0_OR_GREATER + public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) +#else public void AddOptionalEnum(string key, T? value) +#endif + where T : struct, Enum { if (value != null) - Add(key, EnumConverter.GetString(value)); + Add(key, EnumConverter.GetString(value)); } /// /// Add an enum value as the string value as mapped using the . Not added if value is null /// - /// - /// +#if NET5_0_OR_GREATER + public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value) +#else public void AddOptionalEnumAsInt(string key, T? value) +#endif + where T : struct, Enum { if (value != null) { - var stringVal = EnumConverter.GetString(value); + var stringVal = EnumConverter.GetString(value); Add(key, int.Parse(stringVal)); } } diff --git a/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs b/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs index 31c3224..5ce4c1a 100644 --- a/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs +++ b/CryptoExchange.Net/Objects/Sockets/WebSocketParameters.cs @@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Objects.Sockets /// public TimeSpan? KeepAliveInterval { get; set; } + /// + /// Timeout for keep alive response messages + /// + public TimeSpan? KeepAliveTimeout { get; set; } + /// /// The rate limiter for the socket connection /// diff --git a/CryptoExchange.Net/Objects/TraceLogger.cs b/CryptoExchange.Net/Objects/TraceLogger.cs index 5906059..ee96141 100644 --- a/CryptoExchange.Net/Objects/TraceLogger.cs +++ b/CryptoExchange.Net/Objects/TraceLogger.cs @@ -56,7 +56,7 @@ namespace CryptoExchange.Net.Objects if (!IsEnabled(logLevel)) return; - var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}"; + var logMessage = $"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | {logLevel} | {(_categoryName == null ? "" : $"{_categoryName} | ")}{formatter(state, exception)}{(exception == null ? string.Empty : (", " + exception.ToLogString()))}"; Trace.WriteLine(logMessage); } } diff --git a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs index e457994..5a71974 100644 --- a/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs +++ b/CryptoExchange.Net/OrderBook/ProcessBufferEntry.cs @@ -22,11 +22,11 @@ namespace CryptoExchange.Net.OrderBook /// /// List of changed/new asks /// - public IEnumerable Asks { get; set; } = Array.Empty(); + public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); /// /// List of changed/new bids /// - public IEnumerable Bids { get; set; } = Array.Empty(); + public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); } } diff --git a/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs b/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs index 2ddc46e..f23110c 100644 --- a/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs +++ b/CryptoExchange.Net/OrderBook/ProcessQueueItem.cs @@ -8,16 +8,16 @@ namespace CryptoExchange.Net.OrderBook { public long StartUpdateId { get; set; } public long EndUpdateId { get; set; } - public IEnumerable Bids { get; set; } = Array.Empty(); - public IEnumerable Asks { get; set; } = Array.Empty(); + public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); + public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); } internal class InitialOrderBookItem { public long StartUpdateId { get; set; } public long EndUpdateId { get; set; } - public IEnumerable Bids { get; set; } = Array.Empty(); - public IEnumerable Asks { get; set; } = Array.Empty(); + public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty(); + public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty(); } internal class ChecksumItem diff --git a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs index 124de93..d6ee7b4 100644 --- a/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs +++ b/CryptoExchange.Net/OrderBook/SymbolOrderBook.cs @@ -123,7 +123,7 @@ namespace CryptoExchange.Net.OrderBook public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged; /// - public event Action<(IEnumerable Bids, IEnumerable Asks)>? OnOrderBookUpdate; + public event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)>? OnOrderBookUpdate; /// public DateTime UpdateTime { get; private set; } @@ -135,27 +135,27 @@ namespace CryptoExchange.Net.OrderBook public int BidCount { get; private set; } /// - public IEnumerable Asks + public ISymbolOrderBookEntry[] Asks { get { lock (_bookLock) - return _asks.Select(a => a.Value).ToList(); + return _asks.Select(a => a.Value).ToArray(); } } /// - public IEnumerable Bids + public ISymbolOrderBookEntry[] Bids { get { lock (_bookLock) - return _bids.Select(a => a.Value).ToList(); + return _bids.Select(a => a.Value).ToArray(); } } /// - public (IEnumerable bids, IEnumerable asks) Book + public (ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book { get { @@ -412,7 +412,7 @@ namespace CryptoExchange.Net.OrderBook /// The last update sequence number until which the snapshot is in sync /// List of asks /// List of bids - protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable bidList, IEnumerable askList) + protected void SetInitialOrderBook(long orderBookSequenceNumber, ISymbolOrderBookEntry[] bidList, ISymbolOrderBookEntry[] askList) { _processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList }); _queueEvent.Set(); @@ -424,7 +424,7 @@ namespace CryptoExchange.Net.OrderBook /// The sequence number /// List of updated/new bids /// List of updated/new asks - protected void UpdateOrderBook(long updateId, IEnumerable bids, IEnumerable asks) + protected void UpdateOrderBook(long updateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) { _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids }); _queueEvent.Set(); @@ -437,7 +437,7 @@ namespace CryptoExchange.Net.OrderBook /// The sequence number of the last update /// List of updated/new bids /// List of updated/new asks - protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable bids, IEnumerable asks) + protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) { _processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids }); _queueEvent.Set(); @@ -448,7 +448,7 @@ namespace CryptoExchange.Net.OrderBook /// /// List of updated/new bids /// List of updated/new asks - protected void UpdateOrderBook(IEnumerable bids, IEnumerable asks) + protected void UpdateOrderBook(ISymbolOrderSequencedBookEntry[] bids, ISymbolOrderSequencedBookEntry[] asks) { var highest = Math.Max(bids.Any() ? bids.Max(b => b.Sequence) : 0, asks.Any() ? asks.Max(a => a.Sequence) : 0); var lowest = Math.Min(bids.Any() ? bids.Min(b => b.Sequence) : long.MaxValue, asks.Any() ? asks.Min(a => a.Sequence) : long.MaxValue); @@ -707,7 +707,7 @@ namespace CryptoExchange.Net.OrderBook UpdateTime = DateTime.UtcNow; _logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.EndUpdateId); CheckProcessBuffer(); - OnOrderBookUpdate?.Invoke((item.Bids, item.Asks)); + OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray())); OnBestOffersChanged?.Invoke((BestBid, BestAsk)); } } @@ -745,7 +745,7 @@ namespace CryptoExchange.Net.OrderBook return; } - OnOrderBookUpdate?.Invoke((item.Bids, item.Asks)); + OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray())); CheckBestOffersChanged(prevBestBid, prevBestAsk); } } diff --git a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs index 8eb089a..c07c319 100644 --- a/CryptoExchange.Net/RateLimiting/RateLimitGate.cs +++ b/CryptoExchange.Net/RateLimiting/RateLimitGate.cs @@ -46,11 +46,11 @@ namespace CryptoExchange.Net.RateLimiting { return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } - catch (TaskCanceledException) + catch (TaskCanceledException tce) { // The semaphore has already been released if the task was cancelled release = false; - return new CallResult(new CancellationRequestedError()); + return new CallResult(new CancellationRequestedError(tce)); } finally { @@ -81,11 +81,11 @@ namespace CryptoExchange.Net.RateLimiting { return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false); } - catch (TaskCanceledException) + catch (TaskCanceledException tce) { // The semaphore has already been released if the task was cancelled release = false; - return new CallResult(new CancellationRequestedError()); + return new CallResult(new CancellationRequestedError(tce)); } finally { @@ -146,7 +146,7 @@ namespace CryptoExchange.Net.RateLimiting } } - return new CallResult(null); + return CallResult.SuccessResult; } /// diff --git a/CryptoExchange.Net/Requests/Request.cs b/CryptoExchange.Net/Requests/Request.cs index f73b0fc..23f1f1c 100644 --- a/CryptoExchange.Net/Requests/Request.cs +++ b/CryptoExchange.Net/Requests/Request.cs @@ -67,9 +67,9 @@ namespace CryptoExchange.Net.Requests } /// - public Dictionary> GetHeaders() + public KeyValuePair[] GetHeaders() { - return _request.Headers.ToDictionary(h => h.Key, h => h.Value); + return _request.Headers.Select(h => new KeyValuePair(h.Key, h.Value.ToArray())).ToArray(); } /// diff --git a/CryptoExchange.Net/Requests/Response.cs b/CryptoExchange.Net/Requests/Response.cs index 55f74ad..78505b1 100644 --- a/CryptoExchange.Net/Requests/Response.cs +++ b/CryptoExchange.Net/Requests/Response.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -24,7 +25,7 @@ namespace CryptoExchange.Net.Requests public long? ContentLength => _response.Content.Headers.ContentLength; /// - public IEnumerable>> ResponseHeaders => _response.Headers; + public KeyValuePair[] ResponseHeaders => _response.Headers.Select(x => new KeyValuePair(x.Key, x.Value.ToArray())).ToArray(); /// /// Create response for a http response message diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedQuoteQuantitySupport.cs b/CryptoExchange.Net/SharedApis/Enums/SharedQuantityType.cs similarity index 100% rename from CryptoExchange.Net/SharedApis/Enums/SharedQuoteQuantitySupport.cs rename to CryptoExchange.Net/SharedApis/Enums/SharedQuantityType.cs diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs new file mode 100644 index 0000000..c8cd0ab --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Take Profit / Stop Loss side + /// + public enum SharedTpSlSide + { + /// + /// Take profit + /// + TakeProfit, + /// + /// Stop loss + /// + StopLoss + } +} diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs new file mode 100644 index 0000000..1aefff9 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderDirection.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// The order direction when order trigger parameters are reached + /// + public enum SharedTriggerOrderDirection + { + /// + /// Enter, Buy for Spot and long futures positions, Sell for short futures positions + /// + Enter, + /// + /// Exit, Sell for Spot and long futures positions, Buy for short futures positions + /// + Exit + } +} diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs new file mode 100644 index 0000000..60082a9 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerOrderStatus.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Trigger order status + /// + public enum SharedTriggerOrderStatus + { + /// + /// Order is active + /// + Active, + /// + /// Order has been filled + /// + Filled, + /// + /// Trigger canceled, can be user cancelation or system cancelation due to an error + /// + CanceledOrRejected, + /// + /// Trigger order has been triggered. Resulting order might be filled or not. + /// + Triggered + } +} diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs new file mode 100644 index 0000000..be513d2 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceDirection.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Price direction for trigger order + /// + public enum SharedTriggerPriceDirection + { + /// + /// Trigger when the price goes below the specified trigger price + /// + PriceBelow, + /// + /// Trigger when the price goes above the specified trigger price + /// + PriceAbove + } +} diff --git a/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs new file mode 100644 index 0000000..84a2a1c --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Enums/SharedTriggerPriceType.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Price direction for trigger order + /// + public enum SharedTriggerPriceType + { + /// + /// Last traded price + /// + LastPrice, + /// + /// Mark price + /// + MarkPrice, + /// + /// Index price + /// + IndexPrice + } +} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs index e502164..0553db7 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFundingRateRestClient.cs @@ -19,6 +19,6 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// The pagination token from the previous request to continue pagination /// Cancellation token - Task>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs new file mode 100644 index 0000000..f937403 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderClientIdRestClient.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Threading; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Client for managing futures orders using a client order id + /// + public interface IFuturesOrderClientIdRestClient : ISharedClient + { + /// + /// Futures get order by client order id request options + /// + EndpointOptions GetFuturesOrderByClientOrderIdOptions { get; } + + /// + /// Get info on a specific futures order using a client order id + /// + /// Request info + /// Cancellation token + Task> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); + + /// + /// Futures cancel order by client order id request options + /// + EndpointOptions CancelFuturesOrderByClientOrderIdOptions { get; } + /// + /// Cancel a futures order using client order id + /// + /// Request info + /// Cancellation token + Task> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); + } +} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs index 7142432..42b2ef1 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesOrderRestClient.cs @@ -21,20 +21,27 @@ namespace CryptoExchange.Net.SharedApis /// /// Supported order types /// - IEnumerable FuturesSupportedOrderTypes { get; } + SharedOrderType[] FuturesSupportedOrderTypes { get; } /// /// Supported time in force /// - IEnumerable FuturesSupportedTimeInForce { get; } + SharedTimeInForce[] FuturesSupportedTimeInForce { get; } /// /// Quantity types support /// SharedQuantitySupport FuturesSupportedOrderQuantity { get; } + /// + /// Generate a new random client order id + /// + /// + string GenerateClientOrderId(); + /// /// Futures place order request options /// PlaceFuturesOrderOptions PlaceFuturesOrderOptions { get; } + /// /// Place a new futures order /// @@ -62,7 +69,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Request info /// Cancellation token - Task>> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); + Task> GetOpenFuturesOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); /// /// Spot get closed orders request options @@ -74,7 +81,7 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// The pagination token from the previous request to continue pagination /// Cancellation token - Task>> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetClosedFuturesOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); /// /// Futures get order trades request options @@ -85,7 +92,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Request info /// Cancellation token - Task>> GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); + Task> GetFuturesOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); /// /// Futures user trades request options @@ -97,7 +104,7 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// The pagination token from the previous request to continue pagination /// Cancellation token - Task>> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetFuturesUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); /// /// Futures cancel order request options @@ -119,7 +126,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Request info /// Cancellation token - Task>> GetPositionsAsync(GetPositionsRequest request, CancellationToken ct = default); + Task> GetPositionsAsync(GetPositionsRequest request, CancellationToken ct = default); /// /// Close position order request options diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs index 1a3bb17..672ef51 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesSymbolRestClient.cs @@ -14,10 +14,10 @@ namespace CryptoExchange.Net.SharedApis /// EndpointOptions GetFuturesSymbolsOptions { get; } /// - /// Get info on all futures symbols supported on the exchagne + /// Get info on all futures symbols supported on the exchange /// /// Request info /// Cancellation token - Task>> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); + Task> GetFuturesSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs index c08ca0a..b11c01c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTickerRestClient.cs @@ -25,10 +25,10 @@ namespace CryptoExchange.Net.SharedApis /// EndpointOptions GetFuturesTickersOptions { get; } /// - /// Get ticker info for aall futures symbols + /// Get ticker info for all futures symbols /// /// Request info /// Cancellation token - Task>> GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct = default); + Task> GetFuturesTickersAsync(GetTickersRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs new file mode 100644 index 0000000..adc9f94 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTpSlRestClient.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Threading; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Take profit / Stop loss client + /// + public interface IFuturesTpSlRestClient : ISharedClient + { + /// + /// Set take profit and/or stop loss options + /// + EndpointOptions SetFuturesTpSlOptions { get; } + /// + /// Set a take profit and/or stop loss for an open position + /// + /// Request info + /// Cancellation token + /// + Task> SetFuturesTpSlAsync(SetTpSlRequest request, CancellationToken ct = default); + + /// + /// Cancel a take profit and/or stop loss options + /// + EndpointOptions CancelFuturesTpSlOptions { get; } + /// + /// Cancel an active take profit and/or stop loss for an open position + /// + /// Request info + /// Cancellation token + /// + Task> CancelFuturesTpSlAsync(CancelTpSlRequest request, CancellationToken ct = default); + } +} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs new file mode 100644 index 0000000..176f2ef --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IFuturesTriggerOrderRestClient.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Client for placing trigger orders + /// + public interface IFuturesTriggerOrderRestClient : ISharedClient + { + /// + /// Place spot trigger order options + /// + PlaceFuturesTriggerOrderOptions PlaceFuturesTriggerOrderOptions { get; } + + /// + /// Place a new trigger order + /// + /// Request info + /// Cancellation token + /// + Task> PlaceFuturesTriggerOrderAsync(PlaceFuturesTriggerOrderRequest request, CancellationToken ct = default); + + + /// + /// Get trigger order request options + /// + EndpointOptions GetFuturesTriggerOrderOptions { get; } + /// + /// Get info on a specific trigger order + /// + /// Request info + /// Cancellation token + Task> GetFuturesTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); + + /// + /// Cancel trigger order request options + /// + EndpointOptions CancelFuturesTriggerOrderOptions { get; } + /// + /// Cancel a trigger order + /// + /// Request info + /// Cancellation token + Task> CancelFuturesTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); + } +} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs index b77c3b0..103055d 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IIndexPriceKlineRestClient.cs @@ -19,6 +19,6 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// The pagination token from the previous request to continue pagination /// Cancellation token - Task>> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetIndexPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs index dca1414..69f2f5c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IMarkPriceKlineRestClient.cs @@ -19,6 +19,6 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// The pagination token from the previous request to continue pagination /// Cancellation token - Task>> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetMarkPriceKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs index 31135c4..8097fff 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Futures/IPositionHistoryRestClient.cs @@ -19,6 +19,6 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// The pagination token from the previous request to continue pagination /// Cancellation token - Task>> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetPositionHistoryAsync(GetPositionHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs index ca0e1b4..a688386 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IAssetsRestClient.cs @@ -31,6 +31,6 @@ namespace CryptoExchange.Net.SharedApis /// /// Request info /// Cancellation token - Task>> GetAssetsAsync(GetAssetsRequest request, CancellationToken ct = default); + Task> GetAssetsAsync(GetAssetsRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs index 1220053..8983c61 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBalanceRestClient.cs @@ -20,6 +20,6 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// Cancellation token /// - Task>> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default); + Task> GetBalancesAsync(GetBalancesRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs new file mode 100644 index 0000000..a0ca57d --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IBookTickerRestClient.cs @@ -0,0 +1,24 @@ +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Client for retrieving the current best bid/ask price + /// + public interface IBookTickerRestClient : ISharedClient + { + /// + /// Book ticker request options + /// + EndpointOptions GetBookTickerOptions { get; } + + /// + /// Get the best ask/bid info for a symbol + /// + /// Request info + /// Cancellation token + /// + Task> GetBookTickerAsync(GetBookTickerRequest request, CancellationToken ct = default); + } +} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs index 261c275..0229d4a 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IDepositRestClient.cs @@ -20,7 +20,7 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// Cancellation token /// - Task>> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default); + Task> GetDepositAddressesAsync(GetDepositAddressesRequest request, CancellationToken ct = default); /// /// Deposits request options @@ -34,6 +34,6 @@ namespace CryptoExchange.Net.SharedApis /// The pagination token from the previous request to continue pagination /// Cancellation token /// - Task>> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetDepositsAsync(GetDepositsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs index 39cfb30..06cee87 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IKlineRestClient.cs @@ -21,6 +21,6 @@ namespace CryptoExchange.Net.SharedApis /// The pagination token from the previous request to continue pagination /// Cancellation token /// - Task>> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetKlinesAsync(GetKlinesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs index f89afdb..714b7d5 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IRecentTradeRestClient.cs @@ -20,6 +20,6 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// Cancellation token /// - Task>> GetRecentTradesAsync(GetRecentTradesRequest request, CancellationToken ct = default); + Task> GetRecentTradesAsync(GetRecentTradesRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs index a99e723..e14c64d 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/ITradeHistoryRestClient.cs @@ -21,6 +21,6 @@ namespace CryptoExchange.Net.SharedApis /// The pagination token from the previous request to continue pagination /// Cancellation token /// - Task>> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetTradeHistoryAsync(GetTradeHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs index 9e24915..3316888 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/IWithdrawalRestClient .cs @@ -21,6 +21,6 @@ namespace CryptoExchange.Net.SharedApis /// The pagination token from the previous request to continue pagination /// Cancellation token /// - Task>> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetWithdrawalsAsync(GetWithdrawalsRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs new file mode 100644 index 0000000..c77a76c --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderClientIdRestClient.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using System.Threading; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Client for managing spot orders using a client order id + /// + public interface ISpotOrderClientIdRestClient : ISharedClient + { + /// + /// Spot get order by client order id request options + /// + EndpointOptions GetSpotOrderByClientOrderIdOptions { get; } + + /// + /// Get info on a specific spot order using a client order id + /// + /// Request info + /// Cancellation token + Task> GetSpotOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default); + + /// + /// Spot cancel order by client order id request options + /// + EndpointOptions CancelSpotOrderByClientOrderIdOptions { get; } + /// + /// Cancel a spot order using client order id + /// + /// Request info + /// Cancellation token + Task> CancelSpotOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default); + } +} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs index fa6dd66..cb5e816 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotOrderRestClient.cs @@ -21,16 +21,22 @@ namespace CryptoExchange.Net.SharedApis /// /// Supported order types /// - IEnumerable SpotSupportedOrderTypes { get; } + SharedOrderType[] SpotSupportedOrderTypes { get; } /// /// Supported time in force /// - IEnumerable SpotSupportedTimeInForce { get; } + SharedTimeInForce[] SpotSupportedTimeInForce { get; } /// /// Quantity types support /// SharedQuantitySupport SpotSupportedOrderQuantity { get; } + /// + /// Generate a new random client order id + /// + /// + string GenerateClientOrderId(); + /// /// Spot place order request options /// @@ -62,7 +68,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Request info /// Cancellation token - Task>> GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); + Task> GetOpenSpotOrdersAsync(GetOpenOrdersRequest request, CancellationToken ct = default); /// /// Spot get closed orders request options @@ -74,7 +80,7 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// The pagination token from the previous request to continue pagination /// Cancellation token - Task>> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetClosedSpotOrdersAsync(GetClosedOrdersRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); /// /// Spot get order trades request options @@ -85,7 +91,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Request info /// Cancellation token - Task>> GetSpotOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); + Task> GetSpotOrderTradesAsync(GetOrderTradesRequest request, CancellationToken ct = default); /// /// Spot user trades request options @@ -97,7 +103,7 @@ namespace CryptoExchange.Net.SharedApis /// Request info /// The pagination token from the previous request to continue pagination /// Cancellation token - Task>> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); + Task> GetSpotUserTradesAsync(GetUserTradesRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default); /// /// Spot cancel order request options diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs index b982bd0..520f07c 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotSymbolRestClient.cs @@ -19,6 +19,6 @@ namespace CryptoExchange.Net.SharedApis /// /// Request info /// Cancellation token - Task>> GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); + Task> GetSpotSymbolsAsync(GetSymbolsRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs index dbb7abc..84a9108 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTickerRestClient.cs @@ -28,6 +28,6 @@ namespace CryptoExchange.Net.SharedApis /// /// Request info /// Cancellation token - Task>> GetSpotTickersAsync(GetTickersRequest request, CancellationToken ct = default); + Task> GetSpotTickersAsync(GetTickersRequest request, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs new file mode 100644 index 0000000..a3009c7 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Interfaces/Rest/Spot/ISpotTriggerOrderRestClient.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Client for placing trigger orders + /// + public interface ISpotTriggerOrderRestClient : ISharedClient + { + /// + /// Place spot trigger order options + /// + PlaceSpotTriggerOrderOptions PlaceSpotTriggerOrderOptions { get; } + + /// + /// Place a new trigger order + /// + /// Request info + /// Cancellation token + /// + Task> PlaceSpotTriggerOrderAsync(PlaceSpotTriggerOrderRequest request, CancellationToken ct = default); + + /// + /// Get trigger order request options + /// + EndpointOptions GetSpotTriggerOrderOptions { get; } + /// + /// Get info on a specific trigger order + /// + /// Request info + /// Cancellation token + Task> GetSpotTriggerOrderAsync(GetOrderRequest request, CancellationToken ct = default); + + /// + /// Cancel trigger order request options + /// + EndpointOptions CancelSpotTriggerOrderOptions { get; } + /// + /// Cancel a trigger order + /// + /// Request info + /// Cancellation token + Task> CancelSpotTriggerOrderAsync(CancelOrderRequest request, CancellationToken ct = default); + } +} diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs index 8cfa4eb..db22ab6 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IFuturesOrderSocketClient.cs @@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action>> handler, CancellationToken ct = default); + Task> SubscribeToFuturesOrderUpdatesAsync(SubscribeFuturesOrderRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs index d657748..03cba56 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Futures/IPositionSocketClient.cs @@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action>> handler, CancellationToken ct = default); + Task> SubscribeToPositionUpdatesAsync(SubscribePositionRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs index f9df2de..24c66a6 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IBalanceSocketClient.cs @@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action>> handler, CancellationToken ct = default); + Task> SubscribeToBalanceUpdatesAsync(SubscribeBalancesRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs index e187f1e..f3652c3 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITickersSocketClient.cs @@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action>> handler, CancellationToken ct = default); + Task> SubscribeToAllTickersUpdatesAsync(SubscribeAllTickersRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs index a46b072..e6a8f21 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/ITradeSocketClient.cs @@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action>> handler, CancellationToken ct = default); + Task> SubscribeToTradeUpdatesAsync(SubscribeTradeRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs index 941afed..031e9f8 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/IUserTradeSocketClient.cs @@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action>> handler, CancellationToken ct = default); + Task> SubscribeToUserTradeUpdatesAsync(SubscribeUserTradeRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs index 5852aff..12d24b2 100644 --- a/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs +++ b/CryptoExchange.Net/SharedApis/Interfaces/Socket/Spot/ISpotOrderSocketClient.cs @@ -23,6 +23,6 @@ namespace CryptoExchange.Net.SharedApis /// Update handler /// Cancellation token, can be used to stop the updates /// - Task> SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action>> handler, CancellationToken ct = default); + Task> SubscribeToSpotOrderUpdatesAsync(SubscribeSpotOrderRequest request, Action> handler, CancellationToken ct = default); } } diff --git a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs index 61908e4..b00ad20 100644 --- a/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs +++ b/CryptoExchange.Net/SharedApis/Models/ExchangeWebResult.cs @@ -100,7 +100,7 @@ namespace CryptoExchange.Net.SharedApis string exchange, TradingMode[]? dataTradeModes, HttpStatusCode? code, - IEnumerable>>? responseHeaders, + KeyValuePair[]? responseHeaders, TimeSpan? responseTime, long? responseLength, string? originalData, @@ -108,7 +108,7 @@ namespace CryptoExchange.Net.SharedApis string? requestUrl, string? requestBody, HttpMethod? requestMethod, - IEnumerable>>? requestHeaders, + KeyValuePair[]? requestHeaders, ResultDataSource dataSource, [AllowNull] T data, Error? error, diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs index bd28507..3bc4b4b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/EndpointOptions.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text; @@ -17,6 +18,10 @@ namespace CryptoExchange.Net.SharedApis /// public List RequiredExchangeParameters { get; set; } = new List(); /// + /// Optional exchange-specific parameters + /// + public List OptionalExchangeParameters { get; set; } = new List(); + /// /// Endpoint name /// public string EndpointName { get; set; } @@ -28,6 +33,10 @@ namespace CryptoExchange.Net.SharedApis /// Whether the call requires authentication /// public bool NeedsAuthentication { get; set; } + /// + /// Whether the call is supported by the exchange + /// + public bool Supported { get; set; } = true; /// /// ctor @@ -71,12 +80,16 @@ namespace CryptoExchange.Net.SharedApis /// public virtual string ToString(string exchange) { + if (!Supported) + return $"{exchange} {EndpointName} NOT SUPPORTED"; + var sb = new StringBuilder(); sb.AppendLine($"{exchange} {EndpointName}"); if (!string.IsNullOrEmpty(RequestNotes)) sb.AppendLine(RequestNotes); sb.AppendLine($"Needs authentication: {NeedsAuthentication}"); sb.AppendLine($"Required exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); + sb.AppendLine($"Optional exchange specific parameters: {string.Join(", ", OptionalExchangeParameters.Select(x => x.ToString()))}"); return sb.ToString(); } } @@ -85,7 +98,11 @@ namespace CryptoExchange.Net.SharedApis /// Options for an exchange endpoint /// /// Type of data +#if NET5_0_OR_GREATER + public class EndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : EndpointOptions where T : SharedRequest +#else public class EndpointOptions : EndpointOptions where T : SharedRequest +#endif { /// /// Required optional parameters in the request @@ -130,6 +147,9 @@ namespace CryptoExchange.Net.SharedApis /// public override string ToString(string exchange) { + if (!Supported) + return $"{exchange} {EndpointName} NOT SUPPORTED"; + var sb = new StringBuilder(); sb.AppendLine($"{exchange} {typeof(T).Name}"); sb.AppendLine($"Needs authentication: {NeedsAuthentication}"); @@ -139,6 +159,8 @@ namespace CryptoExchange.Net.SharedApis sb.AppendLine($"Required optional parameters: {string.Join(", ", RequiredOptionalParameters.Select(x => x.ToString()))}"); if (RequiredExchangeParameters.Any()) sb.AppendLine($"Required exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); + if (OptionalExchangeParameters.Any()) + sb.AppendLine($"Optional exchange specific parameters: {string.Join(", ", RequiredExchangeParameters.Select(x => x.ToString()))}"); return sb.ToString(); } } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs index d94e7af..1a1b97e 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetKlinesOptions.cs @@ -14,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// The supported kline intervals /// - public IEnumerable SupportIntervals { get; } + public SharedKlineInterval[] SupportIntervals { get; } /// /// Max number of data points which can be requested /// diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs index 12e2745..7c117ee 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/GetOrderBookOptions.cs @@ -14,7 +14,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Supported order book depths /// - public IEnumerable? SupportedLimits { get; set; } + public int[]? SupportedLimits { get; set; } /// /// The min order book depth @@ -37,7 +37,7 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public GetOrderBookOptions(IEnumerable supportedLimits, bool authenticated) : base(authenticated) + public GetOrderBookOptions(int[] supportedLimits, bool authenticated) : base(authenticated) { SupportedLimits = supportedLimits; } diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs index 572a9ef..10e98bb 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PaginatedEndpointOptions.cs @@ -1,4 +1,5 @@ using CryptoExchange.Net.Objects; +using System.Diagnostics.CodeAnalysis; using System.Text; namespace CryptoExchange.Net.SharedApis @@ -7,7 +8,11 @@ namespace CryptoExchange.Net.SharedApis /// Options for paginated endpoints /// /// +#if NET5_0_OR_GREATER + public class PaginatedEndpointOptions<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : EndpointOptions where T : SharedRequest +#else public class PaginatedEndpointOptions : EndpointOptions where T : SharedRequest +#endif { /// /// Type of pagination supported diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs index d989a78..f89d1d3 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesOrderOptions.cs @@ -10,11 +10,17 @@ namespace CryptoExchange.Net.SharedApis /// public class PlaceFuturesOrderOptions : EndpointOptions { + /// + /// Whether or not the API supports setting take profit / stop loss with the order + /// + public bool SupportsTpSl { get; set; } + /// /// ctor /// - public PlaceFuturesOrderOptions() : base(true) + public PlaceFuturesOrderOptions(bool supportsTpSl) : base(true) { + SupportsTpSl = supportsTpSl; } /// @@ -25,10 +31,13 @@ namespace CryptoExchange.Net.SharedApis PlaceFuturesOrderRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes, - IEnumerable supportedOrderTypes, - IEnumerable supportedTimeInForce, + SharedOrderType[] supportedOrderTypes, + SharedTimeInForce[] supportedTimeInForce, SharedQuantitySupport quantitySupport) { + if (!SupportsTpSl && (request.StopLossPrice != null || request.TakeProfitPrice != null)) + return new ArgumentError("Tp/Sl parameters not supported"); + if (request.OrderType == SharedOrderType.Other) throw new ArgumentException("OrderType can't be `Other`", nameof(request.OrderType)); @@ -38,7 +47,7 @@ namespace CryptoExchange.Net.SharedApis if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) return new ArgumentError("Order time in force not supported"); - var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity, request.QuoteQuantity); + var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); if (quantityError != null) return quantityError; diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs new file mode 100644 index 0000000..a6e43db --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceFuturesTriggerOrderOptions.cs @@ -0,0 +1,44 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for placing a new spot trigger order + /// + public class PlaceFuturesTriggerOrderOptions : EndpointOptions + { + /// + /// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time. + /// + public bool HoldsFunds { get; set; } + + /// + /// ctor + /// + public PlaceFuturesTriggerOrderOptions(bool holdsFunds) : base(true) + { + HoldsFunds = holdsFunds; + } + + /// + /// Validate a request + /// + public Error? ValidateRequest( + string exchange, + PlaceFuturesTriggerOrderRequest request, + TradingMode? tradingMode, + TradingMode[] supportedApiTypes, + SharedOrderSide side, + SharedQuantitySupport quantitySupport) + { + var quantityError = quantitySupport.Validate(side, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); + if (quantityError != null) + return quantityError; + + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs index 36edbf2..75c61b7 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotOrderOptions.cs @@ -26,8 +26,8 @@ namespace CryptoExchange.Net.SharedApis PlaceSpotOrderRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes, - IEnumerable supportedOrderTypes, - IEnumerable supportedTimeInForce, + SharedOrderType[] supportedOrderTypes, + SharedTimeInForce[] supportedTimeInForce, SharedQuantitySupport quantitySupport) { if (request.OrderType == SharedOrderType.Other) @@ -39,7 +39,7 @@ namespace CryptoExchange.Net.SharedApis if (request.TimeInForce != null && !supportedTimeInForce.Contains(request.TimeInForce.Value)) return new ArgumentError("Order time in force not supported"); - var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity, request.QuoteQuantity); + var quantityError = quantitySupport.Validate(request.Side, request.OrderType, request.Quantity); if (quantityError != null) return quantityError; diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs new file mode 100644 index 0000000..ce7fd30 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Options/Endpoints/PlaceSpotTriggerOrderOptions.cs @@ -0,0 +1,43 @@ +using CryptoExchange.Net.Objects; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Options for placing a new spot trigger order + /// + public class PlaceSpotTriggerOrderOptions : EndpointOptions + { + /// + /// When true the API holds the funds until the order is triggered or canceled. When true the funds will only be required when the order is triggered and will fail if the funds are not available at that time. + /// + public bool HoldsFunds { get; set; } + + /// + /// ctor + /// + public PlaceSpotTriggerOrderOptions(bool holdsFunds) : base(true) + { + HoldsFunds = holdsFunds; + } + + /// + /// Validate a request + /// + public Error? ValidateRequest( + string exchange, + PlaceSpotTriggerOrderRequest request, + TradingMode? tradingMode, + TradingMode[] supportedApiTypes, + SharedQuantitySupport quantitySupport) + { + var quantityError = quantitySupport.Validate(request.OrderSide, request.OrderPrice == null ? SharedOrderType.Market : SharedOrderType.Limit, request.Quantity); + if (quantityError != null) + return quantityError; + + return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes); + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs index f46bd74..f6c4d7f 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeKlineOptions.cs @@ -13,7 +13,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Kline intervals supported for updates /// - public IEnumerable SupportIntervals { get; } + public SharedKlineInterval[] SupportIntervals { get; } /// /// ctor diff --git a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs index 9b4bf27..bd3c36b 100644 --- a/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs +++ b/CryptoExchange.Net/SharedApis/Models/Options/Subscriptions/SubscribeOrderBookOptions.cs @@ -13,12 +13,12 @@ namespace CryptoExchange.Net.SharedApis /// /// Order book depths supported for updates /// - public IEnumerable SupportedLimits { get; } + public int[] SupportedLimits { get; } /// /// ctor /// - public SubscribeOrderBookOptions(bool needsAuthentication, IEnumerable limits) : base(needsAuthentication) + public SubscribeOrderBookOptions(bool needsAuthentication, int[] limits) : base(needsAuthentication) { SupportedLimits = limits; } diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/CancelTpSlRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/CancelTpSlRequest.cs new file mode 100644 index 0000000..ae25693 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Rest/CancelTpSlRequest.cs @@ -0,0 +1,56 @@ +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Request to cancel a take profit / stop loss + /// + public record CancelTpSlRequest : SharedSymbolRequest + { + /// + /// Id of order to cancel + /// + public string? OrderId { get; set; } + + /// + /// Position mode + /// + public SharedPositionMode? PositionMode { get; set; } + /// + /// Position side + /// + public SharedPositionSide? PositionSide { get; set; } + /// + /// Take profit / Stop loss side + /// + public SharedTpSlSide? TpSlSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } + + /// + /// ctor for canceling by order id + /// + /// Symbol the order is on + /// Id of the order to close + /// Exchange specific parameters + public CancelTpSlRequest(SharedSymbol symbol, string orderId, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + OrderId = orderId; + } + + /// + /// ctor for canceling without order id + /// + /// Symbol the order is on + /// The position mode of the account + /// The side of the position + /// The side to cancel + /// Exchange specific parameters + public CancelTpSlRequest(SharedSymbol symbol, SharedPositionMode mode, SharedPositionSide positionSide, SharedTpSlSide tpSlSide, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + PositionMode = mode; + PositionSide = positionSide; + TpSlSide = tpSlSide; + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetBookTickerRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetBookTickerRequest.cs new file mode 100644 index 0000000..51f374d --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetBookTickerRequest.cs @@ -0,0 +1,17 @@ +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Request to retrieve best bid/ask info for a symbol + /// + public record GetBookTickerRequest : SharedSymbolRequest + { + /// + /// ctor + /// + /// Symbol to retrieve book ticker for + /// Exchange specific parameters + public GetBookTickerRequest(SharedSymbol symbol, ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderRequest.cs index cd86800..944edb2 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/GetOrderRequest.cs @@ -1,7 +1,7 @@ namespace CryptoExchange.Net.SharedApis { /// - /// Request to retrieve info on a specifc order + /// Request to retrieve info on a specific order /// public record GetOrderRequest : SharedSymbolRequest { diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs index 59cdfb8..42989f7 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesOrderRequest.cs @@ -18,13 +18,9 @@ /// public SharedTimeInForce? TimeInForce { get; set; } /// - /// Quantity of the order in base asset. + /// Quantity of the order /// - public decimal? Quantity { get; set; } - /// - /// Quantity of the order in quote asset. - /// - public decimal? QuoteQuantity { get; set; } + public SharedQuantity? Quantity { get; set; } /// /// Price of the order /// @@ -50,6 +46,15 @@ /// public decimal? Leverage { get; set; } + /// + /// Take profit price + /// + public decimal? TakeProfitPrice { get; set; } + /// + /// Stop loss price + /// + public decimal? StopLossPrice { get; set; } + /// /// ctor /// @@ -58,7 +63,6 @@ /// Side of the order /// Type of the order /// Quantity of the order - /// Quantity of the order in quote asset /// Price of the order /// Time in force /// Client order id @@ -71,8 +75,7 @@ SharedSymbol symbol, SharedOrderSide side, SharedOrderType orderType, - decimal? quantity = null, - decimal? quoteQuantity = null, + SharedQuantity? quantity = null, decimal? price = null, bool? reduceOnly = null, decimal? leverage = null, @@ -85,7 +88,6 @@ Side = side; OrderType = orderType; Quantity = quantity; - QuoteQuantity = quoteQuantity; Price = price; MarginMode = marginMode; ClientOrderId = clientOrderId; diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesTriggerOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesTriggerOrderRequest.cs new file mode 100644 index 0000000..83559b1 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceFuturesTriggerOrderRequest.cs @@ -0,0 +1,85 @@ +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Request to place a new trigger order + /// + public record PlaceFuturesTriggerOrderRequest : SharedSymbolRequest + { + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } + /// + /// Direction of the trigger order + /// + public SharedTriggerOrderDirection OrderDirection { get; set; } + /// + /// Price trigger direction + /// + public SharedTriggerPriceDirection PriceDirection { get; set; } + /// + /// Quantity of the order + /// + public SharedQuantity Quantity { get; set; } + /// + /// Price of the order + /// + public decimal? OrderPrice { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } + /// + /// Time in force + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Position mode + /// + public SharedPositionMode? PositionMode { get; set; } + /// + /// Position side + /// + public SharedPositionSide PositionSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } + /// + /// Leverage + /// + public decimal? Leverage { get; set; } + /// + /// Trigger price type + /// + public SharedTriggerPriceType? TriggerPriceType { get; set; } + + /// + /// ctor + /// + /// Symbol the order is on + /// Direction of the order when triggered + /// Price direction + /// Quantity of the order + /// Position side + /// Price at which the order should activate + /// Limit price for the order + /// Exchange specific parameters + public PlaceFuturesTriggerOrderRequest(SharedSymbol symbol, + SharedTriggerPriceDirection priceDirection, + decimal triggerPrice, + SharedTriggerOrderDirection orderDirection, + SharedPositionSide positionSide, + SharedQuantity quantity, + decimal? orderPrice = null, + ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + PriceDirection = priceDirection; + PositionSide = positionSide; + Quantity = quantity; + OrderPrice = orderPrice; + TriggerPrice = triggerPrice; + OrderDirection = orderDirection; + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotOrderRequest.cs index a2a2c99..379c6ec 100644 --- a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotOrderRequest.cs +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotOrderRequest.cs @@ -18,13 +18,9 @@ /// public SharedTimeInForce? TimeInForce { get; set; } /// - /// Quantity of the order in base asset or contracts, depending on the exchange. + /// Quantity of the order /// - public decimal? Quantity { get; set; } - /// - /// Quantity of the order in quote asset. - /// - public decimal? QuoteQuantity { get; set; } + public SharedQuantity? Quantity { get; set; } /// /// Price of the order /// @@ -41,7 +37,6 @@ /// Side of the order /// Type of the order /// Quantity of the order - /// Quantity of the order in quote asset /// Price of the order /// Time in force /// Client order id @@ -50,8 +45,7 @@ SharedSymbol symbol, SharedOrderSide side, SharedOrderType orderType, - decimal? quantity = null, - decimal? quoteQuantity = null, + SharedQuantity? quantity = null, decimal? price = null, SharedTimeInForce? timeInForce = null, string? clientOrderId = null, @@ -60,7 +54,6 @@ OrderType = orderType; Side = side; Quantity = quantity; - QuoteQuantity = quoteQuantity; Price = price; TimeInForce = timeInForce; ClientOrderId = clientOrderId; diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotTriggerOrderRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotTriggerOrderRequest.cs new file mode 100644 index 0000000..5f4e686 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Rest/PlaceSpotTriggerOrderRequest.cs @@ -0,0 +1,62 @@ +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Request to place a new trigger order + /// + public record PlaceSpotTriggerOrderRequest : SharedSymbolRequest + { + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } + /// + /// Direction of the trigger order + /// + public SharedOrderSide OrderSide { get; set; } + /// + /// Price trigger direction + /// + public SharedTriggerPriceDirection PriceDirection { get; set; } + /// + /// Time in force + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Quantity of the order + /// + public SharedQuantity Quantity { get; set; } + /// + /// Price of the order + /// + public decimal? OrderPrice { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } + + /// + /// ctor + /// + /// Symbol the order is on + /// Order side + /// Price direction + /// Quantity of the order + /// Price at which the order should activate + /// Limit price for the order + /// Exchange specific parameters + public PlaceSpotTriggerOrderRequest(SharedSymbol symbol, + SharedTriggerPriceDirection priceDirection, + decimal triggerPrice, + SharedOrderSide orderSide, + SharedQuantity quantity, + decimal? orderPrice = null, + ExchangeParameters? exchangeParameters = null) : base(symbol, exchangeParameters) + { + PriceDirection = priceDirection; + Quantity = quantity; + OrderPrice = orderPrice; + TriggerPrice = triggerPrice; + OrderSide = orderSide; + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/Rest/SetTpSlRequest.cs b/CryptoExchange.Net/SharedApis/Models/Rest/SetTpSlRequest.cs new file mode 100644 index 0000000..eef810a --- /dev/null +++ b/CryptoExchange.Net/SharedApis/Models/Rest/SetTpSlRequest.cs @@ -0,0 +1,50 @@ +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Set a take profit and/or stop loss for an open position + /// + public record SetTpSlRequest : SharedSymbolRequest + { + /// + /// Position mode + /// + public SharedPositionMode? PositionMode { get; set; } + /// + /// Position side + /// + public SharedPositionSide PositionSide { get; set; } + /// + /// Margin mode + /// + public SharedMarginMode? MarginMode { get; set; } + /// + /// Take profit / Stop loss side + /// + public SharedTpSlSide TpSlSide { get; set; } + /// + /// Quantity to close. Only used for some API's which require a quantity in the order. Most API's will close the full position + /// + public decimal? Quantity { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } + + /// + /// ctor + /// + /// Symbol of the order + /// Position side + /// Take Profit / Stop Loss side + /// Trigger price + /// Exchange specific parameters + public SetTpSlRequest(SharedSymbol symbol, SharedPositionSide positionSide, SharedTpSlSide tpSlSide, decimal triggerPrice, ExchangeParameters? exchangeParameters = null) + : base(symbol, exchangeParameters) + { + PositionSide = positionSide; + TpSlSide = tpSlSide; + Symbol = symbol; + TriggerPrice = triggerPrice; + } + } +} diff --git a/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs b/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs index f25b5f2..2da80cb 100644 --- a/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs +++ b/CryptoExchange.Net/SharedApis/Models/SharedQuantitySupport.cs @@ -11,19 +11,19 @@ namespace CryptoExchange.Net.SharedApis /// /// Supported quantity notations for buy limit orders /// - public SharedQuantityType BuyLimit { get; } + public SharedQuantityType BuyLimit { get; set; } /// /// Supported quantity notations for sell limit orders /// - public SharedQuantityType SellLimit { get; } + public SharedQuantityType SellLimit { get; set; } /// /// Supported quantity notations for buy market orders /// - public SharedQuantityType BuyMarket { get; } + public SharedQuantityType BuyMarket { get; set; } /// /// Supported quantity notations for sell market orders /// - public SharedQuantityType SellMarket { get; } + public SharedQuantityType SellMarket { get; set; } /// /// ctor @@ -36,7 +36,13 @@ namespace CryptoExchange.Net.SharedApis SellMarket = sellMarket; } - private SharedQuantityType GetSupportedQuantityType(SharedOrderSide side, SharedOrderType orderType) + /// + /// Get the supported quantity type for a specific order configuration + /// + /// Side of the order + /// Type of the order + /// The supported quantity type + public SharedQuantityType GetSupportedQuantityType(SharedOrderSide side, SharedOrderType orderType) { if (side == SharedOrderSide.Buy && (orderType == SharedOrderType.Limit || orderType == SharedOrderType.LimitMaker)) return BuyLimit; if (side == SharedOrderSide.Buy && orderType == SharedOrderType.Market) return BuyMarket; @@ -46,25 +52,45 @@ namespace CryptoExchange.Net.SharedApis throw new ArgumentException("Unknown side/type combination"); } + /// + /// Get whether the API supports a specific quantity type for an order configuration + /// + /// Side of the order + /// Type of the order + /// Type of quantity + /// True if supported, false if not + public bool IsSupported(SharedOrderSide side, SharedOrderType orderType, SharedQuantityType quantityType) + { + var supportedType = GetSupportedQuantityType(side, orderType); + if (supportedType == quantityType) + return true; + + if (supportedType == SharedQuantityType.BaseAndQuoteAsset && (quantityType == SharedQuantityType.BaseAsset || quantityType == SharedQuantityType.QuoteAsset)) + return true; + + return false; + } + /// /// Validate a request /// - /// - /// - /// - /// - /// - public Error? Validate(SharedOrderSide side, SharedOrderType type, decimal? quantity, decimal? quoteQuantity) + public Error? Validate(SharedOrderSide side, SharedOrderType type, SharedQuantity? quantity) { var supportedType = GetSupportedQuantityType(side, type); if (supportedType == SharedQuantityType.BaseAndQuoteAsset) return null; - if ((supportedType == SharedQuantityType.BaseAsset || supportedType == SharedQuantityType.Contracts) && quoteQuantity != null) - return new ArgumentError($"Quote quantity not supported for {side}.{type} order, specify Quantity instead"); + if (supportedType == SharedQuantityType.BaseAndQuoteAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInQuoteAsset == null) + return new ArgumentError($"Quantity for {side}.{type} required in base or quote asset"); - if (supportedType == SharedQuantityType.QuoteAsset && quantity != null) - return new ArgumentError($"Quantity not supported for {side}.{type} order, specify QuoteQuantity instead"); + if (supportedType == SharedQuantityType.QuoteAsset && quantity != null && quantity.QuantityInQuoteAsset == null) + return new ArgumentError($"Quantity for {side}.{type} required in quote asset"); + + if (supportedType == SharedQuantityType.BaseAsset && quantity != null && quantity.QuantityInBaseAsset == null && quantity.QuantityInContracts == null) + return new ArgumentError($"Quantity for {side}.{type} required in base asset"); + + if (supportedType == SharedQuantityType.Contracts && quantity != null && quantity.QuantityInContracts == null) + return new ArgumentError($"Quantity for {side}.{type} required in contracts"); return null; } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs index c3d9cfc..3fe5aff 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedAsset.cs @@ -19,7 +19,7 @@ namespace CryptoExchange.Net.SharedApis /// /// Asset networks info /// - public IEnumerable? Networks { get; set; } = Array.Empty(); + public SharedAssetNetwork[]? Networks { get; set; } = Array.Empty(); /// /// ctor diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedBookTicker.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedBookTicker.cs index fc3a454..7a67a10 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedBookTicker.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedBookTicker.cs @@ -3,7 +3,7 @@ /// /// Book ticker /// - public record SharedBookTicker + public record SharedBookTicker : SharedSymbolModel { /// /// Price of the best ask @@ -25,7 +25,8 @@ /// /// ctor /// - public SharedBookTicker(decimal bestAskPrice, decimal bestAskQuantity, decimal bestBidPrice, decimal bestBidQuantity) + public SharedBookTicker(SharedSymbol? sharedSymbol, string symbol, decimal bestAskPrice, decimal bestAskQuantity, decimal bestBidPrice, decimal bestBidQuantity) + : base(sharedSymbol, symbol) { BestAskPrice = bestAskPrice; BestAskQuantity = bestAskQuantity; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesOrder.cs index 3ad4dc1..38afc31 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesOrder.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesOrder.cs @@ -5,12 +5,8 @@ namespace CryptoExchange.Net.SharedApis /// /// Futures order info /// - public record SharedFuturesOrder + public record SharedFuturesOrder : SharedSymbolModel { - /// - /// Symbol - /// - public string Symbol { get; set; } /// /// Id of the order /// @@ -40,21 +36,13 @@ namespace CryptoExchange.Net.SharedApis /// public bool? ReduceOnly { get; set; } /// - /// Order quantity in the base asset or number of contracts + /// Order quantity /// - public decimal? Quantity { get; set; } + public SharedOrderQuantity? OrderQuantity { get; set; } /// - /// Quantity filled in the base asset or number of contracts + /// Filled quantity /// - public decimal? QuantityFilled { get; set; } - /// - /// Quantity of the order in quote asset - /// - public decimal? QuoteQuantity { get; set; } - /// - /// Quantity filled in the quote asset - /// - public decimal? QuoteQuantityFilled { get; set; } + public SharedOrderQuantity? QuantityFilled { get; set; } /// /// Order price /// @@ -93,18 +81,43 @@ namespace CryptoExchange.Net.SharedApis /// public SharedUserTrade? LastTrade { get; set; } + /// + /// Trigger price for a trigger order + /// + public decimal? TriggerPrice { get; set; } + /// + /// Whether or not the is order is a trigger order + /// + public bool? IsTriggerOrder { get; set; } + + /// + /// Take profit price + /// + public decimal? TakeProfitPrice { get; set; } + + /// + /// Stop loss price + /// + public decimal? StopLossPrice { get; set; } + + /// + /// Whether this order is to close an existing position. If this is the case quantities might not be specified + /// + public bool? IsCloseOrder { get; set; } + /// /// ctor /// public SharedFuturesOrder( + SharedSymbol? sharedSymbol, string symbol, string orderId, SharedOrderType orderType, SharedOrderSide orderSide, SharedOrderStatus orderStatus, DateTime? createTime) + : base(sharedSymbol, symbol) { - Symbol = symbol; OrderId = orderId; OrderType = orderType; Side = orderSide; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesSymbol.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesSymbol.cs index 39a461f..ed4e84e 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesSymbol.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesSymbol.cs @@ -7,10 +7,6 @@ namespace CryptoExchange.Net.SharedApis /// public record SharedFuturesSymbol : SharedSpotSymbol { - /// - /// Symbol type - /// - public SharedSymbolType SymbolType { get; set; } /// /// The size of a single contract /// @@ -19,13 +15,20 @@ namespace CryptoExchange.Net.SharedApis /// Delivery time of the contract /// public DateTime? DeliveryTime { get; set; } + /// + /// Max short leverage setting + /// + public decimal? MaxShortLeverage { get; set; } + /// + /// Max long leverage setting + /// + public decimal? MaxLongLeverage { get; set; } /// /// ctor /// - public SharedFuturesSymbol(SharedSymbolType symbolType, string baseAsset, string quoteAsset, string symbol, bool trading) : base(baseAsset, quoteAsset, symbol, trading) + public SharedFuturesSymbol(TradingMode symbolType, string baseAsset, string quoteAsset, string symbol, bool trading) : base(baseAsset, quoteAsset, symbol, trading, symbolType) { - SymbolType = symbolType; } } } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTicker.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTicker.cs index 357b80c..cfd6915 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTicker.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTicker.cs @@ -5,12 +5,8 @@ namespace CryptoExchange.Net.SharedApis /// /// Futures ticker info /// - public record SharedFuturesTicker + public record SharedFuturesTicker: SharedSymbolModel { - /// - /// The symbol - /// - public string Symbol { get; set; } /// /// Last trade price /// @@ -51,9 +47,9 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public SharedFuturesTicker(string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) + public SharedFuturesTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) + :base(sharedSymbol, symbol) { - Symbol = symbol; LastPrice = lastPrice; HighPrice = highPrice; LowPrice = lowPrice; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs new file mode 100644 index 0000000..49fdbf8 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedFuturesTriggerOrder.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Trigger order info + /// + public record SharedFuturesTriggerOrder : SharedSymbolModel + { + /// + /// The id of the trigger order + /// + public string TriggerOrderId { get; set; } + /// + /// The id of the order that was placed when this order was activated + /// + public string? PlacedOrderId { get; set; } + /// + /// The type of the order + /// + public SharedOrderType OrderType { get; set; } + /// + /// Status of the trigger order + /// + public SharedTriggerOrderStatus Status { get; set; } + /// + /// Time in force for the order + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Order quantity + /// + public SharedOrderQuantity? OrderQuantity { get; set; } + /// + /// Filled quantity + /// + public SharedOrderQuantity? QuantityFilled { get; set; } + /// + /// Order price + /// + public decimal? OrderPrice { get; set; } + /// + /// Average fill price + /// + public decimal? AveragePrice { get; set; } + /// + /// Trigger order direction + /// + public SharedTriggerOrderDirection? OrderDirection { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } + /// + /// Asset the fee is in + /// + public string? FeeAsset { get; set; } + /// + /// Fee paid for the order + /// + public decimal? Fee { get; set; } + /// + /// Timestamp the order was created + /// + public DateTime? CreateTime { get; set; } + /// + /// Last update timestamp + /// + public DateTime? UpdateTime { get; set; } + /// + /// Position side for futures order + /// + public SharedPositionSide? PositionSide { get; set; } + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } + + /// + /// ctor + /// + public SharedFuturesTriggerOrder( + SharedSymbol? sharedSymbol, + string symbol, + string triggerOrderId, + SharedOrderType orderType, + SharedTriggerOrderDirection? orderDirection, + SharedTriggerOrderStatus triggerStatus, + decimal triggerPrice, + SharedPositionSide? positionSide, + DateTime? createTime) + : base(sharedSymbol, symbol) + { + TriggerOrderId = triggerOrderId; + OrderType = orderType; + OrderDirection = orderDirection; + Status = triggerStatus; + CreateTime = createTime; + TriggerPrice = triggerPrice; + PositionSide = positionSide; + } + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs index 4fecefc..d4e85c9 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedOrderBook.cs @@ -11,16 +11,16 @@ namespace CryptoExchange.Net.SharedApis /// /// Asks list /// - public IEnumerable Asks { get; set; } + public ISymbolOrderBookEntry[] Asks { get; set; } /// /// Bids list /// - public IEnumerable Bids { get; set; } + public ISymbolOrderBookEntry[] Bids { get; set; } /// /// ctor /// - public SharedOrderBook(IEnumerable asks, IEnumerable bids) + public SharedOrderBook(ISymbolOrderBookEntry[] asks, ISymbolOrderBookEntry[] bids) { Asks = asks; Bids = bids; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPosition.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPosition.cs index fac4dd0..796a77f 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPosition.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPosition.cs @@ -5,12 +5,8 @@ namespace CryptoExchange.Net.SharedApis /// /// Position info /// - public record SharedPosition + public record SharedPosition : SharedSymbolModel { - /// - /// Symbol - /// - public string Symbol { get; set; } /// /// Current size of the position /// @@ -39,13 +35,21 @@ namespace CryptoExchange.Net.SharedApis /// Last update time /// public DateTime? UpdateTime { get; set; } + /// + /// Stop loss price for the position. Not available in all API's so might be empty even though stop loss price is set + /// + public decimal? StopLossPrice { get; set; } + /// + /// Take profit price for the position. Not available in all API's so might be empty even though stop loss price is set + /// + public decimal? TakeProfitPrice { get; set; } /// /// ctor /// - public SharedPosition(string symbol, decimal positionSize, DateTime? updateTime) + public SharedPosition(SharedSymbol? sharedSymbol, string symbol, decimal positionSize, DateTime? updateTime) + : base(sharedSymbol, symbol) { - Symbol = symbol; PositionSize = positionSize; UpdateTime = updateTime; } diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionHistory.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionHistory.cs index 42263a9..e4f1261 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionHistory.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedPositionHistory.cs @@ -5,12 +5,8 @@ namespace CryptoExchange.Net.SharedApis /// /// Position history /// - public record SharedPositionHistory + public record SharedPositionHistory : SharedSymbolModel { - /// - /// Symbol of the position - /// - public string Symbol { get; set; } /// /// The side of the position /// @@ -52,6 +48,7 @@ namespace CryptoExchange.Net.SharedApis /// ctor /// public SharedPositionHistory( + SharedSymbol? sharedSymbol, string symbol, SharedPositionSide side, decimal openPrice, @@ -59,8 +56,8 @@ namespace CryptoExchange.Net.SharedApis decimal quantity, decimal realizedPnl, DateTime timestamp) + : base(sharedSymbol, symbol) { - Symbol = symbol; PositionSide = side; AverageOpenPrice = openPrice; AverageClosePrice = closePrice; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotOrder.cs index 2cb82b1..577232f 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotOrder.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotOrder.cs @@ -5,12 +5,8 @@ namespace CryptoExchange.Net.SharedApis /// /// Spot order info /// - public record SharedSpotOrder + public record SharedSpotOrder : SharedSymbolModel { - /// - /// The symbol the order is on - /// - public string Symbol { get; set; } /// /// The id of the order /// @@ -32,21 +28,13 @@ namespace CryptoExchange.Net.SharedApis /// public SharedTimeInForce? TimeInForce { get; set; } /// - /// Order quantity in base asset + /// Order quantity /// - public decimal? Quantity { get; set; } + public SharedOrderQuantity? OrderQuantity { get; set; } /// - /// Quantity filled in base asset, note that this quantity has not yet included trading fees paid + /// Filled quantity /// - public decimal? QuantityFilled { get; set; } - /// - /// Order quantity in quote asset - /// - public decimal? QuoteQuantity { get; set; } - /// - /// Quantity filled in the quote asset, note that this quantity has not yet included trading fees paid - /// - public decimal? QuoteQuantityFilled { get; set; } + public SharedOrderQuantity? QuantityFilled { get; set; } /// /// Order price /// @@ -80,18 +68,28 @@ namespace CryptoExchange.Net.SharedApis /// public SharedUserTrade? LastTrade { get; set; } + /// + /// Trigger price for a trigger order + /// + public decimal? TriggerPrice { get; set; } + /// + /// Whether or not the is order is a trigger order + /// + public bool IsTriggerOrder { get; set; } + /// /// ctor /// public SharedSpotOrder( + SharedSymbol? sharedSymbol, string symbol, string orderId, SharedOrderType orderType, SharedOrderSide orderSide, SharedOrderStatus orderStatus, DateTime? createTime) + : base(sharedSymbol, symbol) { - Symbol = symbol; OrderId = orderId; OrderType = orderType; Side = orderSide; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotSymbol.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotSymbol.cs index 818c012..71cd41d 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotSymbol.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotSymbol.cs @@ -5,6 +5,10 @@ /// public record SharedSpotSymbol { + /// + /// The trading mode of the symbol + /// + public TradingMode TradingMode { get; set; } /// /// Base asset of the symbol /// @@ -57,8 +61,9 @@ /// /// ctor /// - public SharedSpotSymbol(string baseAsset, string quoteAsset, string symbol, bool trading) + public SharedSpotSymbol(string baseAsset, string quoteAsset, string symbol, bool trading, TradingMode? tradingMode = null) { + TradingMode = tradingMode ?? TradingMode.Spot; BaseAsset = baseAsset; QuoteAsset = quoteAsset; Name = symbol; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTicker.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTicker.cs index fd641a4..c19ee01 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTicker.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTicker.cs @@ -3,12 +3,8 @@ /// /// Ticker info /// - public record SharedSpotTicker + public record SharedSpotTicker: SharedSymbolModel { - /// - /// Symbol name - /// - public string Symbol { get; set; } /// /// Last trade price /// @@ -26,6 +22,10 @@ /// public decimal Volume { get; set; } /// + /// Trade volume in quote asset in the last 24h + /// + public decimal? QuoteVolume { get; set; } + /// /// Change percentage in the last 24h /// public decimal? ChangePercentage { get; set; } @@ -33,9 +33,9 @@ /// /// ctor /// - public SharedSpotTicker(string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) + public SharedSpotTicker(SharedSymbol? sharedSymbol, string symbol, decimal? lastPrice, decimal? highPrice, decimal? lowPrice, decimal volume, decimal? changePercentage) + : base(sharedSymbol, symbol) { - Symbol = symbol; LastPrice = lastPrice; HighPrice = highPrice; LowPrice = lowPrice; diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs new file mode 100644 index 0000000..fedade0 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSpotTriggerOrder.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Trigger order info + /// + public record SharedSpotTriggerOrder : SharedSymbolModel + { + /// + /// The id of the trigger order + /// + public string TriggerOrderId { get; set; } + /// + /// The id of the order that was placed when this order was activated + /// + public string? PlacedOrderId { get; set; } + /// + /// The type of the order + /// + public SharedOrderType OrderType { get; set; } + /// + /// Status of the trigger order + /// + public SharedTriggerOrderStatus Status { get; set; } + /// + /// Time in force for the order + /// + public SharedTimeInForce? TimeInForce { get; set; } + /// + /// Order quantity + /// + public SharedOrderQuantity? OrderQuantity { get; set; } + /// + /// Filled quantity + /// + public SharedOrderQuantity? QuantityFilled { get; set; } + /// + /// Order price + /// + public decimal? OrderPrice { get; set; } + /// + /// Average fill price + /// + public decimal? AveragePrice { get; set; } + /// + /// Trigger order direction + /// + public SharedTriggerOrderDirection OrderDirection { get; set; } + /// + /// Trigger price + /// + public decimal TriggerPrice { get; set; } + /// + /// Asset the fee is in + /// + public string? FeeAsset { get; set; } + /// + /// Fee paid for the order + /// + public decimal? Fee { get; set; } + /// + /// Timestamp the order was created + /// + public DateTime? CreateTime { get; set; } + /// + /// Last update timestamp + /// + public DateTime? UpdateTime { get; set; } + /// + /// Client order id + /// + public string? ClientOrderId { get; set; } + + /// + /// ctor + /// + public SharedSpotTriggerOrder( + SharedSymbol? sharedSymbol, + string symbol, + string triggerOrderId, + SharedOrderType orderType, + SharedTriggerOrderDirection orderDirection, + SharedTriggerOrderStatus triggerStatus, + decimal triggerPrice, + DateTime? createTime) + : base(sharedSymbol, symbol) + { + TriggerOrderId = triggerOrderId; + OrderType = orderType; + OrderDirection = orderDirection; + Status = triggerStatus; + CreateTime = createTime; + TriggerPrice = triggerPrice; + } + } +} \ No newline at end of file diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs new file mode 100644 index 0000000..d15849f --- /dev/null +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedSymbolModel.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Symbol model + /// + public record SharedSymbolModel + { + /// + /// SharedSymbol, only filled when the related GetSpotSymbolsAsync or GetFuturesSymbolsAsync method has been called previously + /// + public SharedSymbol? SharedSymbol { get; set; } + /// + /// Symbol name + /// + public string Symbol { get; set; } + + /// + /// ctor + /// + public SharedSymbolModel(SharedSymbol? sharedSymbol, string symbol) + { + Symbol = symbol; + SharedSymbol = sharedSymbol; + } + } +} diff --git a/CryptoExchange.Net/SharedApis/ResponseModels/SharedUserTrade.cs b/CryptoExchange.Net/SharedApis/ResponseModels/SharedUserTrade.cs index 37c2a1f..1791ce6 100644 --- a/CryptoExchange.Net/SharedApis/ResponseModels/SharedUserTrade.cs +++ b/CryptoExchange.Net/SharedApis/ResponseModels/SharedUserTrade.cs @@ -5,12 +5,8 @@ namespace CryptoExchange.Net.SharedApis /// /// A user trade /// - public record SharedUserTrade + public record SharedUserTrade : SharedSymbolModel { - /// - /// Symbol the trade was on - /// - public string Symbol { get; set; } /// /// The trade id /// @@ -51,7 +47,8 @@ namespace CryptoExchange.Net.SharedApis /// /// ctor /// - public SharedUserTrade(string symbol, string orderId, string id, SharedOrderSide? side, decimal quantity, decimal price, DateTime timestamp) + public SharedUserTrade(SharedSymbol? sharedSymbol, string symbol, string orderId, string id, SharedOrderSide? side, decimal quantity, decimal price, DateTime timestamp) + : base(sharedSymbol, symbol) { Symbol = symbol; OrderId = orderId; diff --git a/CryptoExchange.Net/SharedApis/SharedQuantity.cs b/CryptoExchange.Net/SharedApis/SharedQuantity.cs new file mode 100644 index 0000000..ef1ca59 --- /dev/null +++ b/CryptoExchange.Net/SharedApis/SharedQuantity.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CryptoExchange.Net.SharedApis +{ + /// + /// Quantity reference + /// + public record SharedQuantityReference + { + /// + /// Quantity denoted in the base asset of the symbol + /// + public decimal? QuantityInBaseAsset { get; set; } + /// + /// Quantity denoted in the quote asset of the symbol + /// + public decimal? QuantityInQuoteAsset { get; set; } + /// + /// Quantity denoted in the number of contracts + /// + public decimal? QuantityInContracts { get; set; } + + /// + /// ctor + /// + protected SharedQuantityReference(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity) + { + QuantityInBaseAsset = baseAssetQuantity; + QuantityInQuoteAsset = quoteAssetQuantity; + QuantityInContracts = contractQuantity; + } + } + + /// + /// Quantity for an order + /// + public record SharedQuantity : SharedQuantityReference + { + private SharedQuantity(decimal? baseAssetQuantity, decimal? quoteAssetQuantity, decimal? contractQuantity) + : base(baseAssetQuantity, quoteAssetQuantity, contractQuantity) + { + } + + /// + /// Specify quantity in base asset + /// + public static SharedQuantity Base(decimal quantity) => new SharedQuantity(quantity, null, null); + /// + /// Specify quantity in quote asset + /// + public static SharedQuantity Quote(decimal quantity) => new SharedQuantity(null, quantity, null); + /// + /// Specify quantity in number of contracts + /// + public static SharedQuantity Contracts(decimal quantity) => new SharedQuantity(null, null, quantity); + + /// + /// Get the base asset quantity from a quote quantity using a price + /// + /// Quantity in quote asset to convert + /// Price to use for conversion + /// The max number of decimal places for the result + /// The lot size (step per quantity) for the base asset + public static SharedQuantity BaseFromQuote(decimal quoteQuantity, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) + => new SharedQuantity(ExchangeHelpers.ApplyRules(quoteQuantity / price, decimalPlaces, lotSize), null, null); + /// + /// Get the quote asset quantity from a base quantity using a price + /// + /// Quantity in base asset to convert + /// Price to use for conversion + /// The max number of decimal places for the result + /// The lot size (step per quantity) for the quote asset + public static SharedQuantity QuoteFromBase(decimal baseQuantity, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) + => new SharedQuantity(ExchangeHelpers.ApplyRules(baseQuantity * price, decimalPlaces, lotSize), null, null); + /// + /// Get a quantity in number of contracts from a base asset + /// + /// Quantity in base asset to convert + /// The contract size of a single contract + /// The max number of decimal places for the result + /// The lot size (step per quantity) for the contract + public static SharedQuantity ContractsFromBase(decimal baseQuantity, decimal contractSize, int decimalPlaces = 8, decimal lotSize = 0.00000001m) + => new SharedQuantity(ExchangeHelpers.ApplyRules(baseQuantity / contractSize, decimalPlaces, lotSize), null, null); + /// + /// Get a quantity in number of contracts from a quote asset + /// + /// Quantity in quote asset to convert + /// The contract size of a single contract + /// The price to use for conversion + /// The max number of decimal places for the result + /// The lot size (step per quantity) for the contract + public static SharedQuantity ContractsFromQuote(decimal quoteQuantity, decimal contractSize, decimal price, int decimalPlaces = 8, decimal lotSize = 0.00000001m) + => new SharedQuantity(ExchangeHelpers.ApplyRules(quoteQuantity / price / contractSize, decimalPlaces, lotSize), null, null); + } + + /// + /// Order quantity + /// + public record SharedOrderQuantity : SharedQuantityReference + { + /// + /// ctor + /// + public SharedOrderQuantity(): base(null, null,null) { } + + /// + /// ctor + /// + public SharedOrderQuantity(decimal? baseAssetQuantity = null, decimal? quoteAssetQuantity = null, decimal? contractQuantity = null) + : base(baseAssetQuantity, quoteAssetQuantity, contractQuantity) + { + } + } +} diff --git a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs index 37cd625..8be0316 100644 --- a/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs +++ b/CryptoExchange.Net/Sockets/CryptoExchangeWebSocketClient.cs @@ -203,7 +203,7 @@ namespace CryptoExchange.Net.Sockets socket.Options.CollectHttpResponseDetails = true; #endif #if NET9_0_OR_GREATER - socket.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10); + socket.Options.KeepAliveTimeout = Parameters.KeepAliveTimeout ?? TimeSpan.FromSeconds(10); #endif } catch (PlatformNotSupportedException) @@ -246,7 +246,7 @@ namespace CryptoExchange.Net.Sockets if (_socket.HttpStatusCode == HttpStatusCode.TooManyRequests) { await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - return new CallResult(new ServerRateLimitError(we.Message)); + return new CallResult(new ServerRateLimitError(we.Message, we)); } #else // ClientWebSocket.HttpStatusCode is only available in .NET6+ https://learn.microsoft.com/en-us/dotnet/api/system.net.websockets.clientwebsocket.httpstatuscode?view=net-8.0 @@ -254,16 +254,16 @@ namespace CryptoExchange.Net.Sockets if (we.Message.Contains("429")) { await (OnConnectRateLimited?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); - return new CallResult(new ServerRateLimitError(we.Message)); + return new CallResult(new ServerRateLimitError(we.Message, we)); } #endif } - return new CallResult(new CantConnectError()); + return new CallResult(new CantConnectError(e)); } _logger.SocketConnected(Id, Uri); - return new CallResult(null); + return CallResult.SuccessResult; } /// diff --git a/CryptoExchange.Net/Sockets/Query.cs b/CryptoExchange.Net/Sockets/Query.cs index 397ae43..c024fb3 100644 --- a/CryptoExchange.Net/Sockets/Query.cs +++ b/CryptoExchange.Net/Sockets/Query.cs @@ -184,7 +184,7 @@ namespace CryptoExchange.Net.Sockets { var typedMessage = message.As((TServerResponse)message.Data); if (!ValidateMessage(typedMessage)) - return new CallResult(null); + return CallResult.SuccessResult; CurrentResponses++; if (CurrentResponses == RequiredResponses) diff --git a/CryptoExchange.Net/Sockets/SocketConnection.cs b/CryptoExchange.Net/Sockets/SocketConnection.cs index 8e32437..d22928e 100644 --- a/CryptoExchange.Net/Sockets/SocketConnection.cs +++ b/CryptoExchange.Net/Sockets/SocketConnection.cs @@ -195,12 +195,12 @@ namespace CryptoExchange.Net.Sockets /// /// Current subscription topics on this connection /// - public IEnumerable Topics + public string[] Topics { get { lock (_listenersLock) - return _listeners.OfType().Select(x => x.Topic).Where(t => t != null).ToList()!; + return _listeners.OfType().Select(x => x.Topic).Where(t => t != null).ToArray()!; } } @@ -535,7 +535,7 @@ namespace CryptoExchange.Net.Sockets var desResult = processor.Deserialize(_accessor, messageType); if (!desResult) { - _logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString()); + _logger.FailedToDeserializeMessage(SocketId, desResult.Error?.ToString(), desResult.Error?.Exception); continue; } deserialized = desResult.Data; @@ -553,7 +553,7 @@ namespace CryptoExchange.Net.Sockets } catch (Exception ex) { - _logger.UserMessageProcessingFailed(SocketId, ex.ToLogString(), ex); + _logger.UserMessageProcessingFailed(SocketId, ex.Message, ex); if (processor is Subscription subscription) subscription.InvokeExceptionHandler(ex); } @@ -856,11 +856,11 @@ namespace CryptoExchange.Net.Sockets if (!_socket.Send(requestId, data, weight)) return new CallResult(new WebError("Failed to send message, connection not open")); - return new CallResult(null); + return CallResult.SuccessResult; } catch(Exception ex) { - return new CallResult(new WebError("Failed to send message: " + ex.Message)); + return new CallResult(new WebError("Failed to send message: " + ex.Message, exception: ex)); } } @@ -879,7 +879,7 @@ namespace CryptoExchange.Net.Sockets // No need to resubscribe anything _logger.NothingToResubscribeCloseConnection(SocketId); _ = _socket.CloseAsync(); - return new CallResult(null); + return CallResult.SuccessResult; } } @@ -966,7 +966,7 @@ namespace CryptoExchange.Net.Sockets return new CallResult(new WebError("Socket not connected")); _logger.AllSubscriptionResubscribed(SocketId); - return new CallResult(null); + return CallResult.SuccessResult; } internal async Task UnsubscribeAsync(Subscription subscription) @@ -982,11 +982,11 @@ namespace CryptoExchange.Net.Sockets internal async Task ResubscribeAsync(Subscription subscription) { if (!_socket.IsOpen) - return new CallResult(new UnknownError("Socket is not connected")); + return new CallResult(new WebError("Socket is not connected")); var subQuery = subscription.GetSubQuery(this); if (subQuery == null) - return new CallResult(null); + return CallResult.SuccessResult; var result = await SendAndWaitQueryAsync(subQuery).ConfigureAwait(false); subscription.HandleSubQueryResponse(subQuery.Response!); @@ -1036,7 +1036,7 @@ namespace CryptoExchange.Net.Sockets } catch (Exception ex) { - _logger.PeriodicSendFailed(SocketId, identifier, ex.ToLogString(), ex); + _logger.PeriodicSendFailed(SocketId, identifier, ex.Message, ex); } } }); diff --git a/CryptoExchange.Net/Testing/Comparers/JsonNetComparer.cs b/CryptoExchange.Net/Testing/Comparers/JsonNetComparer.cs deleted file mode 100644 index 3893b33..0000000 --- a/CryptoExchange.Net/Testing/Comparers/JsonNetComparer.cs +++ /dev/null @@ -1,386 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Reflection; -using CryptoExchange.Net.Converters; -using CryptoExchange.Net.Converters.JsonNet; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace CryptoExchange.Net.Testing.Comparers -{ - internal class JsonNetComparer - { - internal static void CompareData( - string method, - object resultData, - string json, - string? nestedJsonProperty, - List? ignoreProperties = null, - bool userSingleArrayItem = false) - { - var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyAttribute?)p.GetCustomAttributes(typeof(JsonPropertyAttribute), true).SingleOrDefault())); - var jsonObject = JToken.Parse(json); - if (nestedJsonProperty != null) - { - var nested = nestedJsonProperty.Split('.'); - foreach (var nest in nested) - { - if (int.TryParse(nest, out var index)) - jsonObject = jsonObject![index]; - else - jsonObject = jsonObject![nest]; - } - } - - if (userSingleArrayItem) - jsonObject = ((JArray)jsonObject!)[0]; - - if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary))) - { - var dict = (IDictionary)resultData; - var jObj = (JObject)jsonObject!; - var properties = jObj.Properties(); - foreach (var dictProp in properties) - { - if (!dict.Contains(dictProp.Name)) - throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}"); - - if (dictProp.Value.Type == JTokenType.Object) - { - // TODO Some additional checking for objects - foreach (var prop in ((JObject)dictProp.Value).Properties()) - CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!); - } - else - { - if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null) - // Property value not correct - throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}"); - } - } - } - else if (jsonObject!.Type == JTokenType.Array) - { - var jObjs = (JArray)jsonObject; - if (resultData is IEnumerable list) - { - var enumerator = list.GetEnumerator(); - foreach (var jObj in jObjs) - { - enumerator.MoveNext(); - if (jObj.Type == JTokenType.Object) - { - foreach (var subProp in ((JObject)jObj).Properties()) - { - if (ignoreProperties?.Contains(subProp.Name) == true) - continue; - CheckObject(method, subProp, enumerator.Current, ignoreProperties!); - } - } - else if (jObj.Type == JTokenType.Array) - { - var resultObj = enumerator.Current; - if (resultObj is string) - // string list - continue; - - var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); - var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); - var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) - // Not array converter? - continue; - - int i = 0; - foreach (var item in jObj.Children()) - { - var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; - if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); - i++; - } - } - else - { - var value = enumerator.Current; - if (value == default && ((JValue)jObj).Type != JTokenType.Null) - throw new Exception($"{method}: Array has no value while input json array has value {jObj}"); - } - } - } - else - { - var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); - int i = 0; - foreach (var item in jObjs.Children()) - { - var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; - if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); - i++; - } - } - } - else - { - foreach (var item in jsonObject) - { - if (item is JProperty prop) - { - if (ignoreProperties?.Contains(prop.Name) == true) - continue; - - CheckObject(method, prop, resultData, ignoreProperties); - } - } - } - - Debug.WriteLine($"Successfully validated {method}"); - } - - private static void CheckObject(string method, JProperty prop, object obj, List? ignoreProperties) - { - var resultProperties = obj.GetType().GetProperties().Select(p => (p, ((JsonPropertyAttribute?)p.GetCustomAttributes(typeof(JsonPropertyAttribute), true).SingleOrDefault())?.PropertyName)); - - // Property has a value - var property = resultProperties.SingleOrDefault(p => p.PropertyName == prop.Name).p; - property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p; - property ??= resultProperties.SingleOrDefault(p => p.p.Name.Equals(prop.Name, StringComparison.InvariantCultureIgnoreCase)).p; - - if (property is null) - // Property not found - throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`"); - - var propertyValue = property.GetValue(obj); - if (property.GetCustomAttribute(true)?.ItemConverterType == null) - CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties); - } - - private static void CheckPropertyValue(string method, JToken propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List? ignoreProperties = null) - { - if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString())) - { - if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1")) - return; - - // Property value not correct - if (propValue.ToString() != "0") - throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); - } - - if ((propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString()))) || propValue.ToString() == "0") - return; - - if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary))) - { - var dict = (IDictionary)propertyValue; - var jObj = (JObject)propValue; - var properties = jObj.Properties(); - foreach (var dictProp in properties) - { - if (!dict.Contains(dictProp.Name)) - throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); - - if (dictProp.Value.Type == JTokenType.Object) - { - CheckObject(method, dictProp, dict[dictProp.Name]!, ignoreProperties); - } - else - { - if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null) - // Property value not correct - throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for"); - } - } - } - else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable)) - && propertyValue.GetType() != typeof(string)) - { - var jObjs = (JArray)propValue; - var list = (IEnumerable)propertyValue; - var enumerator = list.GetEnumerator(); - foreach (JToken jtoken in jObjs) - { - enumerator.MoveNext(); - var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); - if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter)) - // Custom converter for the type, skip - continue; - - if (jtoken.Type == JTokenType.Object) - { - foreach (var subProp in ((JObject)jtoken).Properties()) - { - if (ignoreProperties?.Contains(subProp.Name) == true) - continue; - - CheckObject(method, subProp, enumerator.Current, ignoreProperties); - } - } - else if (jtoken.Type == JTokenType.Array) - { - var resultObj = enumerator.Current; - var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); - var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); - var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) - // Not array converter? - continue; - - int i = 0; - foreach (var item in jtoken.Children()) - { - var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; - if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); - - i++; - } - } - else - { - var value = enumerator.Current; - if (value == default && ((JValue)jtoken).Type != JTokenType.Null) - throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jtoken}"); - - CheckValues(method, propertyName!, propertyType, (JValue)jtoken, value!); - } - } - } - else - { - if (propValue.Type == JTokenType.Object) - { - foreach (var item in propValue) - { - if (item is JProperty prop) - { - if (ignoreProperties?.Contains(prop.Name) == true) - continue; - - CheckObject(method, prop, propertyValue, ignoreProperties); - } - } - } - else if(propValue.Type == JTokenType.Array) - { - var jObjs = (JArray)propValue; - if (propertyValue is IEnumerable list) - { - var enumerator = list.GetEnumerator(); - foreach (var jObj in jObjs) - { - if (!enumerator.MoveNext()) - { - } - - if (jObj.Type == JTokenType.Object) - { - foreach (var subProp in ((JObject)jObj).Properties()) - { - if (ignoreProperties?.Contains(subProp.Name) == true) - continue; - CheckObject(method, subProp, enumerator.Current, ignoreProperties!); - } - } - else if (jObj.Type == JTokenType.Array) - { - var resultObj = enumerator.Current; - var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); - var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); - var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) - // Not array converter? - continue; - - int i = 0; - foreach (var item in jObj.Values()) - { - var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p; - if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); - i++; - } - } - else - { - var value = enumerator.Current; - if (value == default && ((JValue)jObj).Type != JTokenType.Null) - throw new Exception($"{method}: Array has no value while input json array has value {jObj}"); - } - } - } - else - { - var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); - int i = 0; - foreach (var item in jObjs.Children()) - { - var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; - if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); - i++; - } - } - } - else - { - CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue); - } - } - } - - private static void CheckValues(string method, string property, Type propertyType, JValue jsonValue, object objectValue) - { - if (jsonValue.Type == JTokenType.String) - { - if (objectValue is decimal dec) - { - if (jsonValue.Value() != dec) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {dec}"); - } - else if (objectValue is DateTime time) - { - if (time != DateTimeConverter.ParseFromString(jsonValue.Value()!)) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {time}"); - } - else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) - { - // TODO enum comparing - } - else if (!jsonValue.Value()!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase)) - { - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {objectValue}"); - } - } - else if (jsonValue.Type == JTokenType.Integer) - { - if (objectValue is DateTime time) - { - if (time != DateTimeConverter.ParseFromLong(jsonValue.Value()!)) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {time}"); - } - else if (propertyType.IsEnum) - { - // TODO enum comparing - } - else if (jsonValue.Value() != Convert.ToInt64(objectValue)) - { - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {Convert.ToInt64(objectValue)}"); - } - } - else if (jsonValue.Type == JTokenType.Boolean) - { - if (objectValue is bool boolVal && jsonValue.Value() != boolVal) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {(bool)objectValue}"); - - if (jsonValue.Value() != bool.Parse(objectValue.ToString()!)) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {(bool)objectValue}"); - } - } - } -} diff --git a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs index 3adbdb3..b11fb01 100644 --- a/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs +++ b/CryptoExchange.Net/Testing/Comparers/SystemTextJsonComparer.cs @@ -4,10 +4,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Text.Json.Serialization; using CryptoExchange.Net.Converters; using CryptoExchange.Net.Converters.SystemTextJson; -using Newtonsoft.Json.Linq; namespace CryptoExchange.Net.Testing.Comparers { @@ -15,14 +16,13 @@ namespace CryptoExchange.Net.Testing.Comparers { internal static void CompareData( string method, - object resultData, + object? resultData, string json, string? nestedJsonProperty, List? ignoreProperties = null, bool userSingleArrayItem = false) { - var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())); - var jsonObject = JToken.Parse(json); + var jsonObject = JsonDocument.Parse(json).RootElement; if (nestedJsonProperty != null) { var nested = nestedJsonProperty.Split('.'); @@ -31,32 +31,43 @@ namespace CryptoExchange.Net.Testing.Comparers if (int.TryParse(nest, out var index)) jsonObject = jsonObject![index]; else - jsonObject = jsonObject![nest]; + jsonObject = jsonObject!.GetProperty(nest); } } if (userSingleArrayItem) - jsonObject = ((JArray)jsonObject!)[0]; + jsonObject = jsonObject[0]; + + + if (resultData == null) + { + if (jsonObject.ValueKind == JsonValueKind.Null) + return; + + if (jsonObject.ValueKind == JsonValueKind.Object && jsonObject.GetPropertyCount() == 0) + return; + + throw new Exception("ResultData null"); + } if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary))) { var dict = (IDictionary)resultData; - var jObj = (JObject)jsonObject!; - var properties = jObj.Properties(); - foreach (var dictProp in properties) + var jObj = jsonObject!; + foreach (var dictProp in jObj.EnumerateObject()) { if (!dict.Contains(dictProp.Name)) throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}"); - if (dictProp.Value.Type == JTokenType.Object) + if (dictProp.Value.ValueKind == JsonValueKind.Object) { // TODO Some additional checking for objects - foreach (var prop in ((JObject)dictProp.Value).Properties()) + foreach (var prop in dictProp.Value.EnumerateObject()) CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!); } else { - if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null) + if (dict[dictProp.Name] == default && dictProp.Value.ValueKind != JsonValueKind.Null) { if (dictProp.Value.ToString() == "") continue; @@ -67,28 +78,27 @@ namespace CryptoExchange.Net.Testing.Comparers } } } - else if (jsonObject!.Type == JTokenType.Array) + else if (jsonObject!.ValueKind == JsonValueKind.Array) { - var jArray = (JArray)jsonObject; if (resultData is IEnumerable list) { var enumerator = list.GetEnumerator(); - foreach (var jObj in jArray) + foreach (var jObj in jsonObject.EnumerateArray()) { if (!enumerator.MoveNext()) { } - if (jObj.Type == JTokenType.Object) + if (jObj.ValueKind == JsonValueKind.Object) { - foreach (var subProp in ((JObject)jObj).Properties()) + foreach (var subProp in jObj.EnumerateObject()) { if (ignoreProperties?.Contains(subProp.Name) == true) continue; CheckObject(method, subProp, enumerator.Current, ignoreProperties!); } } - else if (jObj.Type == JTokenType.Array) + else if (jObj.ValueKind == JsonValueKind.Array) { var resultObj = enumerator.Current; if (resultObj is string) @@ -98,23 +108,23 @@ namespace CryptoExchange.Net.Testing.Comparers var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) + if (jsonConverter != typeof(ArrayConverter<>)) // Not array converter? continue; int i = 0; - foreach (var item in jObj.Children()) + foreach (var item in jObj.EnumerateObject()) { var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; if (arrayProp != null) - CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); + CheckPropertyValue(method, item.Value, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); i++; } } else { var value = enumerator.Current; - if (value == default && ((JValue)jObj).Type != JTokenType.Null) + if (value == default && jObj.ValueKind != JsonValueKind.Null) throw new Exception($"{method}: Array has no value while input json array has value {jObj}"); } } @@ -123,33 +133,37 @@ namespace CryptoExchange.Net.Testing.Comparers { var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); int i = 0; - foreach (var item in jArray.Children()) + foreach (var item in jsonObject.EnumerateArray()) { - var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p; + var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; if (arrayProp != null) CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!); i++; } } } - else + else if (jsonObject.ValueKind == JsonValueKind.Object) { - foreach (var item in jsonObject) + foreach (var item in jsonObject.EnumerateObject()) { - if (item is JProperty prop) - { - if (ignoreProperties?.Contains(prop.Name) == true) + //if (item is JProperty prop) + //{ + if (ignoreProperties?.Contains(item.Name) == true) continue; - CheckObject(method, prop, resultData, ignoreProperties); - } + CheckObject(method, item, resultData, ignoreProperties); + //} } } + else + { + //? + } Debug.WriteLine($"Successfully validated {method}"); } - private static void CheckObject(string method, JProperty prop, object obj, List? ignoreProperties) + private static void CheckObject(string method, JsonProperty prop, object obj, List? ignoreProperties) { var publicProperties = obj.GetType().GetProperties( System.Reflection.BindingFlags.Public @@ -184,9 +198,9 @@ namespace CryptoExchange.Net.Testing.Comparers CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties); } - private static void CheckPropertyValue(string method, JToken propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List? ignoreProperties = null) + private static void CheckPropertyValue(string method, JsonElement propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List? ignoreProperties = null) { - if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString())) + if (propertyValue == default && propValue.ValueKind != JsonValueKind.Null && !string.IsNullOrEmpty(propValue.ToString())) { if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1" || propValue.ToString() == "01/01/0001 00:00:00")) return; @@ -196,26 +210,24 @@ namespace CryptoExchange.Net.Testing.Comparers throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); } - if ((propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString()))) || propValue.ToString() == "0") + if ((propertyValue == default && (propValue.ValueKind == JsonValueKind.Null || string.IsNullOrEmpty(propValue.ToString()))) || propValue.ToString() == "0") return; if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary))) { var dict = (IDictionary)propertyValue; - var jObj = (JObject)propValue; - var properties = jObj.Properties(); - foreach (var dictProp in properties) + foreach (var dictProp in propValue.EnumerateObject()) { if (!dict.Contains(dictProp.Name)) throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}"); - if (dictProp.Value.Type == JTokenType.Object) + if (dictProp.Value.ValueKind == JsonValueKind.Object) { CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name]!.GetType(), null, null, ignoreProperties); } else { - if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null) + if (dict[dictProp.Name] == default && dictProp.Value.ValueKind != JsonValueKind.Null) // Property value not correct throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for"); } @@ -224,26 +236,25 @@ namespace CryptoExchange.Net.Testing.Comparers else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable)) && propertyValue.GetType() != typeof(string)) { - if (propValue.Type != JTokenType.Array) + if (propValue.ValueKind != JsonValueKind.Array) return; - var jArray = (JArray)propValue; var list = (IEnumerable)propertyValue; var enumerator = list.GetEnumerator(); - foreach (JToken jToken in jArray) + foreach (var jToken in propValue.EnumerateArray()) { var moved = enumerator.MoveNext(); if (!moved) throw new Exception("Enumeration not moved; incorrect amount of results?"); var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); - if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter)) + if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter<>)) // Custom converter for the type, skip continue; - if (jToken.Type == JTokenType.Object) + if (jToken.ValueKind == JsonValueKind.Object) { - foreach (var subProp in ((JObject)jToken).Properties()) + foreach (var subProp in jToken.EnumerateObject()) { if (ignoreProperties?.Contains(subProp.Name) == true) continue; @@ -251,18 +262,18 @@ namespace CryptoExchange.Net.Testing.Comparers CheckObject(method, subProp, enumerator.Current, ignoreProperties); } } - else if (jToken.Type == JTokenType.Array) + else if (jToken.ValueKind == JsonValueKind.Array) { var resultObj = enumerator.Current; var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) + if (jsonConverter != typeof(ArrayConverter<>)) // Not array converter? continue; int i = 0; - foreach (var item in jToken.Children()) + foreach (var item in jToken.EnumerateArray()) { var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; if (arrayProp != null) @@ -274,61 +285,60 @@ namespace CryptoExchange.Net.Testing.Comparers else { var value = enumerator.Current; - if (value == default && ((JValue)jToken).Type != JTokenType.Null) + if (value == default && jToken.ValueKind != JsonValueKind.Null) throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jToken}"); - CheckValues(method, propertyName!, propertyType, (JValue)jToken, value!); + CheckValues(method, propertyName!, propertyType, jToken, value!); } } } else { - if (propValue.Type == JTokenType.Object) + if (propValue.ValueKind == JsonValueKind.Object) { - foreach (var item in propValue) + foreach (var item in propValue.EnumerateObject()) { - if (item is JProperty prop) - { - if (ignoreProperties?.Contains(prop.Name) == true) + //if (item is JProperty prop) + //{ + if (ignoreProperties?.Contains(item.Name) == true) continue; - CheckObject(method, prop, propertyValue, ignoreProperties); - } + CheckObject(method, item, propertyValue, ignoreProperties); + //} } } - else if (propValue.Type == JTokenType.Array) + else if (propValue.ValueKind == JsonValueKind.Array) { - var jArray = (JArray)propValue; if (propertyValue is IEnumerable list) { var enumerator = list.GetEnumerator(); - foreach (var jObj in jArray) + foreach (var jObj in propValue.EnumerateArray()) { if (!enumerator.MoveNext()) { } - if (jObj.Type == JTokenType.Object) + if (jObj.ValueKind == JsonValueKind.Object) { - foreach (var subProp in ((JObject)jObj).Properties()) + foreach (var subProp in jObj.EnumerateObject()) { if (ignoreProperties?.Contains(subProp.Name) == true) continue; CheckObject(method, subProp, enumerator.Current, ignoreProperties!); } } - else if (jObj.Type == JTokenType.Array) + else if (jObj.ValueKind == JsonValueKind.Array) { var resultObj = enumerator.Current; var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault(); var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType; - if (jsonConverter != typeof(ArrayConverter)) + if (jsonConverter != typeof(ArrayConverter<>)) // Not array converter? continue; int i = 0; - foreach (var item in jObj.Values()) + foreach (var item in jObj.EnumerateArray()) { var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p; if (arrayProp != null) @@ -339,7 +349,7 @@ namespace CryptoExchange.Net.Testing.Comparers else { var value = enumerator.Current; - if (value == default && ((JValue)jObj).Type != JTokenType.Null) + if (value == default && jObj.ValueKind != JsonValueKind.Null) throw new Exception($"{method}: Array has no value while input json array has value {jObj}"); } } @@ -348,7 +358,7 @@ namespace CryptoExchange.Net.Testing.Comparers { var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast().SingleOrDefault())); int i = 0; - foreach (var item in jArray.Children()) + foreach (var item in propValue.EnumerateArray()) { var arrayProp = resultProps.Where(p => p.Item2 != null).FirstOrDefault(p => p.Item2!.Index == i).p; if (arrayProp != null) @@ -359,63 +369,78 @@ namespace CryptoExchange.Net.Testing.Comparers } else { - CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue); + CheckValues(method, propertyName!, propertyType, propValue, propertyValue); } } } - private static void CheckValues(string method, string property, Type propertyType, JValue jsonValue, object objectValue) + private static void CheckValues(string method, string property, Type propertyType, JsonElement jsonValue, object objectValue) { - if (jsonValue.Type == JTokenType.String) + if (jsonValue.ValueKind == JsonValueKind.String) { + var stringValue = jsonValue.GetString(); if (objectValue is decimal dec) { - if (jsonValue.Value() != dec) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {dec}"); + if (decimal.Parse(stringValue!, CultureInfo.InvariantCulture) != dec) + throw new Exception($"{method}: {property} not equal: {stringValue} vs {dec}"); } else if (objectValue is DateTime time) { - var jsonStr = jsonValue.Value()!; - if (!string.IsNullOrEmpty(jsonStr) && time != DateTimeConverter.ParseFromString(jsonStr)) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {time}"); + if (!string.IsNullOrEmpty(stringValue) && time != DateTimeConverter.ParseFromString(stringValue!)) + throw new Exception($"{method}: {property} not equal: {stringValue} vs {time}"); } else if (objectValue is bool bl) { - var jsonStr = jsonValue.Value(); - if (bl && (jsonStr != "1" && jsonStr != "true" && jsonStr != "True")) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {bl}"); - if (!bl && (jsonStr != "0" && jsonStr != "-1" && jsonStr != "false" && jsonStr != "False")) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {bl}"); + if (bl && (stringValue != "1" && stringValue != "true" && stringValue != "True")) + throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}"); + if (!bl && (stringValue != "0" && stringValue != "-1" && stringValue != "false" && stringValue != "False")) + throw new Exception($"{method}: {property} not equal: {stringValue} vs {bl}"); } else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) { // TODO enum comparing } - else if (!jsonValue.Value()!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase)) + else if (!stringValue!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase)) { - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {objectValue}"); + throw new Exception($"{method}: {property} not equal: {stringValue} vs {objectValue}"); } } - else if (jsonValue.Type == JTokenType.Integer) + else if (jsonValue.ValueKind == JsonValueKind.Number) { + var value = jsonValue.GetDecimal(); if (objectValue is DateTime time) { - if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value()!)) - throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble(jsonValue.Value()!)} vs {time}"); + if (time != DateTimeConverter.ParseFromDouble((double)value)) + throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble((double)value!)} vs {time}"); } else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true) { // TODO enum comparing } - else if (jsonValue.Value() != Convert.ToInt64(objectValue)) + else if(objectValue is decimal dec) { - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {Convert.ToInt64(objectValue)}"); + if (dec != value) + throw new Exception($"{method}: {property} not equal: {dec} vs {value}"); + } + else if (objectValue is double dbl) + { + if ((decimal)dbl != value) + throw new Exception($"{method}: {property} not equal: {dbl} vs {value}"); + } + else if(objectValue is string objStr) + { + if (objStr != value.ToString()) + throw new Exception($"{method}: {property} not equal: {value} vs {objStr}"); + } + else if (value != Convert.ToInt64(objectValue, CultureInfo.InvariantCulture)) + { + throw new Exception($"{method}: {property} not equal: {value} vs {Convert.ToInt64(objectValue)}"); } } - else if (jsonValue.Type == JTokenType.Boolean) + else if (jsonValue.ValueKind == JsonValueKind.True || jsonValue.ValueKind == JsonValueKind.False) { - if (jsonValue.Value() != (bool)objectValue) - throw new Exception($"{method}: {property} not equal: {jsonValue.Value()} vs {(bool)objectValue}"); + if (jsonValue.GetBoolean() != (bool)objectValue) + throw new Exception($"{method}: {property} not equal: {jsonValue.GetBoolean()} vs {(bool)objectValue}"); } } } diff --git a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs index 5f4d9b8..dc6ba5a 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestRequest.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestRequest.cs @@ -10,7 +10,7 @@ namespace CryptoExchange.Net.Testing.Implementations { internal class TestRequest : IRequest { - private readonly Dictionary> _headers = new Dictionary>(); + private readonly List> _headers = new(); private readonly TestResponse _response; public string Accept { set { } } @@ -32,10 +32,10 @@ namespace CryptoExchange.Net.Testing.Implementations public void AddHeader(string key, string value) { - _headers.Add(key, new[] { value }); + _headers.Add(new KeyValuePair(key, new[] { value })); } - public Dictionary> GetHeaders() => _headers; + public KeyValuePair[] GetHeaders() => _headers.ToArray(); public Task GetResponseAsync(CancellationToken cancellationToken) => Task.FromResult(_response); diff --git a/CryptoExchange.Net/Testing/Implementations/TestResponse.cs b/CryptoExchange.Net/Testing/Implementations/TestResponse.cs index 3cb32a0..53d59e4 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestResponse.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestResponse.cs @@ -16,7 +16,7 @@ namespace CryptoExchange.Net.Testing.Implementations public long? ContentLength { get; } - public IEnumerable>> ResponseHeaders { get; } = new Dictionary>(); + public KeyValuePair[] ResponseHeaders { get; } = new KeyValuePair[0]; public TestResponse(HttpStatusCode code, Stream response) { diff --git a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs index bec8b8d..dde591d 100644 --- a/CryptoExchange.Net/Testing/Implementations/TestSocket.cs +++ b/CryptoExchange.Net/Testing/Implementations/TestSocket.cs @@ -1,10 +1,10 @@ using System; using System.Net.WebSockets; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Objects; -using Newtonsoft.Json; namespace CryptoExchange.Net.Testing.Implementations { @@ -87,7 +87,7 @@ namespace CryptoExchange.Net.Testing.Implementations public void InvokeMessage(T data) { - OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data)))).Wait(); + OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data)))).Wait(); } public Task ReconnectAsync() => throw new NotImplementedException(); diff --git a/CryptoExchange.Net/Testing/RestIntegrationTest.cs b/CryptoExchange.Net/Testing/RestIntegrationTest.cs index f045cee..18b359b 100644 --- a/CryptoExchange.Net/Testing/RestIntegrationTest.cs +++ b/CryptoExchange.Net/Testing/RestIntegrationTest.cs @@ -1,4 +1,5 @@ -using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; using Microsoft.Extensions.Logging; using System; using System.Diagnostics; @@ -96,5 +97,35 @@ namespace CryptoExchange.Net.Testing Debug.WriteLine($"{expressionBody.Method.Name} {result}"); } + + /// + /// Start an order book implementation and expect it to sync and produce an update + /// + public async Task TestOrderBook(ISymbolOrderBook book) + { + if (!ShouldRun()) + return; + + var bookHasChanged = false; + book.OnStatusChange += (_, news) => + { + if (news == OrderBookStatus.Reconnecting) + throw new Exception($"Book reconnecting"); + }; + book.OnOrderBookUpdate += (change) => + { + bookHasChanged = true; + }; + + var result = await book.StartAsync().ConfigureAwait(false); + if (!result) + throw new Exception($"Book failed to start: " + result.Error); + + await Task.Delay(5000).ConfigureAwait(false); + await book.StopAsync().ConfigureAwait(false); + + if (!bookHasChanged) + throw new Exception($"Expected book to have changed at least once"); + } } } diff --git a/CryptoExchange.Net/Testing/RestRequestValidator.cs b/CryptoExchange.Net/Testing/RestRequestValidator.cs index 701bb54..1bc0bea 100644 --- a/CryptoExchange.Net/Testing/RestRequestValidator.cs +++ b/CryptoExchange.Net/Testing/RestRequestValidator.cs @@ -22,7 +22,6 @@ namespace CryptoExchange.Net.Testing private readonly string _folder; private readonly string _baseAddress; private readonly string? _nestedPropertyForCompare; - private readonly bool _stjCompare; /// /// ctor @@ -32,15 +31,13 @@ namespace CryptoExchange.Net.Testing /// The base address that is expected /// Func for checking if the request is authenticated /// Property to use for compare - /// Use System.Text.Json for comparing - public RestRequestValidator(TClient client, string folder, string baseAddress, Func isAuthenticated, string? nestedPropertyForCompare = null, bool stjCompare = true) + public RestRequestValidator(TClient client, string folder, string baseAddress, Func isAuthenticated, string? nestedPropertyForCompare = null) { _client = client; _folder = folder; _baseAddress = baseAddress; _nestedPropertyForCompare = nestedPropertyForCompare; _isAuthenticated = isAuthenticated; - _stjCompare = stjCompare; } /// @@ -61,7 +58,7 @@ namespace CryptoExchange.Net.Testing string? nestedJsonProperty = null, List? ignoreProperties = null, bool useSingleArrayItem = false, - bool skipResponseValidation = false) + bool skipResponseValidation = false) => ValidateAsync(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation); /// @@ -127,10 +124,7 @@ namespace CryptoExchange.Net.Testing { // Check response data object responseData = (TActualResponse)result.Data!; - if (_stjCompare == true) - SystemTextJsonComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); - else - JsonNetComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); + SystemTextJsonComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); } Trace.Listeners.Remove(listener); diff --git a/CryptoExchange.Net/Testing/SocketIntegrationTest.cs b/CryptoExchange.Net/Testing/SocketIntegrationTest.cs new file mode 100644 index 0000000..d00ed06 --- /dev/null +++ b/CryptoExchange.Net/Testing/SocketIntegrationTest.cs @@ -0,0 +1,118 @@ +using CryptoExchange.Net.Interfaces; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using Microsoft.Extensions.Logging; +using System; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Testing +{ + /// + /// Base class for executing websocket API integration tests + /// + /// Client type + public abstract class SocketIntegrationTest + { + /// + /// Get a client instance + /// + /// + /// + public abstract TClient GetClient(ILoggerFactory loggerFactory); + + /// + /// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution. + /// + public virtual bool Run { get; set; } + + /// + /// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation. + /// + public bool Authenticated { get; set; } + + /// + /// Create a client + /// + /// + protected TClient CreateClient() + { + var fact = new LoggerFactory(); + fact.AddProvider(new TraceLoggerProvider()); + return GetClient(fact); + } + + /// + /// Check if integration tests should be executed + /// + /// + protected bool ShouldRun() + { + var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION"); + if (!Run && integrationTests != "1") + return false; + + return true; + } + + /// + /// Execute a REST endpoint call and check for any errors or warnings. + /// + /// Type of the update + /// The call expression + /// Whether an update is expected + /// Whether this is an authenticated request + public async Task RunAndCheckUpdate(Expression>, Task>>> expression, bool expectUpdate, bool authRequest) + { + if (!ShouldRun()) + return; + + var client = CreateClient(); + + var expressionBody = (MethodCallExpression)expression.Body; + if (authRequest && !Authenticated) + { + Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated"); + return; + } + + var listener = new EnumValueTraceListener(); + Trace.Listeners.Add(listener); + + var evnt = new ManualResetEvent(!expectUpdate); + DataEvent? receivedUpdate = null; + var updateHandler = (DataEvent update) => + { + receivedUpdate = update; + evnt.Set(); + }; + + CallResult result; + try + { + result = await expression.Compile().Invoke(client, updateHandler).ConfigureAwait(false); + } + catch (Exception ex) + { + throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString()); + } + finally + { + Trace.Listeners.Remove(listener); + } + + if (!result.Success) + throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error); + + evnt.WaitOne(TimeSpan.FromSeconds(10)); + + if (expectUpdate && receivedUpdate == null) + throw new Exception($"Method {expressionBody.Method.Name} has not received an update"); + + await result.Data.CloseAsync().ConfigureAwait(false); + Debug.WriteLine($"{expressionBody.Method.Name} {result}"); + } + } +} diff --git a/CryptoExchange.Net/Testing/SocketRequestValidator.cs b/CryptoExchange.Net/Testing/SocketRequestValidator.cs new file mode 100644 index 0000000..24446cd --- /dev/null +++ b/CryptoExchange.Net/Testing/SocketRequestValidator.cs @@ -0,0 +1,186 @@ +using CryptoExchange.Net.Clients; +using CryptoExchange.Net.Objects; +using CryptoExchange.Net.Objects.Sockets; +using CryptoExchange.Net.Testing.Comparers; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace CryptoExchange.Net.Testing +{ + /// + /// Validator for websocket subscriptions, checking expected requests and responses and comparing update models + /// + /// + public class SocketRequestValidator where TClient : BaseSocketClient + { + private readonly string _baseAddress = "wss://localhost"; + private readonly string _folder; + private readonly string? _nestedPropertyForCompare; + + /// + /// ctor + /// + /// Folder for json test values + /// Property to use for compare + public SocketRequestValidator(string folder, string? nestedPropertyForCompare = null) + { + _folder = folder; + _nestedPropertyForCompare = nestedPropertyForCompare; + } + + /// + /// Validate a subscription + /// + /// Expected response type + /// Client to test + /// Subscription method invocation + /// Method name for looking up json test values + /// Chose nested property to use for comparing + /// Use nested json property for compare + /// Ignore certain properties + /// Use the first item of an array update + /// Whether to skip the response model validation + /// + /// + public async Task ValidateAsync( + TClient client, + Func>> methodInvoke, + string name, + Func? responseMapper = null, + string? nestedJsonProperty = null, + List? ignoreProperties = null, + bool useSingleArrayItem = false, + bool skipResponseValidation = false) + { + var listener = new EnumValueTraceListener(); + Trace.Listeners.Add(listener); + + var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName; + FileStream file ; + try + { + file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt")); + } + catch (FileNotFoundException) + { + throw new Exception("Response file not found"); + } + + var buffer = new byte[file.Length]; + await file.ReadAsync(buffer, 0, (int)file.Length).ConfigureAwait(false); + file.Close(); + + var data = Encoding.UTF8.GetString(buffer); + using var reader = new StringReader(data); + + var socket = TestHelpers.ConfigureSocketClient(client, _baseAddress); + + var waiter = new AutoResetEvent(false); + string? lastMessage = null; + socket.OnMessageSend += (x) => + { + lastMessage = x; + waiter.Set(); + }; + + // Invoke subscription method + var task = methodInvoke(client); + + var replaceValues = new Dictionary(); + while (true) + { + var line = reader.ReadLine(); + if (line == null) + break; + + if (line.StartsWith("> ")) + { + // Expect a message from client to server + waiter.WaitOne(TimeSpan.FromSeconds(5)); + + if (lastMessage == null) + throw new Exception($"{name} expected {line} to be send to server but did not receive anything"); + + var lastMessageJson = JsonDocument.Parse(lastMessage).RootElement; + var expectedJson = JsonDocument.Parse(line.Substring(2)).RootElement; + if (expectedJson.ValueKind == JsonValueKind.Object) + { + foreach (var item in expectedJson.EnumerateObject()) + { + if (item.Value.ValueKind == JsonValueKind.Object) + { + foreach (var innerItem in item.Value.EnumerateObject()) + { + if (innerItem.Value.ToString().StartsWith("|") && innerItem.Value.ToString().EndsWith("|")) + { + // |x| values are used to replace parts of response messages + if (!lastMessageJson.GetProperty(item.Name).TryGetProperty(innerItem.Name, out var prop)) + continue; + + replaceValues.Add(innerItem.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); + } + } + } + + if (item.Value.ToString().StartsWith("|") && item.Value.ToString().EndsWith("|")) + { + // |x| values are used to replace parts of response messages + if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) + continue; + + replaceValues.Add(item.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); + } + else if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) + { + } + else if (lastMessageJson.GetProperty(item.Name).ValueKind == JsonValueKind.String && lastMessageJson.GetProperty(item.Name).GetString() != item.Value.ToString() && ignoreProperties?.Contains(item.Name) != true) + { + throw new Exception($"{name} Expected {item.Name} to be {item.Value}, but was {lastMessageJson.GetProperty(item.Name).GetString()}"); + } + else + { + // TODO check arrays and sub-objects + + } + } + // TODO check arrays and sub-objects + + } + } + else if (line.StartsWith("< ")) + { + // Expect a message from server to client + foreach(var item in replaceValues) + line = line.Replace(item.Key, item.Value); + + socket.InvokeMessage(line.Substring(2)); + } + else + { + // A update message from server to client + var compareData = reader.ReadToEnd(); + foreach (var item in replaceValues) + compareData = compareData.Replace(item.Key, item.Value); + + socket.InvokeMessage(compareData); + + await task.ConfigureAwait(false); + object? result = task.Result.Data; + if (responseMapper != null) + result = responseMapper(task.Result.Data); + + if (!skipResponseValidation) + SystemTextJsonComparer.CompareData(name, result, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem); + } + } + + Trace.Listeners.Remove(listener); + } + } +} diff --git a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs index 87ca865..b4dbc48 100644 --- a/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs +++ b/CryptoExchange.Net/Testing/SocketSubscriptionValidator.cs @@ -2,12 +2,12 @@ using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Testing.Comparers; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -23,7 +23,6 @@ namespace CryptoExchange.Net.Testing private readonly string _folder; private readonly string _baseAddress; private readonly string? _nestedPropertyForCompare; - private readonly bool _stjCompare; /// /// ctor @@ -32,14 +31,12 @@ namespace CryptoExchange.Net.Testing /// Folder for json test values /// The base address that is expected /// Property to use for compare - /// Use System.Text.Json for comparing - public SocketSubscriptionValidator(TClient client, string folder, string baseAddress, string? nestedPropertyForCompare = null, bool stjCompare = true) + public SocketSubscriptionValidator(TClient client, string folder, string baseAddress, string? nestedPropertyForCompare = null) { _client = client; _folder = folder; _baseAddress = baseAddress; _nestedPropertyForCompare = nestedPropertyForCompare; - _stjCompare = stjCompare; } /// @@ -52,6 +49,7 @@ namespace CryptoExchange.Net.Testing /// Ignore certain properties /// Use the first item of an array update /// Path + /// Whether to skip comparing the json model with the update model /// /// public async Task ValidateAsync( @@ -60,7 +58,8 @@ namespace CryptoExchange.Net.Testing string? nestedJsonProperty = null, List? ignoreProperties = null, string? addressPath = null, - bool? useFirstUpdateItem = null) + bool? useFirstUpdateItem = null, + bool? skipUpdateValidation = null) { var listener = new EnumValueTraceListener(); Trace.Listeners.Add(listener); @@ -97,8 +96,7 @@ namespace CryptoExchange.Net.Testing // Invoke subscription method var task = methodInvoke(_client, x => { update = x.Data; }); - string? overrideKey = null; - string? overrideValue = null; + var replaceValues = new Dictionary(); while (true) { var line = reader.ReadLine(); @@ -113,42 +111,62 @@ namespace CryptoExchange.Net.Testing if (lastMessage == null) throw new Exception($"{name} expected to {line} to be send to server but did not receive anything"); - var lastMessageJson = JToken.Parse(lastMessage); - var expectedJson = JToken.Parse(line.Substring(2)); - foreach(var item in expectedJson) + var lastMessageJson = JsonDocument.Parse(lastMessage).RootElement; + var expectedJson = JsonDocument.Parse(line.Substring(2)).RootElement; + if (expectedJson.ValueKind == JsonValueKind.Object) { - if (item is JProperty prop && prop.Value is JValue val) + foreach (var item in expectedJson.EnumerateObject()) { - if (val.ToString().StartsWith("|") && val.ToString().EndsWith("|")) + if (item.Value.ValueKind == JsonValueKind.Object) { - // |x| values are used to replace parts or response messages - overrideKey = val.ToString(); - overrideValue = lastMessageJson[prop.Name]?.Value(); + foreach (var innerItem in item.Value.EnumerateObject()) + { + if (innerItem.Value.ToString().StartsWith("|") && innerItem.Value.ToString().EndsWith("|")) + { + // |x| values are used to replace parts of response messages + if (!lastMessageJson.GetProperty(item.Name).TryGetProperty(innerItem.Name, out var prop)) + continue; + + replaceValues.Add(innerItem.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); + } + } } - else if (val.ToString() == "-999") + + if (item.Value.ToString().StartsWith("|") && item.Value.ToString().EndsWith("|")) { - // -999 value is used to replace parts or response messages - overrideKey = val.ToString(); - overrideValue = lastMessageJson[prop.Name]?.Value().ToString(); + // |x| values are used to replace parts of response messages + if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) + continue; + + replaceValues.Add(item.Value.ToString(), prop.ValueKind == JsonValueKind.String ? prop.GetString()! : prop.GetInt64().ToString()!); } - else if (lastMessageJson[prop.Name]?.Value() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true) + else if (item.Value.ToString() == "-999") { - throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value()}"); + // |x| values are used to replace parts of response messages + if (!lastMessageJson.TryGetProperty(item.Name, out var prop)) + continue; + + replaceValues.Add(item.Value.ToString(), prop.GetDecimal().ToString()!); + } + else if (lastMessageJson.GetProperty(item.Name).ValueKind == JsonValueKind.String && lastMessageJson.GetProperty(item.Name).GetString() != item.Value.ToString() && ignoreProperties?.Contains(item.Name) != true) + { + throw new Exception($"{name} Expected {item.Name} to be {item.Value}, but was {lastMessageJson.GetProperty(item.Name).GetString()}"); + } + else + { + // TODO check arrays and sub-objects + } } + // TODO check arrays and sub-objects - // TODO check objects and arrays } } else if (line.StartsWith("< ")) { // Expect a message from server to client - if (overrideKey != null) - { - line = line.Replace(overrideKey, overrideValue); - overrideKey = null; - overrideValue = null; - } + foreach (var item in replaceValues) + line = line.Replace(item.Key, item.Value); socket.InvokeMessage(line.Substring(2)); } @@ -156,15 +174,16 @@ namespace CryptoExchange.Net.Testing { // A update message from server to client var compareData = reader.ReadToEnd(); + foreach (var item in replaceValues) + compareData = compareData.Replace(item.Key, item.Value); + socket.InvokeMessage(compareData); if (update == null) throw new Exception($"{name} Update send to client did not trigger in update handler"); - if (_stjCompare == true) + if (skipUpdateValidation != true) SystemTextJsonComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false); - else - JsonNetComparer.CompareData(name, update, compareData, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useFirstUpdateItem ?? false); } } diff --git a/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs index f304249..11b7afc 100644 --- a/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/IKlineTracker.cs @@ -91,7 +91,7 @@ namespace CryptoExchange.Net.Trackers.Klines /// Start timestamp to get the data from, defaults to tracked data start time /// End timestamp to get the data until, defaults to current time /// - IEnumerable GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); + SharedKline[] GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); /// /// Get statistics on the klines diff --git a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs index a11c60f..84af551 100644 --- a/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs +++ b/CryptoExchange.Net/Trackers/Klines/KlineTracker.cs @@ -179,7 +179,7 @@ namespace CryptoExchange.Net.Trackers.Klines var startResult = await DoStartAsync().ConfigureAwait(false); if (!startResult) { - _logger.KlineTrackerStartFailed(SymbolName, startResult.Error!.ToString()); + _logger.KlineTrackerStartFailed(SymbolName, startResult.Error!.Message, startResult.Error.Exception); Status = SyncStatus.Disconnected; return new CallResult(startResult.Error!); } @@ -190,7 +190,7 @@ namespace CryptoExchange.Net.Trackers.Klines _updateSubscription.ConnectionRestored += HandleConnectionRestored; Status = SyncStatus.Synced; _logger.KlineTrackerStarted(SymbolName); - return new CallResult(null); + return CallResult.SuccessResult; } /// @@ -285,7 +285,7 @@ namespace CryptoExchange.Net.Trackers.Klines } /// - public IEnumerable GetData(DateTime? since = null, DateTime? until = null) + public SharedKline[] GetData(DateTime? since = null, DateTime? until = null) { lock (_lock) { @@ -297,7 +297,7 @@ namespace CryptoExchange.Net.Trackers.Klines if (until != null) result = result.Where(d => d.OpenTime <= until); - return result.ToList(); + return result.ToArray(); } } diff --git a/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs index 713dee4..4f08aea 100644 --- a/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/ITradeTracker.cs @@ -87,7 +87,7 @@ namespace CryptoExchange.Net.Trackers.Trades /// Start timestamp to get the data from, defaults to tracked data start time /// End timestamp to get the data until, defaults to current time /// - IEnumerable GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); + SharedTrade[] GetData(DateTime? fromTimestamp = null, DateTime? toTimestamp = null); /// /// Get statistics on the trades diff --git a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs index e92be50..bdf56db 100644 --- a/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs +++ b/CryptoExchange.Net/Trackers/Trades/TradeTracker.cs @@ -202,7 +202,7 @@ namespace CryptoExchange.Net.Trackers.Trades var subResult = await DoStartAsync().ConfigureAwait(false); if (!subResult) { - _logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.ToString()); + _logger.TradeTrackerStartFailed(SymbolName, subResult.Error!.Message, subResult.Error.Exception); Status = SyncStatus.Disconnected; return subResult; } @@ -213,7 +213,7 @@ namespace CryptoExchange.Net.Trackers.Trades _updateSubscription.ConnectionRestored += HandleConnectionRestored; SetSyncStatus(); _logger.TradeTrackerStarted(SymbolName); - return new CallResult(null); + return CallResult.SuccessResult; } /// @@ -297,7 +297,7 @@ namespace CryptoExchange.Net.Trackers.Trades protected virtual Task DoStopAsync() => _updateSubscription?.CloseAsync() ?? Task.CompletedTask; /// - public IEnumerable GetData(DateTime? since = null, DateTime? until = null) + public SharedTrade[] GetData(DateTime? since = null, DateTime? until = null) { lock (_lock) { @@ -309,7 +309,7 @@ namespace CryptoExchange.Net.Trackers.Trades if (until != null) result = result.Where(d => d.Timestamp <= until); - return result.ToList(); + return result.ToArray(); } } diff --git a/README.md b/README.md index 1882476..5a4bcbf 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,72 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). ## Release notes +* Version 9.0.0-beta7 - 06 May 2025 + * Added AssetAlias configuration classes + * Added Exception property on Error objects + * Updated logging to no longer log full stacktraces but instead pass exceptions to struct + * Removed the Data property from Error objects (is already available on CallResult)ured logging + * Updated ArrayConverter _typeAttributesCache from ConcurrentDictionary to lazy list since each type has it's own ArrayConverter instance now + * Updated SocketApiClient to create RateLimiter instance if none is provided + * Fixed issue with ArrayConverter creating seperate JsonSerializerContext instance for each type by adding JsonSerializerContextCache + +* Version 9.0.0-beta6 - 03 May 2025 + * Fixed initial asset info in ExchangeSymbolCache not getting cached in uppercase + * Fixed ArrayConverter creating new JsonSerializerContext for each default json deserialization + +* Version 9.0.0-beta5 - 30 Apr 2025 + * Add support for specifying additional custom converters when creating JsonSerializerOptions + * Fix issue overriding default options when using SetApiCredentials on client + +* Version 9.0.0-beta4 - 28 Apr 2025 + * Support reading of int32 (up from int16) as enum values in EnumConverter + +* Version 9.0.0-beta3 - 27 Apr 2025 + * Fixed memory leak in cache + * Fixed memory leak in array converter + * Fixed incorrect log verbosity for log message when stopping order book + +* Version 9.0.0-beta2 - 23 Apr 2025 + * Added QuoteVolume property to SharedSpotTicker model + * Map asset names as uppercase in ExchangeSymbolCache + * Renamed ISpotOrderClientIdClient to ISpotOrderClientIdRestClient and IFuturesOrderClientIdClient to IFuturesOrderClientIdRestClient + +* Version 9.0.0-beta1 - 16 Apr 2025 + * Added support for Native AOT compilation + * Updated all IEnumerable response types to array response types + * Added Pass support for ApiCredentials, removing the need for most implementations to add their own ApiCredentials type + * Added KeepAliveTimeout setting setting ping frame timeouts for SocketApiClient + * Added IBookTickerRestClient Shared interface for requesting book tickers + * Added ISpotTriggerOrderRestClient Shared interface for managing spot trigger orders + * Added ISpotOrderClientIdClient Shared interface for managing spot orders by client order id + * Added IFuturesTriggerOrderRestClient Shared interface for managing futures trigger orders + * Added IFuturesOrderClientIdClient Shared interface for managing futures orders by client order id + * Added IFuturesTpSlRestClient Shared interface for setting TP/SL on open futures positions + * Added GenerateClientOrderId to ISpotOrderRestClient and IFuturesOrderRestClient interface + * Added OptionalExchangeParameters and Supported properties to EndpointOptions + * Refactor Shared interfaces quantity parameters and properties to use SharedQuantity + * Added SharedSymbol property to Shared interface models returning a symbol + * Added TriggerPrice, IsTriggerOrder, TakeProfitPrice, StopLossPrice and IsCloseOrder to SharedFuturesOrder response model + * Added MaxShortLeverage and MaxLongLeverage to SharedFuturesSymbol response model + * Added StopLossPrice and TakeProfitPrice to SharedPosition response model + * Added TriggerPrice and IsTriggerOrder to SharedSpotOrder response model + * Added static ExchangeSymbolCache for tracking symbol information from exchanges + * Added static CallResult.SuccessResult to be used instead of constructing success CallResult instance + * Added static ApplyRules, RandomHexString and RandomLong helper methods to ExchangeHelpers class + * Added AsErrorWithData To CallResult + * Added OriginalData property to CallResult + * Added support for adjusting the rate limit key per call, allowing for ratelimiting depending on request parameters + * Added implementation for integration testing ISymbolOrderBook instances + * Added implementation for integration testing socket subscriptions + * Added implementation for testing socket queries + * Updated request cancellation logging to Debug level + * Fixed memory leak in AsyncAutoRestEvent + * Fixed logging for ping frame timeout + * Fixed socket client `UnsubscribeAll` not unsubscribing dedicated connections + * Removed Newtonsoft.Json dependency + * Removed legacy Rest client code + * Removed legacy ISpotClient and IFuturesClient support + * Version 8.8.0 - 10 Feb 2025 * Split DataEvent.Timestamp in DataEvent.ReceivedTime and DataEvent.DataTime * Added SharedKlineInterval enum values