mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-07 16:06:15 +00:00
Feature/9.0.0 (#236)
* 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 QuoteVolume property to SharedSpotTicker response model * Added AssetAlias configuration models * 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 * Updated logging SourceContext to include the client type * Updated some logging logic, errors no longer contain any data, exception are not logged as string but instead forwarded to structured logging * Fixed warning for Enum parsing throwing exception and output warnings for each object in a response to only once to prevent slowing down execution * Fixed memory leak in AsyncAutoRestEvent * Fixed logging for ping frame timeout * Fixed warning getting logged when user stops SymbolOrderBook instance * Fixed socket client `UnsubscribeAll` not unsubscribing dedicated connections * Fixed memory leak in Rest client cache * Fixed integers bigger than int16 not getting correctly parsed to enums * Fixed issue where the default options were overridden when using SetApiCredentials * Removed Newtonsoft.Json dependency * Removed legacy Rest client code * Removed legacy ISpotClient and IFuturesClient support
This commit is contained in:
parent
3d6267da93
commit
6b14cdbf06
@ -112,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new KeyValuePair<string, string[]>[0],
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
@ -120,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new KeyValuePair<string, string[]>[0],
|
||||||
ResultDataSource.Server,
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
@ -142,7 +142,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new KeyValuePair<string, string[]>[0],
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
@ -150,7 +150,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new KeyValuePair<string, string[]>[0],
|
||||||
ResultDataSource.Server,
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
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<TimeObject>($"{{ \"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<TimeObject>($"{{ \"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<TimeObject>($"{{ \"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<TimeObject>($"{{ \"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<EnumObject>($"{{ \"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<NotNullableEnumObject>($"{{ \"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<BoolObject>($"{{ \"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<NotNullableBoolObject>($"{{ \"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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +1,9 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -18,6 +13,7 @@ using System.Net;
|
|||||||
using CryptoExchange.Net.RateLimiting.Guards;
|
using CryptoExchange.Net.RateLimiting.Guards;
|
||||||
using CryptoExchange.Net.RateLimiting.Filters;
|
using CryptoExchange.Net.RateLimiting.Filters;
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@ -30,7 +26,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
// arrange
|
// arrange
|
||||||
var client = new TestRestClient();
|
var client = new TestRestClient();
|
||||||
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
|
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
|
// act
|
||||||
var result = client.Api1.Request<TestObject>().Result;
|
var result = client.Api1.Request<TestObject>().Result;
|
||||||
@ -84,8 +80,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
ClassicAssert.IsFalse(result.Success);
|
ClassicAssert.IsFalse(result.Success);
|
||||||
Assert.That(result.Error != null);
|
Assert.That(result.Error != null);
|
||||||
Assert.That(result.Error is ServerError);
|
Assert.That(result.Error is ServerError);
|
||||||
Assert.That(result.Error.Message.Contains("Invalid request"));
|
|
||||||
Assert.That(result.Error.Message.Contains("123"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
@ -140,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
client.SetResponse("{}", out var request);
|
client.SetResponse("{}", out var request);
|
||||||
|
|
||||||
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new Dictionary<string, object>
|
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
|
||||||
{
|
{
|
||||||
{ "TestParam1", "Value1" },
|
{ "TestParam1", "Value1" },
|
||||||
{ "TestParam2", 2 },
|
{ "TestParam2", 2 },
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
@ -9,7 +10,6 @@ using CryptoExchange.Net.UnitTests.TestImplementations;
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
});
|
||||||
sub.AddSubscription(subObj);
|
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
|
// act
|
||||||
socket.InvokeMessage(msgToSend);
|
socket.InvokeMessage(msgToSend);
|
||||||
@ -198,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
// act
|
// act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
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;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@ -221,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
// act
|
// act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
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;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
|
@ -146,7 +146,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}");
|
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||||
Assert.That(output.Value == expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,8 +171,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase("three", TestEnum.Three)]
|
[TestCase("three", TestEnum.Three)]
|
||||||
[TestCase("Four", TestEnum.Four)]
|
[TestCase("Four", TestEnum.Four)]
|
||||||
[TestCase("four", TestEnum.Four)]
|
[TestCase("four", TestEnum.Four)]
|
||||||
[TestCase("Four1", TestEnum.One)]
|
[TestCase("Four1", null)]
|
||||||
[TestCase(null, TestEnum.One)]
|
[TestCase(null, null)]
|
||||||
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
||||||
{
|
{
|
||||||
var result = EnumConverter.ParseString<TestEnum>(value);
|
var result = EnumConverter.ParseString<TestEnum>(value);
|
||||||
@ -194,7 +194,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestBoolConverter(string value, bool? expected)
|
public void TestBoolConverter(string value, bool? expected)
|
||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}");
|
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||||
Assert.That(output.Value == expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -213,7 +213,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
public void TestBoolConverterNotNullable(string value, bool expected)
|
||||||
{
|
{
|
||||||
var val = value == null ? "null" : $"\"{value}\"";
|
var val = value == null ? "null" : $"\"{value}\"";
|
||||||
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
|
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
||||||
Assert.That(output.Value == expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,9 +265,22 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Prop31 = 4,
|
Prop31 = 4,
|
||||||
Prop32 = "789"
|
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 serialized = JsonSerializer.Serialize(data);
|
||||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
||||||
|
|
||||||
@ -281,6 +294,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
||||||
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
||||||
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
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
|
public class STJEnumObject
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(EnumConverter))]
|
|
||||||
public TestEnum? Value { get; set; }
|
public TestEnum? Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotNullableSTJEnumObject
|
public class NotNullableSTJEnumObject
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(EnumConverter))]
|
|
||||||
public TestEnum Value { get; set; }
|
public TestEnum Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class STJBoolObject
|
public class STJBoolObject
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(BoolConverter))]
|
|
||||||
public bool? Value { get; set; }
|
public bool? Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NotNullableSTJBoolObject
|
public class NotNullableSTJBoolObject
|
||||||
{
|
{
|
||||||
[JsonConverter(typeof(BoolConverter))]
|
|
||||||
public bool Value { get; set; }
|
public bool Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonConverter(typeof(ArrayConverter))]
|
[JsonConverter(typeof(ArrayConverter<Test>))]
|
||||||
record Test
|
record Test
|
||||||
{
|
{
|
||||||
[ArrayProperty(0)]
|
[ArrayProperty(0)]
|
||||||
@ -339,11 +351,15 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public Test2 Prop5 { get; set; }
|
public Test2 Prop5 { get; set; }
|
||||||
[ArrayProperty(5)]
|
[ArrayProperty(5)]
|
||||||
public Test3 Prop6 { get; set; }
|
public Test3 Prop6 { get; set; }
|
||||||
[ArrayProperty(6), JsonConverter(typeof(EnumConverter))]
|
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||||
public TestEnum? Prop7 { get; set; }
|
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<Test2>))]
|
||||||
record Test2
|
record Test2
|
||||||
{
|
{
|
||||||
[ArrayProperty(0)]
|
[ArrayProperty(0)]
|
||||||
@ -359,4 +375,29 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[JsonPropertyName("prop32")]
|
[JsonPropertyName("prop32")]
|
||||||
public string Prop32 { get; set; }
|
public string Prop32 { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(EnumConverter<TestEnum>))]
|
||||||
|
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
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,31 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||||
{
|
{
|
||||||
internal class SubResponse
|
internal class SubResponse
|
||||||
{
|
{
|
||||||
|
|
||||||
[JsonProperty("action")]
|
[JsonPropertyName("action")]
|
||||||
public string Action { get; set; } = null!;
|
public string Action { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("channel")]
|
[JsonPropertyName("channel")]
|
||||||
public string Channel { get; set; } = null!;
|
public string Channel { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("status")]
|
[JsonPropertyName("status")]
|
||||||
public string Status { get; set; } = null!;
|
public string Status { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class UnsubResponse
|
internal class UnsubResponse
|
||||||
{
|
{
|
||||||
[JsonProperty("action")]
|
[JsonPropertyName("action")]
|
||||||
public string Action { get; set; } = null!;
|
public string Action { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("status")]
|
[JsonPropertyName("status")]
|
||||||
public string Status { get; set; } = null!;
|
public string Status { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,14 +3,18 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@ -21,12 +25,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public TestBaseClient(): base(null, "Test")
|
public TestBaseClient(): base(null, "Test")
|
||||||
{
|
{
|
||||||
var options = new TestClientOptions();
|
var options = new TestClientOptions();
|
||||||
|
_logger = NullLogger.Instance;
|
||||||
Initialize(options);
|
Initialize(options);
|
||||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
|
||||||
{
|
{
|
||||||
|
_logger = NullLogger.Instance;
|
||||||
Initialize(exchangeOptions);
|
Initialize(exchangeOptions);
|
||||||
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
|
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 string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
public override TimeSpan? GetTimeOffset() => null;
|
public override TimeSpan? GetTimeOffset() => null;
|
||||||
public override TimeSyncInfo GetTimeSyncInfo() => 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 AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
using Newtonsoft.Json;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
public class TestObject
|
public class TestObject
|
||||||
{
|
{
|
||||||
[JsonProperty("other")]
|
[JsonPropertyName("other")]
|
||||||
public string StringData { get; set; }
|
public string StringData { get; set; }
|
||||||
|
[JsonPropertyName("intData")]
|
||||||
public int IntData { get; set; }
|
public int IntData { get; set; }
|
||||||
|
[JsonPropertyName("decimalData")]
|
||||||
public decimal DecimalData { get; set; }
|
public decimal DecimalData { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -12,11 +11,13 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using System.Linq;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
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.IsSuccessStatusCode).Returns(true);
|
||||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
var headers = new Dictionary<string, string[]>();
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
@ -84,7 +85,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(new Dictionary<string, IEnumerable<string>>());
|
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
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.IsSuccessStatusCode).Returns(false);
|
||||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new Dictionary<string, IEnumerable<string>>();
|
var headers = new List<KeyValuePair<string, string[]>>();
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new List<string> { val }));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(headers);
|
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
@ -137,14 +138,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
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<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, requestWeight: 0, additionalHeaders: headers);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||||
@ -178,15 +182,18 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
|
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
|
||||||
{
|
{
|
||||||
var errorData = accessor.Deserialize<TestError>();
|
var errorData = accessor.Deserialize<TestError>();
|
||||||
|
|
||||||
@ -214,7 +221,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
public class TestError
|
public class TestError
|
||||||
{
|
{
|
||||||
|
[JsonPropertyName("errorCode")]
|
||||||
public int ErrorCode { get; set; }
|
public int ErrorCode { get; set; }
|
||||||
|
[JsonPropertyName("errorMessage")]
|
||||||
public string ErrorMessage { get; set; }
|
public string ErrorMessage { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ using Moq;
|
|||||||
using CryptoExchange.Net.Testing.Implementations;
|
using CryptoExchange.Net.Testing.Implementations;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
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());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
|
21
CryptoExchange.Net.UnitTests/TestSerializerContext.cs
Normal file
21
CryptoExchange.Net.UnitTests/TestSerializerContext.cs
Normal file
@ -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<string, string>))]
|
||||||
|
[JsonSerializable(typeof(IDictionary<string, string>))]
|
||||||
|
[JsonSerializable(typeof(Dictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(IDictionary<string, object>))]
|
||||||
|
[JsonSerializable(typeof(TestObject))]
|
||||||
|
internal partial class TestSerializerContext : JsonSerializerContext
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,11 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string Secret { get; set; }
|
public string Secret { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The api passphrase. Not needed on all exchanges
|
||||||
|
/// </summary>
|
||||||
|
public string? Pass { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of the credentials
|
/// Type of the credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -30,8 +35,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The api key / label used for identification</param>
|
/// <param name="key">The api key / label used for identification</param>
|
||||||
/// <param name="secret">The api secret or private key used for signing</param>
|
/// <param name="secret">The api secret or private key used for signing</param>
|
||||||
|
/// <param name="pass">The api pass for the key. Not always needed</param>
|
||||||
/// <param name="credentialType">The type of credentials</param>
|
/// <param name="credentialType">The type of credentials</param>
|
||||||
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))
|
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||||
throw new ArgumentException("Key and secret can't be null/empty");
|
throw new ArgumentException("Key and secret can't be null/empty");
|
||||||
@ -39,6 +45,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
CredentialType = credentialType;
|
CredentialType = credentialType;
|
||||||
Key = key;
|
Key = key;
|
||||||
Secret = secret;
|
Secret = secret;
|
||||||
|
Pass = pass;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -47,28 +54,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual ApiCredentials Copy()
|
public virtual ApiCredentials Copy()
|
||||||
{
|
{
|
||||||
return new ApiCredentials(Key, Secret, CredentialType);
|
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create Api credentials providing a stream containing json data. The json data should include two values: apiKey and apiSecret
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="inputStream">The stream containing the json data</param>
|
|
||||||
/// <param name="identifierKey">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'.</param>
|
|
||||||
/// <param name="identifierSecret">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'.</param>
|
|
||||||
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<string>(MessagePath.Get().Property(identifierKey ?? "apiKey"));
|
|
||||||
var secret = accessor.GetValue<string>(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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,10 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// Get the API key of the current credentials
|
/// Get the API key of the current credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ApiKey => _credentials.Key!;
|
public string ApiKey => _credentials.Key!;
|
||||||
|
/// <summary>
|
||||||
|
/// Get the Passphrase of the current credentials
|
||||||
|
/// </summary>
|
||||||
|
public string? Pass => _credentials.Pass;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Caching
|
namespace CryptoExchange.Net.Caching
|
||||||
{
|
{
|
||||||
internal class MemoryCache
|
internal class MemoryCache
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a new cache entry. Will override an existing entry if it already exists
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
@ -26,16 +28,13 @@ namespace CryptoExchange.Net.Caching
|
|||||||
/// <returns>Cached value if it was in cache</returns>
|
/// <returns>Cached value if it was in cache</returns>
|
||||||
public object? Get(string key, TimeSpan maxAge)
|
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);
|
_cache.TryGetValue(key, out CacheItem? value);
|
||||||
if (value == null)
|
if (value == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
|
||||||
{
|
|
||||||
_cache.TryRemove(key, out _);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.Value;
|
return value.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,7 +39,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public bool OutputOriginalData { get; }
|
public bool OutputOriginalData { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
|
public bool Authenticated => ApiCredentials != null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api options
|
/// Api options
|
||||||
@ -68,9 +71,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ApiOptions = apiOptions;
|
ApiOptions = apiOptions;
|
||||||
OutputOriginalData = outputOriginalData;
|
OutputOriginalData = outputOriginalData;
|
||||||
BaseAddress = baseAddress;
|
BaseAddress = baseAddress;
|
||||||
|
ApiCredentials = apiCredentials?.Copy();
|
||||||
|
|
||||||
if (apiCredentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -86,9 +90,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
{
|
{
|
||||||
ApiOptions.ApiCredentials = credentials;
|
ApiCredentials = credentials?.Copy();
|
||||||
if (credentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -97,9 +101,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ClientOptions.Proxy = options.Proxy;
|
ClientOptions.Proxy = options.Proxy;
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||||
|
|
||||||
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
|
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||||
if (options.ApiCredentials != null)
|
if (ApiCredentials != null)
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -66,8 +66,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected BaseClient(ILoggerFactory? logger, string exchange)
|
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.
|
#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;
|
Exchange = exchange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@ -19,6 +20,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
{
|
{
|
||||||
|
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,12 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Xml.Linq;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@ -33,10 +35,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="loggerFactory">Logger factory</param>
|
||||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
/// <param name="name">The name of the exchange this client is for</param>
|
||||||
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
|
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
{
|
{
|
||||||
|
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@ -24,24 +23,5 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get a list of the registered ISpotClient implementations
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public IEnumerable<ISpotClient> GetSpotClients()
|
|
||||||
{
|
|
||||||
if (_serviceProvider == null)
|
|
||||||
return new List<ISpotClient>();
|
|
||||||
|
|
||||||
return _serviceProvider.GetServices<ISpotClient>().ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get an ISpotClient implementation by exchange name
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exchangeName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider?.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
@ -9,7 +8,6 @@ using System.Net.Http;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Caching;
|
using CryptoExchange.Net.Caching;
|
||||||
using CryptoExchange.Net.Converters.JsonNet;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
@ -114,13 +112,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Create a message accessor instance
|
/// Create a message accessor instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IStreamMessageAccessor CreateAccessor() => new JsonNetStreamMessageAccessor();
|
protected abstract IStreamMessageAccessor CreateAccessor();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a serializer instance
|
/// Create a serializer instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected abstract IMessageSerializer CreateSerializer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a request to the base address based on the request definition
|
/// Send a request to the base address based on the request definition
|
||||||
@ -243,10 +241,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||||
if (result.Error is not CancellationRequestedError)
|
if (result.Error is not CancellationRequestedError)
|
||||||
{
|
{
|
||||||
|
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
|
||||||
if (!result)
|
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
|
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
|
else
|
||||||
{
|
{
|
||||||
@ -343,7 +342,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -437,215 +436,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return request;
|
return request;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a request to the uri and returns if it was successful
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="gate">The ratelimit gate to use</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNull]
|
|
||||||
protected virtual async Task<WebCallResult> SendRequestAsync(
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
Dictionary<string, string>? 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<object>(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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="gate">The ratelimit gate to use</param>
|
|
||||||
/// <param name="preventCaching">Whether caching should be prevented for this request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNull]
|
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
Dictionary<string, string>? 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<T>)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<T>(request.Error!);
|
|
||||||
|
|
||||||
var result = await GetResponseAsync<T>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prepares a request to be sent to the server
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="requestBodyFormat">The format of the body content</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="gate">The rate limit gate to use</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
IRateLimitGate? gate = null)
|
|
||||||
{
|
|
||||||
var requestId = ExchangeHelpers.NextId();
|
|
||||||
|
|
||||||
if (signed)
|
|
||||||
{
|
|
||||||
if (AuthenticationProvider == null)
|
|
||||||
{
|
|
||||||
_logger.RestApiNoApiCredentials(requestId, uri.AbsolutePath);
|
|
||||||
return new CallResult<IRequest>(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<IRequest>(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<IRequest>(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<IRequest>(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the request and returns the result deserialized into the type parameter class
|
/// Executes the request and returns the result deserialized into the type parameter class
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -676,7 +466,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
// Error response
|
// Error response
|
||||||
await accessor.Read(responseStream, true).ConfigureAwait(false);
|
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
|
||||||
|
|
||||||
Error error;
|
Error error;
|
||||||
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
|
||||||
@ -692,7 +482,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
else
|
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)
|
if (error.Code == null || error.Code == 0)
|
||||||
@ -701,15 +491,15 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new WebCallResult<T>(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!);
|
return new WebCallResult<T>(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))
|
if (typeof(T) == typeof(object))
|
||||||
// Success status code and expected empty response, assume it's correct
|
// Success status code and expected empty response, assume it's correct
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
return new WebCallResult<T>(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)
|
if (!valid)
|
||||||
{
|
{
|
||||||
// Invalid json
|
// 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<T>(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);
|
return new WebCallResult<T>(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)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var exceptionInfo = requestException.ToLogString();
|
return new WebCallResult<T>(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));
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException canceledException)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// Cancellation token canceled by caller
|
||||||
return new WebCallResult<T>(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<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
return new WebCallResult<T>(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<T>(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
|
finally
|
||||||
@ -768,7 +557,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="accessor">Data accessor</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
/// <returns>Null if not an error, Error otherwise</returns>
|
/// <returns>Null if not an error, Error otherwise</returns>
|
||||||
protected virtual Error? TryParseError(IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor) => null;
|
protected virtual Error? TryParseError(KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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.
|
/// 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a request object
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
|
||||||
/// <param name="bodyFormat">Format of the body content</param>
|
|
||||||
/// <param name="requestId">Unique id of a request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual IRequest ConstructRequest(
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
Dictionary<string, object>? parameters,
|
|
||||||
bool signed,
|
|
||||||
HttpMethodParameterPosition parameterPosition,
|
|
||||||
ArrayParametersSerialization arraySerialization,
|
|
||||||
RequestBodyFormat bodyFormat,
|
|
||||||
int requestId,
|
|
||||||
Dictionary<string, string>? additionalHeaders)
|
|
||||||
{
|
|
||||||
parameters ??= new Dictionary<string, object>();
|
|
||||||
|
|
||||||
for (var i = 0; i < parameters.Count; i++)
|
|
||||||
{
|
|
||||||
var kvp = parameters.ElementAt(i);
|
|
||||||
if (kvp.Value is Func<object> 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<string, string>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes the parameters of the request to the request object body
|
/// Writes the parameters of the request to the request object body
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -942,11 +625,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="httpStatusCode">The response status code</param>
|
/// <param name="httpStatusCode">The response status code</param>
|
||||||
/// <param name="responseHeaders">The response headers</param>
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
/// <param name="accessor">Data accessor</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
|
/// <param name="exception">Exception</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
|
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] 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(null, "Unknown request error", exception);
|
||||||
return new ServerError(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -956,23 +639,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="responseHeaders">The response headers</param>
|
/// <param name="responseHeaders">The response headers</param>
|
||||||
/// <param name="accessor">Data accessor</param>
|
/// <param name="accessor">Data accessor</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
|
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
|
||||||
{
|
{
|
||||||
var message = accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Error response content only available when OutputOriginal = true in client options]";
|
|
||||||
|
|
||||||
// Handle retry after header
|
// Handle retry after header
|
||||||
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||||
if (retryAfterHeader.Value?.Any() != true)
|
if (retryAfterHeader.Value?.Any() != true)
|
||||||
return new ServerRateLimitError(message);
|
return new ServerRateLimitError();
|
||||||
|
|
||||||
var value = retryAfterHeader.Value.First();
|
var value = retryAfterHeader.Value.First();
|
||||||
if (int.TryParse(value, out var seconds))
|
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))
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1049,9 +730,5 @@ namespace CryptoExchange.Net.Clients
|
|||||||
=> ClientOptions.CachingEnabled
|
=> ClientOptions.CachingEnabled
|
||||||
&& definition.Method == HttpMethod.Get
|
&& definition.Method == HttpMethod.Get
|
||||||
&& !definition.PreventCaching;
|
&& !definition.PreventCaching;
|
||||||
|
|
||||||
private bool ShouldCache(HttpMethod method)
|
|
||||||
=> ClientOptions.CachingEnabled
|
|
||||||
&& method == HttpMethod.Get;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using CryptoExchange.Net.Converters.JsonNet;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -42,6 +42,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
protected TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keep alive timeout for websocket connection
|
||||||
|
/// </summary>
|
||||||
|
protected TimeSpan KeepAliveTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
/// Handlers for data from the socket which doesn't need to be forwarded to the caller. Ping or welcome messages for example.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -133,13 +138,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Create a message accessor instance
|
/// Create a message accessor instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IByteMessageAccessor CreateAccessor() => new JsonNetByteMessageAccessor();
|
protected internal abstract IByteMessageAccessor CreateAccessor();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a serializer instance
|
/// Create a serializer instance
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected internal abstract IMessageSerializer CreateSerializer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Keep an open connection to this url
|
/// Keep an open connection to this url
|
||||||
@ -206,9 +211,9 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException tce)
|
||||||
{
|
{
|
||||||
return new CallResult<UpdateSubscription>(new CancellationRequestedError());
|
return new CallResult<UpdateSubscription>(new CancellationRequestedError(tce));
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -378,7 +383,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated)
|
||||||
{
|
{
|
||||||
if (socket.Connected)
|
if (socket.Connected)
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
var connectResult = await ConnectSocketAsync(socket).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
@ -388,7 +393,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
|
|
||||||
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
@ -427,7 +432,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
socket.Authenticated = true;
|
socket.Authenticated = true;
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -475,7 +480,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
protected internal virtual Task<CallResult> RevitalizeRequestAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new CallResult(null));
|
return Task.FromResult(CallResult.SuccessResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -561,11 +566,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected async virtual Task HandleConnectRateLimitedAsync()
|
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);
|
var retryAfter = DateTime.UtcNow.Add(ClientOptions.ConnectDelayAfterRateLimited.Value);
|
||||||
_logger.AddingRetryAfterGuard(retryAfter);
|
_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)
|
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
||||||
{
|
{
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
|
KeepAliveTimeout = KeepAliveTimeout,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
RateLimiter = ClientOptions.RateLimiterEnabled ? RateLimiter : null,
|
||||||
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
RateLimitingBehavior = ClientOptions.RateLimitingBehaviour,
|
||||||
@ -712,7 +719,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult(connectResult.Error!);
|
return new CallResult(connectResult.Error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Balance data
|
|
||||||
/// </summary>
|
|
||||||
public class Balance: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The asset name
|
|
||||||
/// </summary>
|
|
||||||
public string Asset { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity available
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Available { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Total quantity
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Total { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for common objects
|
|
||||||
/// </summary>
|
|
||||||
public class BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The source object the data is derived from
|
|
||||||
/// </summary>
|
|
||||||
public object SourceObject { get; set; } = null!;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,73 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order type
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderType
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Limit type
|
|
||||||
/// </summary>
|
|
||||||
Limit,
|
|
||||||
/// <summary>
|
|
||||||
/// Market type
|
|
||||||
/// </summary>
|
|
||||||
Market,
|
|
||||||
/// <summary>
|
|
||||||
/// Other order type
|
|
||||||
/// </summary>
|
|
||||||
Other
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Order side
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderSide
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Buy order
|
|
||||||
/// </summary>
|
|
||||||
Buy,
|
|
||||||
/// <summary>
|
|
||||||
/// Sell order
|
|
||||||
/// </summary>
|
|
||||||
Sell
|
|
||||||
}
|
|
||||||
/// <summary>
|
|
||||||
/// Order status
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonOrderStatus
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// placed and not fully filled order
|
|
||||||
/// </summary>
|
|
||||||
Active,
|
|
||||||
/// <summary>
|
|
||||||
/// canceled order
|
|
||||||
/// </summary>
|
|
||||||
Canceled,
|
|
||||||
/// <summary>
|
|
||||||
/// filled order
|
|
||||||
/// </summary>
|
|
||||||
Filled
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Position side
|
|
||||||
/// </summary>
|
|
||||||
public enum CommonPositionSide
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Long position
|
|
||||||
/// </summary>
|
|
||||||
Long,
|
|
||||||
/// <summary>
|
|
||||||
/// Short position
|
|
||||||
/// </summary>
|
|
||||||
Short,
|
|
||||||
/// <summary>
|
|
||||||
/// Both
|
|
||||||
/// </summary>
|
|
||||||
Both
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Kline data
|
|
||||||
/// </summary>
|
|
||||||
public class Kline: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Opening time of the kline
|
|
||||||
/// </summary>
|
|
||||||
public DateTime OpenTime { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Price at the open time
|
|
||||||
/// </summary>
|
|
||||||
public decimal? OpenPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Highest price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? HighPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Lowest price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LowPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Close price of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? ClosePrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Volume of the kline
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Volume { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order data
|
|
||||||
/// </summary>
|
|
||||||
public class Order: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the order
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the order
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Price { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The quantity of the order which has been filled
|
|
||||||
/// </summary>
|
|
||||||
public decimal? QuantityFilled { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Status of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderStatus Status { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Side of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderSide Side { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Type of the order
|
|
||||||
/// </summary>
|
|
||||||
public CommonOrderType Type { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Order time
|
|
||||||
/// </summary>
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order book data
|
|
||||||
/// </summary>
|
|
||||||
public class OrderBook: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// List of bids
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<OrderBookEntry> Bids { get; set; } = Array.Empty<OrderBookEntry>();
|
|
||||||
/// <summary>
|
|
||||||
/// List of asks
|
|
||||||
/// </summary>
|
|
||||||
public IEnumerable<OrderBookEntry> Asks { get; set; } = Array.Empty<OrderBookEntry>();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Order book entry
|
|
||||||
/// </summary>
|
|
||||||
public class OrderBookEntry
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the entry
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the entry
|
|
||||||
/// </summary>
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of an order
|
|
||||||
/// </summary>
|
|
||||||
public class OrderId: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of an order
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Position data
|
|
||||||
/// </summary>
|
|
||||||
public class Position: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the position
|
|
||||||
/// </summary>
|
|
||||||
public string? Id { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the position
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Leverage
|
|
||||||
/// </summary>
|
|
||||||
public decimal Leverage { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position quantity
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Entry price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? EntryPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Liquidation price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LiquidationPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Unrealized profit and loss
|
|
||||||
/// </summary>
|
|
||||||
public decimal? UnrealizedPnl { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Realized profit and loss
|
|
||||||
/// </summary>
|
|
||||||
public decimal? RealizedPnl { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Mark price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MarkPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Auto adding margin
|
|
||||||
/// </summary>
|
|
||||||
public bool? AutoMargin { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position margin
|
|
||||||
/// </summary>
|
|
||||||
public decimal? PositionMargin { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Position side
|
|
||||||
/// </summary>
|
|
||||||
public CommonPositionSide? Side { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Is isolated
|
|
||||||
/// </summary>
|
|
||||||
public bool? Isolated { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Maintenance margin
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MaintananceMargin { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol data
|
|
||||||
/// </summary>
|
|
||||||
public class Symbol: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Name of the symbol
|
|
||||||
/// </summary>
|
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Minimal quantity of an order
|
|
||||||
/// </summary>
|
|
||||||
public decimal? MinTradeQuantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Step with which the quantity should increase
|
|
||||||
/// </summary>
|
|
||||||
public decimal? QuantityStep { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// step with which the price should increase
|
|
||||||
/// </summary>
|
|
||||||
public decimal? PriceStep { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of decimals for quantity
|
|
||||||
/// </summary>
|
|
||||||
public int? QuantityDecimals { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The max amount of decimal for price
|
|
||||||
/// </summary>
|
|
||||||
public int? PriceDecimals { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Ticker data
|
|
||||||
/// </summary>
|
|
||||||
public class Ticker: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price 24 hours ago
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Price24H { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Last trade price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LastPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour low price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? LowPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour high price
|
|
||||||
/// </summary>
|
|
||||||
public decimal? HighPrice { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// 24 hour volume
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Volume { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.CommonObjects
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Trade data
|
|
||||||
/// </summary>
|
|
||||||
public class Trade: BaseCommonObject
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Symbol of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string Symbol { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Price of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal Price { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Quantity of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal Quantity { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Timestamp of the trade
|
|
||||||
/// </summary>
|
|
||||||
public DateTime Timestamp { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// User trade info
|
|
||||||
/// </summary>
|
|
||||||
public class UserTrade: Trade
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Id of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string Id { get; set; } = string.Empty;
|
|
||||||
/// <summary>
|
|
||||||
/// Order id of the trade
|
|
||||||
/// </summary>
|
|
||||||
public string? OrderId { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// Fee of the trade
|
|
||||||
/// </summary>
|
|
||||||
public decimal? Fee { get; set; }
|
|
||||||
/// <summary>
|
|
||||||
/// The asset the fee is paid in
|
|
||||||
/// </summary>
|
|
||||||
public string? FeeAsset { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 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
|
|
||||||
/// </summary>
|
|
||||||
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>();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<ArrayPropertyAttribute>(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<JsonConverterAttribute>(property) ?? GetCustomAttribute<JsonConverterAttribute>(property.PropertyType);
|
|
||||||
var conversionAttribute = GetCustomAttribute<JsonConversionAttribute>(property) ?? GetCustomAttribute<JsonConversionAttribute>(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<decimal>();
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<ArrayPropertyAttribute>(p)?.Index);
|
|
||||||
|
|
||||||
var last = -1;
|
|
||||||
foreach (var prop in ordered)
|
|
||||||
{
|
|
||||||
var arrayProp = GetCustomAttribute<ArrayPropertyAttribute>(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<JsonConverterAttribute>(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<T>(MemberInfo memberInfo) where T : Attribute =>
|
|
||||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
|
|
||||||
|
|
||||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
|
||||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,98 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base class for enum converters
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">Type of enum to convert</typeparam>
|
|
||||||
public abstract class BaseConverter<T>: JsonConverter where T: struct
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The enum->string mapping
|
|
||||||
/// </summary>
|
|
||||||
protected abstract List<KeyValuePair<T, string>> Mapping { get; }
|
|
||||||
private readonly bool _quotes;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="useQuotes"></param>
|
|
||||||
protected BaseConverter(bool useQuotes)
|
|
||||||
{
|
|
||||||
_quotes = useQuotes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a string value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public T ReadString(string data)
|
|
||||||
{
|
|
||||||
return Mapping.FirstOrDefault(v => v.Value == data).Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<T, string>)))
|
|
||||||
mapping = Mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
{
|
|
||||||
result = mapping.Key;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetValue(T value)
|
|
||||||
{
|
|
||||||
return Mapping.FirstOrDefault(v => v.Key.Equals(value)).Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
|
||||||
/// </summary>
|
|
||||||
public class BigDecimalConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return Nullable.GetUnderlyingType(objectType) == typeof(decimal);
|
|
||||||
return objectType == typeof(decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
writer.WriteValue(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Boolean converter with support for "0"/"1" (strings)
|
|
||||||
/// </summary>
|
|
||||||
public class BoolConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether this instance can convert the specified object type.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="objectType">Type of the object.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// <c>true</c> if this instance can convert the specified object type; otherwise, <c>false</c>.
|
|
||||||
/// </returns>
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
|
||||||
return Nullable.GetUnderlyingType(objectType) == typeof(bool);
|
|
||||||
return objectType == typeof(bool);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reads the JSON representation of the object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="reader">The <see cref="T:Newtonsoft.Json.JsonReader"/> to read from.</param>
|
|
||||||
/// <param name="objectType">Type of the object.</param>
|
|
||||||
/// <param name="existingValue">The existing value of object being read.</param>
|
|
||||||
/// <param name="serializer">The calling serializer.</param>
|
|
||||||
/// <returns>
|
|
||||||
/// The object value.
|
|
||||||
/// </returns>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Specifies that this converter will not participate in writing results.
|
|
||||||
/// </summary>
|
|
||||||
public override bool CanWrite { get { return false; } }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the JSON representation of the object.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writer">The <see cref="T:Newtonsoft.Json.JsonWriter"/> to write to.</param><param name="value">The value.</param><param name="serializer">The calling serializer.</param>
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,240 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
|
|
||||||
/// </summary>
|
|
||||||
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;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return objectType == typeof(DateTime) || objectType == typeof(DateTime?);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a long value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="longValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a string value to datetime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="stringValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="seconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="milliseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="microseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromMicroseconds(long microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="nanoseconds"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static DateTime ConvertFromNanoseconds(long nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null: (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
|
||||||
/// <summary>
|
|
||||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="time"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("time")]
|
|
||||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
|
||||||
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for serializing decimal values as string
|
|
||||||
/// </summary>
|
|
||||||
public class DecimalStringWriterConverter : JsonConverter
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanRead => false;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? 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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
|
||||||
/// </summary>
|
|
||||||
public class EnumConverter : JsonConverter
|
|
||||||
{
|
|
||||||
private bool _warnOnMissingEntry = true;
|
|
||||||
private bool _writeAsInt;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
public EnumConverter() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="writeAsInt"></param>
|
|
||||||
/// <param name="warnOnMissingEntry"></param>
|
|
||||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
|
||||||
_writeAsInt = writeAsInt;
|
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type objectType)
|
|
||||||
{
|
|
||||||
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<KeyValuePair<object, string>> AddMapping(Type objectType)
|
|
||||||
{
|
|
||||||
var mapping = new List<KeyValuePair<object, string>>();
|
|
||||||
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<object, string>(Enum.Parse(objectType, member.Name), value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_mapping.TryAdd(objectType, mapping);
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> 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<object, string>)))
|
|
||||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
public static string? GetString<T>(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());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net message accessor
|
|
||||||
/// </summary>
|
|
||||||
public abstract class JsonNetMessageAccessor : IMessageAccessor
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// The json token loaded
|
|
||||||
/// </summary>
|
|
||||||
protected JToken? _token;
|
|
||||||
private static readonly JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsJson { get; protected set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public object? Underlying => _token;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (!IsJson)
|
|
||||||
return new CallResult<object>(GetOriginalString());
|
|
||||||
|
|
||||||
var source = _token;
|
|
||||||
if (path != null)
|
|
||||||
source = GetPathNode(path.Value);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = source!.ToObject(type, _serializer)!;
|
|
||||||
return new CallResult<object>(result);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
|
||||||
return new CallResult<object>(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<object>(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<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
|
||||||
{
|
|
||||||
var source = _token;
|
|
||||||
if (path != null)
|
|
||||||
source = GetPathNode(path.Value);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = source!.ToObject<T>(_serializer)!;
|
|
||||||
return new CallResult<T>(result);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
|
||||||
return new CallResult<T>(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<T>(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<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public T? GetValue<T>(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<T>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public List<T?>? GetValues<T>(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<T>().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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract string GetOriginalString();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract void Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net stream message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class JsonNetStreamMessageAccessor : JsonNetMessageAccessor, IStreamMessageAccessor
|
|
||||||
{
|
|
||||||
private Stream? _stream;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<CallResult> 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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/// <inheritdoc />
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_stream?.Dispose();
|
|
||||||
_stream = null;
|
|
||||||
_token = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Json.Net byte message accessor
|
|
||||||
/// </summary>
|
|
||||||
public class JsonNetByteMessageAccessor : JsonNetMessageAccessor, IByteMessageAccessor
|
|
||||||
{
|
|
||||||
private ReadOnlyMemory<byte> _bytes;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public CallResult Read(ReadOnlyMemory<byte> 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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string GetOriginalString() =>
|
|
||||||
// Netstandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
|
||||||
#if NETSTANDARD2_0
|
|
||||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
|
||||||
#else
|
|
||||||
Encoding.UTF8.GetString(_bytes.Span);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => true;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Clear()
|
|
||||||
{
|
|
||||||
_bytes = null;
|
|
||||||
_token = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class JsonNetMessageSerializer : IMessageSerializer
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
using Newtonsoft.Json;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.JsonNet
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Serializer options
|
|
||||||
/// </summary>
|
|
||||||
public static class SerializerOptions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerSettings WithConverters => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture,
|
|
||||||
Converters =
|
|
||||||
{
|
|
||||||
new EnumConverter(),
|
|
||||||
new DateTimeConverter(),
|
|
||||||
new BoolConverter()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default json serializer settings
|
|
||||||
/// </summary>
|
|
||||||
public static JsonSerializerSettings Default => new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
31
CryptoExchange.Net/Converters/JsonSerializerContextCache.cs
Normal file
31
CryptoExchange.Net/Converters/JsonSerializerContextCache.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Caching for JsonSerializerContext instances
|
||||||
|
/// </summary>
|
||||||
|
public static class JsonSerializerContextCache
|
||||||
|
{
|
||||||
|
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
|
||||||
|
public static JsonSerializerContext GetOrCreate<T>() 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,9 @@ using System.Text.Json.Serialization;
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using CryptoExchange.Net.Attributes;
|
using CryptoExchange.Net.Attributes;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
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
|
/// 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
|
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ArrayConverter : JsonConverterFactory
|
#if NET5_0_OR_GREATER
|
||||||
|
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
||||||
|
#else
|
||||||
|
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
||||||
|
#endif
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
|
||||||
public override bool CanConvert(Type typeToConvert) => true;
|
|
||||||
|
private static readonly ConcurrentDictionary<JsonConverter, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<JsonConverter, JsonSerializerOptions>();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
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);
|
if (value == null)
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<ArrayPropertyInfo> CacheTypeAttributes()
|
||||||
|
#else
|
||||||
|
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
var attributes = new List<ArrayPropertyInfo>();
|
||||||
|
var properties = typeof(T).GetProperties();
|
||||||
|
foreach (var property in properties)
|
||||||
|
{
|
||||||
|
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
||||||
|
if (att == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||||
|
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
||||||
|
attributes.Add(new ArrayPropertyInfo
|
||||||
|
{
|
||||||
|
ArrayProperty = att,
|
||||||
|
PropertyInfo = property,
|
||||||
|
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
||||||
|
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
||||||
|
TargetType = targetType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ArrayPropertyInfo
|
private class ArrayPropertyInfo
|
||||||
{
|
{
|
||||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
public PropertyInfo PropertyInfo { get; set; } = null!;
|
||||||
public ArrayPropertyAttribute ArrayProperty { 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 bool DefaultDeserialization { get; set; }
|
||||||
public Type TargetType { get; set; } = null!;
|
public Type TargetType { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
|
||||||
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<ArrayPropertyInfo> CacheTypeAttributes(Type type)
|
|
||||||
{
|
|
||||||
var attributes = new List<ArrayPropertyInfo>();
|
|
||||||
var properties = type.GetProperties();
|
|
||||||
foreach (var property in properties)
|
|
||||||
{
|
|
||||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
|
||||||
if (att == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
attributes.Add(new ArrayPropertyInfo
|
|
||||||
{
|
|
||||||
ArrayProperty = att,
|
|
||||||
PropertyInfo = property,
|
|
||||||
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
|
|
||||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,8 +20,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
|
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BoolConverterInner<T> : JsonConverter<T>
|
private class BoolConverterInner<T> : JsonConverter<T>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@ -8,18 +9,27 @@ using System.Text.Json.Serialization;
|
|||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for comma seperated enum values
|
/// Converter for comma separated enum values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
|
#if NET5_0_OR_GREATER
|
||||||
|
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override IEnumerable<T>? 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<T>(x)).ToArray() ?? new T[0])!;
|
var str = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
|
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
|
||||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
private class DateTimeConverterInner<T> : JsonConverter<T>
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Converter mapping to an object but also handles when an empty array is send
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T? Read(
|
|
||||||
ref Utf8JsonReader reader,
|
|
||||||
Type typeToConvert,
|
|
||||||
JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
switch (reader.TokenType)
|
|
||||||
{
|
|
||||||
case JsonTokenType.StartArray:
|
|
||||||
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
|
|
||||||
return default;
|
|
||||||
case JsonTokenType.StartObject:
|
|
||||||
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
|
|
||||||
};
|
|
||||||
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
=> JsonSerializer.Serialize(writer, (object?)value, options);
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,127 +11,78 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Static EnumConverter methods
|
||||||
|
/// </summary>
|
||||||
|
public static class EnumConverter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the enum value from a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
#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<T>(string value) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.ParseString(value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enumValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
#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>(T enumValue) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.GetString(enumValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enumValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[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>(T? enumValue) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.GetString(enumValue);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class EnumConverter : JsonConverterFactory
|
#if NET5_0_OR_GREATER
|
||||||
|
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
|
||||||
|
#else
|
||||||
|
public class EnumConverter<T>
|
||||||
|
#endif
|
||||||
|
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
||||||
{
|
{
|
||||||
private bool _warnOnMissingEntry = true;
|
private static List<KeyValuePair<T, string>>? _mapping = null;
|
||||||
private bool _writeAsInt;
|
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
|
||||||
|
|
||||||
/// <summary>
|
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
/// </summary>
|
|
||||||
public EnumConverter() { }
|
|
||||||
|
|
||||||
/// <summary>
|
internal class NullableEnumConverter : JsonConverter<T?>
|
||||||
/// </summary>
|
|
||||||
/// <param name="writeAsInt"></param>
|
|
||||||
/// <param name="warnOnMissingEntry"></param>
|
|
||||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
|
||||||
{
|
{
|
||||||
_writeAsInt = writeAsInt;
|
private readonly EnumConverter<T> _enumConverter;
|
||||||
_warnOnMissingEntry = warnOnMissingEntry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
public NullableEnumConverter(EnumConverter<T> enumConverter)
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
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<KeyValuePair<object, string>> AddMapping(Type objectType)
|
|
||||||
{
|
|
||||||
var mapping = new List<KeyValuePair<object, string>>();
|
|
||||||
var enumMembers = objectType.GetMembers();
|
|
||||||
foreach (var member in enumMembers)
|
|
||||||
{
|
{
|
||||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
_enumConverter = enumConverter;
|
||||||
foreach (MapAttribute attribute in maps)
|
|
||||||
{
|
|
||||||
foreach (var value in attribute.Values)
|
|
||||||
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_mapping.TryAdd(objectType, mapping);
|
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class EnumConverterInner<T> : JsonConverter<T>
|
|
||||||
{
|
|
||||||
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)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
|
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (value == null)
|
if (value == null)
|
||||||
{
|
{
|
||||||
@ -139,106 +90,183 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (!_writeAsInt)
|
_enumConverter.Write(writer, value.Value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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);
|
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||||
writer.WriteStringValue(stringValue);
|
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
|
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 t.Value;
|
||||||
return null;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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<KeyValuePair<object, string>> enumMapping, string value, out object? result)
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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
|
// 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));
|
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
if (mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
{
|
{
|
||||||
result = mapping.Key;
|
result = mapping.Key;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
||||||
{
|
{
|
||||||
var intValue = int.Parse(value);
|
var intValue = int.Parse(value);
|
||||||
result = Enum.ToObject(objectType, intValue);
|
result = (T)Enum.ToObject(objectType, intValue);
|
||||||
return true;
|
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<KeyValuePair<T, string>> AddMapping()
|
||||||
|
{
|
||||||
|
var mapping = new List<KeyValuePair<T, string>>();
|
||||||
|
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
|
foreach (var value in attribute.Values)
|
||||||
result = Enum.Parse(objectType, value, true);
|
mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_mapping = mapping;
|
||||||
|
return mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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
|
/// 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
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <param name="enumValue"></param>
|
/// <param name="enumValue"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
[return: NotNullIfNotNull("enumValue")]
|
||||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
public static string? GetString(T? enumValue)
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="objectType"></param>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
public static string? GetString(Type objectType, object? enumValue)
|
|
||||||
{
|
{
|
||||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
if (_mapping == null)
|
||||||
|
_mapping = AddMapping();
|
||||||
|
|
||||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||||
mapping = AddMapping(objectType);
|
|
||||||
|
|
||||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the enum value from a string
|
/// Get the enum value from a string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Enum type</typeparam>
|
|
||||||
/// <param name="value">String value</param>
|
/// <param name="value">String value</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static T? ParseString<T>(string value) where T : Enum
|
public static T? ParseString(string value)
|
||||||
{
|
{
|
||||||
var type = typeof(T);
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
if (!_mapping.TryGetValue(type, out var enumMapping))
|
if (_mapping == null)
|
||||||
enumMapping = AddMapping(type);
|
_mapping = AddMapping();
|
||||||
|
|
||||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
if (mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
||||||
{
|
return mapping.Key;
|
||||||
return (T)mapping.Key;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -250,5 +278,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public JsonConverter CreateNullableConverter()
|
||||||
|
{
|
||||||
|
_nullableEnumConverter ??= new NullableEnumConverter(this);
|
||||||
|
return _nullableEnumConverter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for serializing enum values as int
|
||||||
|
/// </summary>
|
||||||
|
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
=> writer.WriteNumberValue((int)(object)value);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
internal interface INullableConverterFactory
|
||||||
|
{
|
||||||
|
JsonConverter CreateNullableConverter();
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Attribute for allowing specifying a JsonConverter with constructor parameters
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public class JsonConverterCtorAttribute : JsonConverterAttribute
|
|
||||||
{
|
|
||||||
private readonly object[] _parameters;
|
|
||||||
private readonly Type _type;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public JsonConverterCtorAttribute(Type type, params object[] parameters)
|
|
||||||
{
|
|
||||||
_type = type;
|
|
||||||
_parameters = parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert)
|
|
||||||
{
|
|
||||||
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Converter for values which contain a nested json value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
public class ObjectStringConverter<T> : JsonConverter<T>
|
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#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)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
@ -20,10 +24,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#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)
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (value is null)
|
||||||
|
@ -8,7 +8,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replace a value on a string property
|
/// Replace a value on a string property
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReplaceConverter : JsonConverter<string>
|
public abstract class ReplaceConverter : JsonConverter<string>
|
||||||
{
|
{
|
||||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||||
|
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute to mark a model as json serializable. Used for AOT compilation.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
|
||||||
|
public class SerializationModelAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SerializationModelAttribute() { }
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
public SerializationModelAttribute(Type type) { }
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
@ -8,22 +9,39 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SerializerOptions
|
public static class SerializerOptions
|
||||||
{
|
{
|
||||||
|
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions
|
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
if (!_cache.TryGetValue(typeResolver, out var options))
|
||||||
PropertyNameCaseInsensitive = false,
|
{
|
||||||
Converters =
|
options = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
Converters =
|
||||||
{
|
{
|
||||||
new DateTimeConverter(),
|
new DateTimeConverter(),
|
||||||
new EnumConverter(),
|
|
||||||
new BoolConverter(),
|
new BoolConverter(),
|
||||||
new DecimalConverter(),
|
new DecimalConverter(),
|
||||||
new IntConverter(),
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using CryptoExchange.Net.Interfaces;
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@ -20,7 +21,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected JsonDocument? _document;
|
protected JsonDocument? _document;
|
||||||
|
|
||||||
private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
|
|
||||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -32,13 +32,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public object? Underlying => throw new NotImplementedException();
|
public object? Underlying => throw new NotImplementedException();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageAccessor()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -48,6 +41,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#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<object> Deserialize(Type type, MessagePath? path = null)
|
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsJson)
|
||||||
@ -58,22 +55,26 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
|
var result = _document.Deserialize(type, _customSerializerOptions);
|
||||||
return new CallResult<object>(result!);
|
return new CallResult<object>(result!);
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<object>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
var info = $"Deserialize unknown Exception: {ex.Message}";
|
||||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<object>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#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<T> Deserialize<T>(MessagePath? path = null)
|
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||||
{
|
{
|
||||||
if (_document == null)
|
if (_document == null)
|
||||||
@ -81,18 +82,18 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
var result = _document.Deserialize<T>(_customSerializerOptions);
|
||||||
return new CallResult<T>(result!);
|
return new CallResult<T>(result!);
|
||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<T>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
var info = $"Unknown exception: {ex.Message}";
|
var info = $"Unknown exception: {ex.Message}";
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<T>(new DeserializeError(info, ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,6 +133,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
#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<T>(MessagePath path)
|
public T? GetValue<T>(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsJson)
|
||||||
@ -145,7 +150,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
|
|
||||||
@ -158,11 +163,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return (T)(object)value.Value.GetInt64().ToString();
|
return (T)(object)value.Value.GetInt64().ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return value.Value.Deserialize<T>();
|
return value.Value.Deserialize<T>(_customSerializerOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public List<T?>? GetValues<T>(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<T>(MessagePath path)
|
||||||
{
|
{
|
||||||
if (!IsJson)
|
if (!IsJson)
|
||||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
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)
|
if (value.Value.ValueKind != JsonValueKind.Array)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return value.Value.Deserialize<List<T>>()!;
|
return value.Value.Deserialize<T[]>(_customSerializerOptions)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JsonElement? GetPathNode(MessagePath path)
|
private JsonElement? GetPathNode(MessagePath path)
|
||||||
@ -240,13 +249,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonStreamMessageAccessor(): base()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -278,13 +280,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
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<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonByteMessageAccessor() : base()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -348,13 +343,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
_document = JsonDocument.Parse(data);
|
_document = JsonDocument.Parse(data);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
return new CallResult(new DeserializeError("JsonError: " + ex.Message, ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,29 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
||||||
{
|
{
|
||||||
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
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>(T message) => JsonSerializer.Serialize(message, _options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>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.</Description>
|
<Description>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.</Description>
|
||||||
<PackageVersion>8.8.0</PackageVersion>
|
<PackageVersion>9.0.0-beta7</PackageVersion>
|
||||||
<AssemblyVersion>8.8.0</AssemblyVersion>
|
<AssemblyVersion>9.0.0</AssemblyVersion>
|
||||||
<FileVersion>8.8.0</FileVersion>
|
<FileVersion>9.0.0</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>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</PackageTags>
|
<PackageTags>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</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
@ -27,6 +27,9 @@
|
|||||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
||||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<PropertyGroup Label="AOT" Condition=" '$(TargetFramework)' == 'NET8_0' Or '$(TargetFramework)' == 'NET9_0' ">
|
||||||
|
<IsAotCompatible>true</IsAotCompatible>
|
||||||
|
</PropertyGroup>
|
||||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
<IncludeSymbols>true</IncludeSymbols>
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
@ -52,10 +55,9 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||||
|
@ -15,6 +15,7 @@ namespace CryptoExchange.Net
|
|||||||
public static class ExchangeHelpers
|
public static class ExchangeHelpers
|
||||||
{
|
{
|
||||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||||
|
private const string _allowedRandomHexChars = "0123456789ABCDEF";
|
||||||
|
|
||||||
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
||||||
{
|
{
|
||||||
@ -111,6 +112,34 @@ namespace CryptoExchange.Net
|
|||||||
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Apply the provided rules to the value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Value to be adjusted</param>
|
||||||
|
/// <param name="decimals">Max decimal places</param>
|
||||||
|
/// <param name="valueStep">The value step for increase/decrease value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -192,6 +221,44 @@ namespace CryptoExchange.Net
|
|||||||
return new string(randomChars);
|
return new string(randomChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a random string of specified length
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">Length of the random string</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a long value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxLength">Max character length</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a random string of specified length
|
/// Generate a random string of specified length
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -225,10 +292,10 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="request">The request parameters</param>
|
/// <param name="request">The request parameters</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static async IAsyncEnumerable<ExchangeWebResult<IEnumerable<T>>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<IEnumerable<T>>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
public static async IAsyncEnumerable<ExchangeWebResult<T[]>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var result = new List<T>();
|
var result = new List<T>();
|
||||||
ExchangeWebResult<IEnumerable<T>> batch;
|
ExchangeWebResult<T[]> batch;
|
||||||
INextPageToken? nextPageToken = null;
|
INextPageToken? nextPageToken = null;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
70
CryptoExchange.Net/ExchangeSymbolCache.cs
Normal file
70
CryptoExchange.Net/ExchangeSymbolCache.cs
Normal file
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Cache for symbol parsing
|
||||||
|
/// </summary>
|
||||||
|
public static class ExchangeSymbolCache
|
||||||
|
{
|
||||||
|
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the cached symbol data for an exchange
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="updateData">Symbol data</param>
|
||||||
|
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 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a symbol name to a SharedSymbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="symbolName">Symbol name</param>
|
||||||
|
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<string, SharedSymbol> Symbols { get; set; }
|
||||||
|
|
||||||
|
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
|
||||||
|
{
|
||||||
|
UpdateTime = updateTime;
|
||||||
|
Symbols = symbols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,9 @@ using CryptoExchange.Net.Objects;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@ -440,6 +443,8 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
||||||
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||||
|
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||||
@ -447,6 +452,10 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
||||||
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
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)))
|
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
||||||
@ -468,6 +477,12 @@ namespace CryptoExchange.Net
|
|||||||
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
||||||
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
|
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;
|
return services;
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
public interface IBaseRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
string ExchangeName { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
event Action<OrderId> OnOrderPlaced;
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
event Action<OrderId> OnOrderCanceled;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
string GetSymbolName(string baseAsset, string quoteAsset);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
public interface IFuturesClient : IBaseRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
using CryptoExchange.Net.CommonObjects;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
public interface ISpotClient: IBaseRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
|
||||||
/// </summary>
|
|
||||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,6 +1,4 @@
|
|||||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
using System;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
@ -9,19 +7,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ICryptoRestClient
|
public interface ICryptoRestClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Get a list of all registered common ISpotClient types
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
IEnumerable<ISpotClient> GetSpotClients();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get an ISpotClient implementation by exchange name
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="exchangeName"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
ISpotClient? SpotClient(string exchangeName);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try get
|
/// Try get
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -52,7 +52,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <typeparam name="T"></typeparam>
|
/// <typeparam name="T"></typeparam>
|
||||||
/// <param name="path"></param>
|
/// <param name="path"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
List<T?>? GetValues<T>(MessagePath path);
|
T?[]? GetValues<T>(MessagePath path);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Deserialize the message into this type
|
/// Deserialize the message into this type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -10,6 +10,6 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
string Serialize(object message);
|
string Serialize<T>(T message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Get all headers
|
/// Get all headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Dictionary<string, IEnumerable<string>> GetHeaders();
|
KeyValuePair<string, string[]>[] GetHeaders();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the response
|
/// Get the response
|
||||||
|
@ -28,7 +28,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The response headers
|
/// The response headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders { get; }
|
KeyValuePair<string, string[]>[] ResponseHeaders { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the response stream
|
/// Get the response stream
|
||||||
|
@ -42,7 +42,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets
|
/// Event when order book was updated. Be careful! It can generate a lot of events at high-liquidity markets
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<(IEnumerable<ISymbolOrderBookEntry> Bids, IEnumerable<ISymbolOrderBookEntry> Asks)> OnOrderBookUpdate;
|
event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)> OnOrderBookUpdate;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
|
/// Event when the BestBid or BestAsk changes ie a Pricing Tick
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -64,17 +64,17 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a snapshot of the book at this moment
|
/// Get a snapshot of the book at this moment
|
||||||
/// </summary>
|
/// </summary>
|
||||||
(IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book { get; }
|
(ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of asks
|
/// The list of asks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<ISymbolOrderBookEntry> Asks { get; }
|
ISymbolOrderBookEntry[] Asks { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The list of bids
|
/// The list of bids
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IEnumerable<ISymbolOrderBookEntry> Bids { get; }
|
ISymbolOrderBookEntry[] Bids { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The best bid currently in the order book
|
/// The best bid currently in the order book
|
||||||
|
@ -9,7 +9,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
public static class RestApiClientLoggingExtensions
|
public static class RestApiClientLoggingExtensions
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
|
private static readonly Action<ILogger, int?, int?, long, string?, string?, Exception?> _restApiErrorReceived;
|
||||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
|
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _restApiFailedToSyncTime;
|
private static readonly Action<ILogger, int, string, Exception?> _restApiFailedToSyncTime;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
|
private static readonly Action<ILogger, int, string, Exception?> _restApiNoApiCredentials;
|
||||||
@ -25,10 +25,10 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
|
|
||||||
static RestApiClientLoggingExtensions()
|
static RestApiClientLoggingExtensions()
|
||||||
{
|
{
|
||||||
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
_restApiErrorReceived = LoggerMessage.Define<int?, int?, long, string?, string?>(
|
||||||
LogLevel.Warning,
|
LogLevel.Warning,
|
||||||
new EventId(4000, "RestApiErrorReceived"),
|
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<int?, int?, long, string?>(
|
_restApiResponseReceived = LoggerMessage.Define<int?, int?, long, string?>(
|
||||||
LogLevel.Debug,
|
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)
|
public static void RestApiResponseReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? originalData)
|
||||||
|
@ -246,9 +246,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_receivedMessageNotRecognized(logger, socketId, id, null);
|
_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)
|
public static void UserMessageProcessingFailed(this ILogger logger, int socketId, string errorMessage, Exception e)
|
||||||
{
|
{
|
||||||
|
@ -53,7 +53,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
"{Api} order book {Symbol} connection lost");
|
"{Api} order book {Symbol} connection lost");
|
||||||
|
|
||||||
_orderBookDisconnected = LoggerMessage.Define<string, string>(
|
_orderBookDisconnected = LoggerMessage.Define<string, string>(
|
||||||
LogLevel.Warning,
|
LogLevel.Debug,
|
||||||
new EventId(5004, "OrderBookDisconnected"),
|
new EventId(5004, "OrderBookDisconnected"),
|
||||||
"{Api} order book {Symbol} disconnected");
|
"{Api} order book {Symbol} disconnected");
|
||||||
|
|
||||||
|
@ -173,9 +173,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_klineTrackerStarting(logger, symbol, null);
|
_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)
|
public static void KlineTrackerStarted(this ILogger logger, string symbol)
|
||||||
@ -233,9 +233,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_tradeTrackerStarting(logger, symbol, null);
|
_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)
|
public static void TradeTrackerStarted(this ILogger logger, string symbol)
|
||||||
|
30
CryptoExchange.Net/Objects/AssetAlias.cs
Normal file
30
CryptoExchange.Net/Objects/AssetAlias.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An alias used by the exchange for an asset commonly known by another name
|
||||||
|
/// </summary>
|
||||||
|
public class AssetAlias
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the asset on the exchange
|
||||||
|
/// </summary>
|
||||||
|
public string ExchangeAssetName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the asset as it's commonly known
|
||||||
|
/// </summary>
|
||||||
|
public string CommonAssetName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public AssetAlias(string exchangeName, string commonName)
|
||||||
|
{
|
||||||
|
ExchangeAssetName = exchangeName;
|
||||||
|
CommonAssetName = commonName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
CryptoExchange.Net/Objects/AssetAliasConfiguration.cs
Normal file
34
CryptoExchange.Net/Objects/AssetAliasConfiguration.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exchange configuration for asset aliases
|
||||||
|
/// </summary>
|
||||||
|
public class AssetAliasConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defined aliases
|
||||||
|
/// </summary>
|
||||||
|
public AssetAlias[] Aliases { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Auto convert asset names when using the Shared interfaces. Defaults to true
|
||||||
|
/// </summary>
|
||||||
|
public bool AutoConvertEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map the common name to an exchange name for an asset. If there is no alias the input name is returned
|
||||||
|
/// </summary>
|
||||||
|
public string CommonToExchangeName(string commonName) => !AutoConvertEnabled ? commonName : Aliases.SingleOrDefault(x => x.CommonAssetName == commonName)?.ExchangeAssetName ?? commonName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map the exchange name to a common name for an asset. If there is no alias the input name is returned
|
||||||
|
/// </summary>
|
||||||
|
public string ExchangeToCommonName(string exchangeName) => !AutoConvertEnabled ? exchangeName : Aliases.SingleOrDefault(x => x.ExchangeAssetName == exchangeName)?.CommonAssetName ?? exchangeName;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class CallResult
|
public class CallResult
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Static success result
|
||||||
|
/// </summary>
|
||||||
|
public static CallResult SuccessResult { get; } = new CallResult(null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An error if the call didn't succeed, will always be filled if Success = false
|
/// An error if the call didn't succeed, will always be filled if Success = false
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -149,7 +154,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public CallResult AsDataless()
|
public CallResult AsDataless()
|
||||||
{
|
{
|
||||||
return new CallResult(null);
|
return SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -161,6 +166,18 @@ namespace CryptoExchange.Net.Objects
|
|||||||
return new CallResult(error);
|
return new CallResult(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copy the CallResult to a new data type
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="K">The new type</typeparam>
|
||||||
|
/// <param name="data">The data</param>
|
||||||
|
/// <param name="error">The error returned</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public CallResult<K> AsErrorWithData<K>(Error error, K data)
|
||||||
|
{
|
||||||
|
return new CallResult<K>(data, OriginalData, error);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -192,7 +209,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The headers sent with the request
|
/// The headers sent with the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
public KeyValuePair<string, string[]>[]? RequestHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The request id
|
/// The request id
|
||||||
@ -209,6 +226,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? RequestBody { get; set; }
|
public string? RequestBody { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The original data returned by the call, only available when `OutputOriginalData` is set to `true` in the client options
|
||||||
|
/// </summary>
|
||||||
|
public string? OriginalData { get; internal set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
/// The status code of the response. Note that a OK status does not always indicate success, check the Success parameter for this.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -217,7 +239,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The response headers
|
/// The response headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
public KeyValuePair<string, string[]>[]? ResponseHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time between sending the request and receiving the response
|
/// The time between sending the request and receiving the response
|
||||||
@ -227,30 +249,23 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
|
||||||
/// <param name="responseHeaders"></param>
|
|
||||||
/// <param name="responseTime"></param>
|
|
||||||
/// <param name="requestId"></param>
|
|
||||||
/// <param name="requestUrl"></param>
|
|
||||||
/// <param name="requestBody"></param>
|
|
||||||
/// <param name="requestMethod"></param>
|
|
||||||
/// <param name="requestHeaders"></param>
|
|
||||||
/// <param name="error"></param>
|
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
|
string? originalData,
|
||||||
int? requestId,
|
int? requestId,
|
||||||
string? requestUrl,
|
string? requestUrl,
|
||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
KeyValuePair<string, string[]>[]? requestHeaders,
|
||||||
Error? error) : base(error)
|
Error? error) : base(error)
|
||||||
{
|
{
|
||||||
ResponseStatusCode = code;
|
ResponseStatusCode = code;
|
||||||
ResponseHeaders = responseHeaders;
|
ResponseHeaders = responseHeaders;
|
||||||
ResponseTime = responseTime;
|
ResponseTime = responseTime;
|
||||||
RequestId = requestId;
|
RequestId = requestId;
|
||||||
|
OriginalData = originalData;
|
||||||
|
|
||||||
RequestUrl = requestUrl;
|
RequestUrl = requestUrl;
|
||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
@ -271,7 +286,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public WebCallResult AsError(Error error)
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -343,7 +358,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The headers sent with the request
|
/// The headers sent with the request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? RequestHeaders { get; set; }
|
public KeyValuePair<string, string[]>[]? RequestHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The request id
|
/// The request id
|
||||||
@ -373,7 +388,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The response headers
|
/// The response headers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
|
public KeyValuePair<string, string[]>[]? ResponseHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time between sending the request and receiving the response
|
/// The time between sending the request and receiving the response
|
||||||
@ -403,7 +418,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="error"></param>
|
/// <param name="error"></param>
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
HttpStatusCode? code,
|
HttpStatusCode? code,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? responseHeaders,
|
KeyValuePair<string, string[]>[]? responseHeaders,
|
||||||
TimeSpan? responseTime,
|
TimeSpan? responseTime,
|
||||||
long? responseLength,
|
long? responseLength,
|
||||||
string? originalData,
|
string? originalData,
|
||||||
@ -411,7 +426,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
string? requestUrl,
|
string? requestUrl,
|
||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
KeyValuePair<string, string[]>[]? requestHeaders,
|
||||||
ResultDataSource dataSource,
|
ResultDataSource dataSource,
|
||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error) : base(data, originalData, error)
|
Error? error) : base(data, originalData, error)
|
||||||
@ -435,7 +450,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult AsDataless()
|
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);
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy as a dataless result
|
/// Copy as a dataless result
|
||||||
@ -443,7 +458,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult AsDatalessError(Error error)
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -474,6 +489,18 @@ namespace CryptoExchange.Net.Objects
|
|||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copy the WebCallResult to a new data type
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="K">The new type</typeparam>
|
||||||
|
/// <param name="data">The data</param>
|
||||||
|
/// <param name="error">The error returned</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public new WebCallResult<K> AsErrorWithData<K>(Error error, K data)
|
||||||
|
{
|
||||||
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, error);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
/// Copy the WebCallResult to an ExchangeWebResult of a new data type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -553,7 +580,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
if (ResponseLength != null)
|
if (ResponseLength != null)
|
||||||
sb.Append($", {ResponseLength} bytes");
|
sb.Append($", {ResponseLength} bytes");
|
||||||
if (ResponseTime != null)
|
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();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
@ -18,21 +18,18 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The data which caused the error
|
/// Underlying exception
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public object? Data { get; set; }
|
public Exception? Exception { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected Error (int? code, string message, Exception? exception)
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected Error(int? code, string message, object? data)
|
|
||||||
{
|
{
|
||||||
Code = code;
|
Code = code;
|
||||||
Message = message;
|
Message = message;
|
||||||
Data = data;
|
Exception = exception;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -41,7 +38,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public override string ToString()
|
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
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public CantConnectError(Exception? exception) : base(null, "Can't connect to the server", exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
/// <summary>
|
||||||
protected CantConnectError(int? code, string message, object? data) : base(code, message, data) { }
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
protected CantConnectError(int? code, string message, Exception? exception) : base(code, message, exception) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -77,10 +76,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected NoApiCredentialsError(int? code, string message, Exception? exception) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected NoApiCredentialsError(int? code, string message, object? data) : base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -91,25 +87,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
public ServerError(string message) : base(null, message, null) { }
|
||||||
/// <param name="data"></param>
|
|
||||||
public ServerError(string message, object? data = null) : base(null, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public ServerError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
public ServerError(int code, string message, object? data = null) : base(code, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="code"></param>
|
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected ServerError(int? code, string message, object? data) : base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -120,25 +103,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
public WebError(string message, Exception? exception = null) : base(null, message, exception) { }
|
||||||
/// <param name="data"></param>
|
|
||||||
public WebError(string message, object? data = null) : base(null, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public WebError(int code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
public WebError(int code, string message, object? data = null) : base(code, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="code"></param>
|
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected WebError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -149,17 +119,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The error message</param>
|
public DeserializeError(string message, Exception? exception = null) : base(null, message, exception) { }
|
||||||
/// <param name="data">The data which caused the error</param>
|
|
||||||
public DeserializeError(string message, object? data) : base(null, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected DeserializeError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected DeserializeError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -170,17 +135,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">Error message</param>
|
public UnknownError(string message, Exception? exception = null) : base(null, message, exception) { }
|
||||||
/// <param name="data">Error data</param>
|
|
||||||
public UnknownError(string message, object? data = null) : base(null, message, data) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected UnknownError(int? code, string message, Exception? exception = null): base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected UnknownError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -191,16 +151,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
|
||||||
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
public ArgumentError(string message) : base(null, "Invalid parameter: " + message, null) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected ArgumentError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected ArgumentError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -216,10 +172,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected BaseRateLimitError(int? code, string message, Exception? exception) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected BaseRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -236,10 +189,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected ClientRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected ClientRateLimitError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -250,16 +200,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
public ServerRateLimitError(string? message = null, Exception? exception = null) : base(null, "Server rate limit exceeded" + (message?.Length > 0 ? " : " + message : null), exception) { }
|
||||||
public ServerRateLimitError(string message) : base(null, "Server rate limit exceeded: " + message, null) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected ServerRateLimitError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected ServerRateLimitError(int? code, string message, object? data) : base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -270,15 +216,12 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CancellationRequestedError() : base(null, "Cancellation requested", null) { }
|
public CancellationRequestedError(Exception? exception = null) : base(null, "Cancellation requested", exception) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
public CancellationRequestedError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
public CancellationRequestedError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -289,15 +232,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message"></param>
|
public InvalidOperationError(string message, Exception? exception = null) : base(null, message, exception) { }
|
||||||
public InvalidOperationError(string message) : base(null, message, null) { }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="code"></param>
|
protected InvalidOperationError(int? code, string message, Exception? exception = null) : base(code, message, exception) { }
|
||||||
/// <param name="message"></param>
|
|
||||||
/// <param name="data"></param>
|
|
||||||
protected InvalidOperationError(int? code, string message, object? data): base(code, message, data) { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
public T Set<T>(T targetOptions) where T: LibraryOptions<TRestOptions, TSocketOptions, TApiCredentials, TEnvironment>
|
||||||
{
|
{
|
||||||
targetOptions.ApiCredentials = ApiCredentials;
|
targetOptions.ApiCredentials = (TApiCredentials?)ApiCredentials?.Copy();
|
||||||
targetOptions.Environment = Environment;
|
targetOptions.Environment = Environment;
|
||||||
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
targetOptions.SocketClientLifeTime = SocketClientLifeTime;
|
||||||
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
targetOptions.Rest = Rest.Set(targetOptions.Rest);
|
||||||
|
@ -94,7 +94,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Trade environment. Contains info about URL's to use to connect to the API. To swap environment select another environment for
|
/// 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`
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#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; }
|
public TEnvironment Environment { get; set; }
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
@ -173,11 +174,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
#if NET5_0_OR_GREATER
|
||||||
/// <param name="value"></param>
|
public void AddEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
|
||||||
|
#else
|
||||||
public void AddEnum<T>(string key, T value)
|
public void AddEnum<T>(string key, T value)
|
||||||
|
#endif
|
||||||
|
where T : struct, Enum
|
||||||
{
|
{
|
||||||
Add(key, EnumConverter.GetString(value)!);
|
Add(key, EnumConverter<T>.GetString(value)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -185,9 +189,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public void AddEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T value)
|
||||||
|
#else
|
||||||
public void AddEnumAsInt<T>(string key, T value)
|
public void AddEnumAsInt<T>(string key, T value)
|
||||||
|
#endif
|
||||||
|
where T : struct, Enum
|
||||||
{
|
{
|
||||||
var stringVal = EnumConverter.GetString(value)!;
|
var stringVal = EnumConverter<T>.GetString(value)!;
|
||||||
Add(key, int.Parse(stringVal)!);
|
Add(key, int.Parse(stringVal)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,22 +205,30 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
/// <param name="key"></param>
|
||||||
/// <param name="value"></param>
|
/// <param name="value"></param>
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public void AddOptionalEnum<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
|
||||||
|
#else
|
||||||
public void AddOptionalEnum<T>(string key, T? value)
|
public void AddOptionalEnum<T>(string key, T? value)
|
||||||
|
#endif
|
||||||
|
where T : struct, Enum
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
Add(key, EnumConverter.GetString(value));
|
Add(key, EnumConverter<T>.GetString(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
|
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />. Not added if value is null
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key"></param>
|
#if NET5_0_OR_GREATER
|
||||||
/// <param name="value"></param>
|
public void AddOptionalEnumAsInt<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, T? value)
|
||||||
|
#else
|
||||||
public void AddOptionalEnumAsInt<T>(string key, T? value)
|
public void AddOptionalEnumAsInt<T>(string key, T? value)
|
||||||
|
#endif
|
||||||
|
where T : struct, Enum
|
||||||
{
|
{
|
||||||
if (value != null)
|
if (value != null)
|
||||||
{
|
{
|
||||||
var stringVal = EnumConverter.GetString(value);
|
var stringVal = EnumConverter<T>.GetString(value);
|
||||||
Add(key, int.Parse(stringVal));
|
Add(key, int.Parse(stringVal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? KeepAliveInterval { get; set; }
|
public TimeSpan? KeepAliveInterval { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout for keep alive response messages
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan? KeepAliveTimeout { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rate limiter for the socket connection
|
/// The rate limiter for the socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -56,7 +56,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
if (!IsEnabled(logLevel))
|
if (!IsEnabled(logLevel))
|
||||||
return;
|
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);
|
Trace.WriteLine(logMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,11 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of changed/new asks
|
/// List of changed/new asks
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// List of changed/new bids
|
/// List of changed/new bids
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,16 +8,16 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
public long StartUpdateId { get; set; }
|
public long StartUpdateId { get; set; }
|
||||||
public long EndUpdateId { get; set; }
|
public long EndUpdateId { get; set; }
|
||||||
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||||
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class InitialOrderBookItem
|
internal class InitialOrderBookItem
|
||||||
{
|
{
|
||||||
public long StartUpdateId { get; set; }
|
public long StartUpdateId { get; set; }
|
||||||
public long EndUpdateId { get; set; }
|
public long EndUpdateId { get; set; }
|
||||||
public IEnumerable<ISymbolOrderBookEntry> Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
public ISymbolOrderBookEntry[] Bids { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||||
public IEnumerable<ISymbolOrderBookEntry> Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
public ISymbolOrderBookEntry[] Asks { get; set; } = Array.Empty<ISymbolOrderBookEntry>();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class ChecksumItem
|
internal class ChecksumItem
|
||||||
|
@ -123,7 +123,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged;
|
public event Action<(ISymbolOrderBookEntry BestBid, ISymbolOrderBookEntry BestAsk)>? OnBestOffersChanged;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public event Action<(IEnumerable<ISymbolOrderBookEntry> Bids, IEnumerable<ISymbolOrderBookEntry> Asks)>? OnOrderBookUpdate;
|
public event Action<(ISymbolOrderBookEntry[] Bids, ISymbolOrderBookEntry[] Asks)>? OnOrderBookUpdate;
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public DateTime UpdateTime { get; private set; }
|
public DateTime UpdateTime { get; private set; }
|
||||||
@ -135,27 +135,27 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
public int BidCount { get; private set; }
|
public int BidCount { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<ISymbolOrderBookEntry> Asks
|
public ISymbolOrderBookEntry[] Asks
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_bookLock)
|
lock (_bookLock)
|
||||||
return _asks.Select(a => a.Value).ToList();
|
return _asks.Select(a => a.Value).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public IEnumerable<ISymbolOrderBookEntry> Bids
|
public ISymbolOrderBookEntry[] Bids
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_bookLock)
|
lock (_bookLock)
|
||||||
return _bids.Select(a => a.Value).ToList();
|
return _bids.Select(a => a.Value).ToArray();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public (IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks) Book
|
public (ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks) Book
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@ -412,7 +412,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param>
|
/// <param name="orderBookSequenceNumber">The last update sequence number until which the snapshot is in sync</param>
|
||||||
/// <param name="askList">List of asks</param>
|
/// <param name="askList">List of asks</param>
|
||||||
/// <param name="bidList">List of bids</param>
|
/// <param name="bidList">List of bids</param>
|
||||||
protected void SetInitialOrderBook(long orderBookSequenceNumber, IEnumerable<ISymbolOrderBookEntry> bidList, IEnumerable<ISymbolOrderBookEntry> askList)
|
protected void SetInitialOrderBook(long orderBookSequenceNumber, ISymbolOrderBookEntry[] bidList, ISymbolOrderBookEntry[] askList)
|
||||||
{
|
{
|
||||||
_processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList });
|
_processQueue.Enqueue(new InitialOrderBookItem { StartUpdateId = orderBookSequenceNumber, EndUpdateId = orderBookSequenceNumber, Asks = askList, Bids = bidList });
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
@ -424,7 +424,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <param name="updateId">The sequence number</param>
|
/// <param name="updateId">The sequence number</param>
|
||||||
/// <param name="bids">List of updated/new bids</param>
|
/// <param name="bids">List of updated/new bids</param>
|
||||||
/// <param name="asks">List of updated/new asks</param>
|
/// <param name="asks">List of updated/new asks</param>
|
||||||
protected void UpdateOrderBook(long updateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
protected void UpdateOrderBook(long updateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks)
|
||||||
{
|
{
|
||||||
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids });
|
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = updateId, EndUpdateId = updateId, Asks = asks, Bids = bids });
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
@ -437,7 +437,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// <param name="lastUpdateId">The sequence number of the last update</param>
|
/// <param name="lastUpdateId">The sequence number of the last update</param>
|
||||||
/// <param name="bids">List of updated/new bids</param>
|
/// <param name="bids">List of updated/new bids</param>
|
||||||
/// <param name="asks">List of updated/new asks</param>
|
/// <param name="asks">List of updated/new asks</param>
|
||||||
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, IEnumerable<ISymbolOrderBookEntry> bids, IEnumerable<ISymbolOrderBookEntry> asks)
|
protected void UpdateOrderBook(long firstUpdateId, long lastUpdateId, ISymbolOrderBookEntry[] bids, ISymbolOrderBookEntry[] asks)
|
||||||
{
|
{
|
||||||
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids });
|
_processQueue.Enqueue(new ProcessQueueItem { StartUpdateId = firstUpdateId, EndUpdateId = lastUpdateId, Asks = asks, Bids = bids });
|
||||||
_queueEvent.Set();
|
_queueEvent.Set();
|
||||||
@ -448,7 +448,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="bids">List of updated/new bids</param>
|
/// <param name="bids">List of updated/new bids</param>
|
||||||
/// <param name="asks">List of updated/new asks</param>
|
/// <param name="asks">List of updated/new asks</param>
|
||||||
protected void UpdateOrderBook(IEnumerable<ISymbolOrderSequencedBookEntry> bids, IEnumerable<ISymbolOrderSequencedBookEntry> 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 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);
|
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;
|
UpdateTime = DateTime.UtcNow;
|
||||||
_logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.EndUpdateId);
|
_logger.OrderBookDataSet(Api, Symbol, BidCount, AskCount, item.EndUpdateId);
|
||||||
CheckProcessBuffer();
|
CheckProcessBuffer();
|
||||||
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
|
OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray()));
|
||||||
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
|
OnBestOffersChanged?.Invoke((BestBid, BestAsk));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -745,7 +745,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
OnOrderBookUpdate?.Invoke((item.Bids, item.Asks));
|
OnOrderBookUpdate?.Invoke((item.Bids.ToArray(), item.Asks.ToArray()));
|
||||||
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
CheckBestOffersChanged(prevBestBid, prevBestAsk);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,11 +46,11 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
{
|
{
|
||||||
return await CheckGuardsAsync(_guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, keySuffix, ct).ConfigureAwait(false);
|
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
|
// The semaphore has already been released if the task was cancelled
|
||||||
release = false;
|
release = false;
|
||||||
return new CallResult(new CancellationRequestedError());
|
return new CallResult(new CancellationRequestedError(tce));
|
||||||
}
|
}
|
||||||
finally
|
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);
|
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
|
// The semaphore has already been released if the task was cancelled
|
||||||
release = false;
|
release = false;
|
||||||
return new CallResult(new CancellationRequestedError());
|
return new CallResult(new CancellationRequestedError(tce));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -146,7 +146,7 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new CallResult(null);
|
return CallResult.SuccessResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -67,9 +67,9 @@ namespace CryptoExchange.Net.Requests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Dictionary<string, IEnumerable<string>> GetHeaders()
|
public KeyValuePair<string, string[]>[] GetHeaders()
|
||||||
{
|
{
|
||||||
return _request.Headers.ToDictionary(h => h.Key, h => h.Value);
|
return _request.Headers.Select(h => new KeyValuePair<string, string[]>(h.Key, h.Value.ToArray())).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -24,7 +25,7 @@ namespace CryptoExchange.Net.Requests
|
|||||||
public long? ContentLength => _response.Content.Headers.ContentLength;
|
public long? ContentLength => _response.Content.Headers.ContentLength;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders => _response.Headers;
|
public KeyValuePair<string, string[]>[] ResponseHeaders => _response.Headers.Select(x => new KeyValuePair<string, string[]>(x.Key, x.Value.ToArray())).ToArray();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create response for a http response message
|
/// Create response for a http response message
|
||||||
|
21
CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs
Normal file
21
CryptoExchange.Net/SharedApis/Enums/SharedTpSlSide.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Take Profit / Stop Loss side
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTpSlSide
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Take profit
|
||||||
|
/// </summary>
|
||||||
|
TakeProfit,
|
||||||
|
/// <summary>
|
||||||
|
/// Stop loss
|
||||||
|
/// </summary>
|
||||||
|
StopLoss
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The order direction when order trigger parameters are reached
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTriggerOrderDirection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Enter, Buy for Spot and long futures positions, Sell for short futures positions
|
||||||
|
/// </summary>
|
||||||
|
Enter,
|
||||||
|
/// <summary>
|
||||||
|
/// Exit, Sell for Spot and long futures positions, Buy for short futures positions
|
||||||
|
/// </summary>
|
||||||
|
Exit
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger order status
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTriggerOrderStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order is active
|
||||||
|
/// </summary>
|
||||||
|
Active,
|
||||||
|
/// <summary>
|
||||||
|
/// Order has been filled
|
||||||
|
/// </summary>
|
||||||
|
Filled,
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger canceled, can be user cancelation or system cancelation due to an error
|
||||||
|
/// </summary>
|
||||||
|
CanceledOrRejected,
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger order has been triggered. Resulting order might be filled or not.
|
||||||
|
/// </summary>
|
||||||
|
Triggered
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Price direction for trigger order
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTriggerPriceDirection
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger when the price goes below the specified trigger price
|
||||||
|
/// </summary>
|
||||||
|
PriceBelow,
|
||||||
|
/// <summary>
|
||||||
|
/// Trigger when the price goes above the specified trigger price
|
||||||
|
/// </summary>
|
||||||
|
PriceAbove
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Price direction for trigger order
|
||||||
|
/// </summary>
|
||||||
|
public enum SharedTriggerPriceType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Last traded price
|
||||||
|
/// </summary>
|
||||||
|
LastPrice,
|
||||||
|
/// <summary>
|
||||||
|
/// Mark price
|
||||||
|
/// </summary>
|
||||||
|
MarkPrice,
|
||||||
|
/// <summary>
|
||||||
|
/// Index price
|
||||||
|
/// </summary>
|
||||||
|
IndexPrice
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,6 @@ namespace CryptoExchange.Net.SharedApis
|
|||||||
/// <param name="request">Request info</param>
|
/// <param name="request">Request info</param>
|
||||||
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
/// <param name="nextPageToken">The pagination token from the previous request to continue pagination</param>
|
||||||
/// <param name="ct">Cancellation token</param>
|
/// <param name="ct">Cancellation token</param>
|
||||||
Task<ExchangeWebResult<IEnumerable<SharedFundingRate>>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
Task<ExchangeWebResult<SharedFundingRate[]>> GetFundingRateHistoryAsync(GetFundingRateHistoryRequest request, INextPageToken? nextPageToken = null, CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.SharedApis
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for managing futures orders using a client order id
|
||||||
|
/// </summary>
|
||||||
|
public interface IFuturesOrderClientIdRestClient : ISharedClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Futures get order by client order id request options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<GetOrderRequest> GetFuturesOrderByClientOrderIdOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get info on a specific futures order using a client order id
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
Task<ExchangeWebResult<SharedFuturesOrder>> GetFuturesOrderByClientOrderIdAsync(GetOrderRequest request, CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Futures cancel order by client order id request options
|
||||||
|
/// </summary>
|
||||||
|
EndpointOptions<CancelOrderRequest> CancelFuturesOrderByClientOrderIdOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Cancel a futures order using client order id
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">Request info</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
|
Task<ExchangeWebResult<SharedId>> CancelFuturesOrderByClientOrderIdAsync(CancelOrderRequest request, CancellationToken ct = default);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user