mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-12-17 11:37:33 +00:00
Performance update: Authentication Added Ed25519 signing support for NET8.0 and newer Added static methods on ApiCredentials to create credentials of a specific type Added static ApiCredentials.ReadFromFile method to read a key from file Added required abstract SupportedCredentialTypes property on AuthenticationProvider base class General Performance Added checks before logging statements to prevent overhead of building the log string if logging is not needed Added ExchangeHelpers.ProcessQueuedAsync method to process updates async Replaced locking object types from object to Lock in NET9.0 and newer Replaced some Task response types with ValueTask to prevent allocation overhead on hot paths Updated Json ArrayConverter to reduce some allocation overhead Updated Json BoolConverter to prevent boxing Updated Json DateTimeConverter to prevent boxing Updated Json EnumConverter caching to reduce lookup overhead Updated ExtensionMethods.CreateParamString to reduce allocations Updated ExtensionMethods.AppendPath to reduce overhead REST Refactored REST message processing to separate IRestMessageHandler instance Split RestApiClient.PrepareAsync into CheckTimeSync and RateLimitAsync Updated IRequest.Accept type from string to MediaTypeWithQualityHeaderValue to prevent creation on each request Updated IRequest.GetHeaders response type from KeyValuePair<string, string[]>[] to HttpRequestHeaders to prevent additional mapping Updated IResponse.ResponseHeaders type from KeyValuePair<string, string[]>[] to HttpResponseHeaders to prevent additional mapping Updated WebCallResult RequestHeaders and ResponseHeaders types to HttpRequestHeaders and HttpResponseHeaders Removed unnecessary empty dictionary initializations for each request Removed CallResult creation in internal methods to prevent having to create multiple versions for different result types Socket Added HighPerformance websocket client implementation which significantly reduces memory overhead and improves speed but with certain limitations Added MaxIndividualSubscriptionsPerConnection setting in SocketApiClient to limit the number of individual stream subscriptions on a connection Added SocketIndividualSubscriptionCombineTarget option to set the target number of individual stream subscriptions per connection Added new websocket message handling logic which is faster and reduces memory allocation Added UseUpdatedDeserialization option to toggle between updated deserialization and old deserialization Added Exchange property to DataEvent to prevent additional mapping overhead for Shared apis Refactored message callback to be sync instead of async to prevent async overhead Refactored CryptoExchangeWebSocketClient.IncomingKbps calculation to significantly reduce overhead Moved websocket client creation from SocketApiClient to SocketConnection Removed DataEvent.As and DataEvent.ToCallResult methods in favor of single ToType method Removed DataEvent creation on lower levels to prevent having to create multiple versions for different result types Removed Subscription<TSubResponse, TUnsubResponse> as its no longer used Other Added null check to ParameterCollection for required parameters Added Net10.0 target framework Updated dependency versions Updated Shared asset aliases check to be culture invariant Updated Error string representation Updated some namespaces Updated SymbolOrderBook processing of buffered updates to prevent additional allocation Removed ExchangeEvent type which is no longer needed Removed unused usings
441 lines
16 KiB
C#
441 lines
16 KiB
C#
using CryptoExchange.Net.Attributes;
|
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
using System.Text.Json;
|
|
using NUnit.Framework;
|
|
using System;
|
|
using System.Text.Json.Serialization;
|
|
using CryptoExchange.Net.Converters;
|
|
using CryptoExchange.Net.SharedApis;
|
|
|
|
namespace CryptoExchange.Net.UnitTests
|
|
{
|
|
[TestFixture()]
|
|
public class SystemTextJsonConverterTests
|
|
{
|
|
[TestCase("2021-05-12")]
|
|
[TestCase("20210512")]
|
|
[TestCase("210512")]
|
|
[TestCase("1620777600.000")]
|
|
[TestCase("1620777600000")]
|
|
[TestCase("2021-05-12T00:00:00.000Z")]
|
|
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
|
[TestCase("0.000000", true)]
|
|
[TestCase("0", true)]
|
|
[TestCase("", true)]
|
|
[TestCase(" ", true)]
|
|
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
|
{
|
|
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
|
|
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
|
}
|
|
|
|
[TestCase(1620777600.000)]
|
|
[TestCase(1620777600000d)]
|
|
public void TestDateTimeConverterDouble(double input)
|
|
{
|
|
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
|
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
}
|
|
|
|
[TestCase(1620777600)]
|
|
[TestCase(1620777600000)]
|
|
[TestCase(1620777600000000)]
|
|
[TestCase(1620777600000000000)]
|
|
[TestCase(0, true)]
|
|
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
|
{
|
|
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
|
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
|
}
|
|
|
|
[TestCase(1620777600)]
|
|
[TestCase(1620777600.000)]
|
|
public void TestDateTimeConverterFromSeconds(double input)
|
|
{
|
|
var output = DateTimeConverter.ConvertFromSeconds(input);
|
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
}
|
|
|
|
[Test]
|
|
public void TestDateTimeConverterToSeconds()
|
|
{
|
|
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
Assert.That(output == 1620777600);
|
|
}
|
|
|
|
[TestCase(1620777600000)]
|
|
[TestCase(1620777600000.000)]
|
|
public void TestDateTimeConverterFromMilliseconds(double input)
|
|
{
|
|
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
}
|
|
|
|
[Test]
|
|
public void TestDateTimeConverterToMilliseconds()
|
|
{
|
|
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
Assert.That(output == 1620777600000);
|
|
}
|
|
|
|
[TestCase(1620777600000000)]
|
|
public void TestDateTimeConverterFromMicroseconds(long input)
|
|
{
|
|
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
}
|
|
|
|
[Test]
|
|
public void TestDateTimeConverterToMicroseconds()
|
|
{
|
|
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
Assert.That(output == 1620777600000000);
|
|
}
|
|
|
|
[TestCase(1620777600000000000)]
|
|
public void TestDateTimeConverterFromNanoseconds(long input)
|
|
{
|
|
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
|
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
}
|
|
|
|
[Test]
|
|
public void TestDateTimeConverterToNanoseconds()
|
|
{
|
|
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
|
Assert.That(output == 1620777600000000000);
|
|
}
|
|
|
|
[TestCase()]
|
|
public void TestDateTimeConverterNull()
|
|
{
|
|
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
|
|
Assert.That(output.Time == null);
|
|
}
|
|
|
|
[TestCase(TestEnum.One, "1")]
|
|
[TestCase(TestEnum.Two, "2")]
|
|
[TestCase(TestEnum.Three, "three")]
|
|
[TestCase(TestEnum.Four, "Four")]
|
|
[TestCase(null, null)]
|
|
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
|
{
|
|
var output = EnumConverter.GetString(value);
|
|
Assert.That(output == expected);
|
|
}
|
|
|
|
[TestCase(TestEnum.One, "1")]
|
|
[TestCase(TestEnum.Two, "2")]
|
|
[TestCase(TestEnum.Three, "three")]
|
|
[TestCase(TestEnum.Four, "Four")]
|
|
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
|
{
|
|
var output = EnumConverter.GetString(value);
|
|
Assert.That(output == expected);
|
|
}
|
|
|
|
[TestCase("1", TestEnum.One)]
|
|
[TestCase("2", TestEnum.Two)]
|
|
[TestCase("3", TestEnum.Three)]
|
|
[TestCase("three", TestEnum.Three)]
|
|
[TestCase("Four", TestEnum.Four)]
|
|
[TestCase("four", TestEnum.Four)]
|
|
[TestCase("Four1", null)]
|
|
[TestCase(null, null)]
|
|
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
|
{
|
|
var val = value == null ? "null" : $"\"{value}\"";
|
|
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
Assert.That(output.Value == expected);
|
|
}
|
|
|
|
[TestCase("1", TestEnum.One)]
|
|
[TestCase("2", TestEnum.Two)]
|
|
[TestCase("3", TestEnum.Three)]
|
|
[TestCase("three", TestEnum.Three)]
|
|
[TestCase("Four", TestEnum.Four)]
|
|
[TestCase("four", TestEnum.Four)]
|
|
[TestCase("Four1", TestEnum.One)]
|
|
[TestCase(null, TestEnum.One)]
|
|
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
|
|
{
|
|
var val = value == null ? "null" : $"\"{value}\"";
|
|
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"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", null)]
|
|
[TestCase(null, null)]
|
|
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
|
{
|
|
var result = EnumConverter.ParseString<TestEnum>(value);
|
|
Assert.That(result == expected);
|
|
}
|
|
|
|
[TestCase("1", true)]
|
|
[TestCase("true", true)]
|
|
[TestCase("yes", true)]
|
|
[TestCase("y", true)]
|
|
[TestCase("on", true)]
|
|
[TestCase("-1", false)]
|
|
[TestCase("0", false)]
|
|
[TestCase("n", false)]
|
|
[TestCase("no", false)]
|
|
[TestCase("false", false)]
|
|
[TestCase("off", false)]
|
|
[TestCase("", null)]
|
|
public void TestBoolConverter(string value, bool? expected)
|
|
{
|
|
var val = value == null ? "null" : $"\"{value}\"";
|
|
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
Assert.That(output.Value == expected);
|
|
}
|
|
|
|
[TestCase("1", true)]
|
|
[TestCase("true", true)]
|
|
[TestCase("yes", true)]
|
|
[TestCase("y", true)]
|
|
[TestCase("on", true)]
|
|
[TestCase("-1", false)]
|
|
[TestCase("0", false)]
|
|
[TestCase("n", false)]
|
|
[TestCase("no", false)]
|
|
[TestCase("false", false)]
|
|
[TestCase("off", false)]
|
|
[TestCase("", false)]
|
|
public void TestBoolConverterNotNullable(string value, bool expected)
|
|
{
|
|
var val = value == null ? "null" : $"\"{value}\"";
|
|
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}", SerializerOptions.WithConverters(new SerializationContext()));
|
|
Assert.That(output.Value == expected);
|
|
}
|
|
|
|
[TestCase("1", 1)]
|
|
[TestCase("1.1", 1.1)]
|
|
[TestCase("-1.1", -1.1)]
|
|
[TestCase(null, null)]
|
|
[TestCase("", null)]
|
|
[TestCase("null", null)]
|
|
[TestCase("nan", null)]
|
|
[TestCase("1E+2", 100)]
|
|
[TestCase("1E-2", 0.01)]
|
|
[TestCase("Infinity", 999)] // 999 is workaround for not being able to specify decimal.MinValue
|
|
[TestCase("-Infinity", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
|
[TestCase("80228162514264337593543950335", 999)] // 999 is workaround for not being able to specify decimal.MaxValue
|
|
[TestCase("-80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
|
public void TestDecimalConverterString(string value, decimal? expected)
|
|
{
|
|
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": \""+ value + "\"}");
|
|
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MinValue : expected == 999 ? decimal.MaxValue: expected));
|
|
}
|
|
|
|
[TestCase("1", 1)]
|
|
[TestCase("1.1", 1.1)]
|
|
[TestCase("-1.1", -1.1)]
|
|
[TestCase("null", null)]
|
|
[TestCase("1E+2", 100)]
|
|
[TestCase("1E-2", 0.01)]
|
|
[TestCase("80228162514264337593543950335", -999)] // -999 is workaround for not being able to specify decimal.MaxValue
|
|
public void TestDecimalConverterNumber(string value, decimal? expected)
|
|
{
|
|
var result = JsonSerializer.Deserialize<STJDecimalObject>("{ \"test\": " + value + "}");
|
|
Assert.That(result.Test, Is.EqualTo(expected == -999 ? decimal.MaxValue : expected));
|
|
}
|
|
|
|
[Test()]
|
|
public void TestArrayConverter()
|
|
{
|
|
var data = new Test()
|
|
{
|
|
Prop1 = 2,
|
|
Prop2 = null,
|
|
Prop3 = "123",
|
|
Prop3Again = "123",
|
|
Prop4 = null,
|
|
Prop5 = new Test2
|
|
{
|
|
Prop21 = 3,
|
|
Prop22 = "456"
|
|
},
|
|
Prop6 = new Test3
|
|
{
|
|
Prop31 = 4,
|
|
Prop32 = "789"
|
|
},
|
|
Prop7 = TestEnum.Two,
|
|
TestInternal = new Test
|
|
{
|
|
Prop1 = 10
|
|
},
|
|
Prop8 = new Test3
|
|
{
|
|
Prop31 = 5,
|
|
Prop32 = "101"
|
|
},
|
|
};
|
|
|
|
var options = new JsonSerializerOptions()
|
|
{
|
|
TypeInfoResolver = new SerializationContext()
|
|
};
|
|
var serialized = JsonSerializer.Serialize(data);
|
|
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
|
|
|
Assert.That(deserialized.Prop1, Is.EqualTo(2));
|
|
Assert.That(deserialized.Prop2, Is.Null);
|
|
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
|
|
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
|
|
Assert.That(deserialized.Prop4, Is.Null);
|
|
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
|
|
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
|
|
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
|
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
|
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
|
Assert.That(deserialized.TestInternal.Prop1, Is.EqualTo(10));
|
|
Assert.That(deserialized.Prop8.Prop31, Is.EqualTo(5));
|
|
Assert.That(deserialized.Prop8.Prop32, Is.EqualTo("101"));
|
|
}
|
|
|
|
[TestCase(TradingMode.Spot, "ETH", "USDT", null)]
|
|
[TestCase(TradingMode.PerpetualLinear, "ETH", "USDT", null)]
|
|
[TestCase(TradingMode.DeliveryLinear, "ETH", "USDT", 1748432430)]
|
|
public void TestSharedSymbolConversion(TradingMode tradingMode, string baseAsset, string quoteAsset, int? deliverTime)
|
|
{
|
|
DateTime? time = deliverTime == null ? null : DateTimeConverter.ParseFromDouble(deliverTime.Value);
|
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, time);
|
|
|
|
var serialized = JsonSerializer.Serialize(symbol);
|
|
var restored = JsonSerializer.Deserialize<SharedSymbol>(serialized);
|
|
|
|
Assert.That(restored.TradingMode, Is.EqualTo(symbol.TradingMode));
|
|
Assert.That(restored.BaseAsset, Is.EqualTo(symbol.BaseAsset));
|
|
Assert.That(restored.QuoteAsset, Is.EqualTo(symbol.QuoteAsset));
|
|
Assert.That(restored.DeliverTime, Is.EqualTo(symbol.DeliverTime));
|
|
}
|
|
|
|
[TestCase(0.1, null, null)]
|
|
[TestCase(0.1, 0.1, null)]
|
|
[TestCase(0.1, 0.1, 0.1)]
|
|
[TestCase(null, 0.1, null)]
|
|
[TestCase(null, 0.1, 0.1)]
|
|
public void TestSharedQuantityConversion(double? baseQuantity, double? quoteQuantity, double? contractQuantity)
|
|
{
|
|
var symbol = new SharedOrderQuantity((decimal?)baseQuantity, (decimal?)quoteQuantity, (decimal?)contractQuantity);
|
|
|
|
var serialized = JsonSerializer.Serialize(symbol);
|
|
var restored = JsonSerializer.Deserialize<SharedOrderQuantity>(serialized);
|
|
|
|
Assert.That(restored.QuantityInBaseAsset, Is.EqualTo(symbol.QuantityInBaseAsset));
|
|
Assert.That(restored.QuantityInQuoteAsset, Is.EqualTo(symbol.QuantityInQuoteAsset));
|
|
Assert.That(restored.QuantityInContracts, Is.EqualTo(symbol.QuantityInContracts));
|
|
}
|
|
}
|
|
|
|
public class STJDecimalObject
|
|
{
|
|
[JsonConverter(typeof(DecimalConverter))]
|
|
[JsonPropertyName("test")]
|
|
public decimal? Test { get; set; }
|
|
}
|
|
|
|
public class STJTimeObject
|
|
{
|
|
[JsonConverter(typeof(DateTimeConverter))]
|
|
[JsonPropertyName("time")]
|
|
public DateTime? Time { get; set; }
|
|
}
|
|
|
|
public class STJEnumObject
|
|
{
|
|
public TestEnum? Value { get; set; }
|
|
}
|
|
|
|
public class NotNullableSTJEnumObject
|
|
{
|
|
public TestEnum Value { get; set; }
|
|
}
|
|
|
|
public class STJBoolObject
|
|
{
|
|
public bool? Value { get; set; }
|
|
}
|
|
|
|
public class NotNullableSTJBoolObject
|
|
{
|
|
public bool Value { get; set; }
|
|
}
|
|
|
|
[JsonConverter(typeof(ArrayConverter<Test>))]
|
|
record Test
|
|
{
|
|
[ArrayProperty(0)]
|
|
public int Prop1 { get; set; }
|
|
[ArrayProperty(1)]
|
|
public int? Prop2 { get; set; }
|
|
[ArrayProperty(2)]
|
|
public string Prop3 { get; set; }
|
|
[ArrayProperty(2)]
|
|
public string Prop3Again { get; set; }
|
|
[ArrayProperty(3)]
|
|
public string Prop4 { get; set; }
|
|
[ArrayProperty(4)]
|
|
public Test2 Prop5 { get; set; }
|
|
[ArrayProperty(5)]
|
|
public Test3 Prop6 { get; set; }
|
|
[ArrayProperty(6), JsonConverter(typeof(EnumConverter<TestEnum>))]
|
|
public TestEnum? Prop7 { get; set; }
|
|
[ArrayProperty(7)]
|
|
public Test TestInternal { get; set; }
|
|
[ArrayProperty(8), JsonConversion]
|
|
public Test3 Prop8 { get; set; }
|
|
}
|
|
|
|
[JsonConverter(typeof(ArrayConverter<Test2>))]
|
|
record Test2
|
|
{
|
|
[ArrayProperty(0)]
|
|
public int Prop21 { get; set; }
|
|
[ArrayProperty(1)]
|
|
public string Prop22 { get; set; }
|
|
}
|
|
|
|
record Test3
|
|
{
|
|
[JsonPropertyName("prop31")]
|
|
public int Prop31 { get; set; }
|
|
[JsonPropertyName("prop32")]
|
|
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
|
|
{
|
|
}
|
|
}
|