1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2026-04-13 00:22:22 +00:00

Compare commits

..

9 Commits

Author SHA1 Message Date
Jkorf
9ae1263662 Added Weex to examples 2026-04-10 12:57:37 +02:00
Jkorf
ee30a6716e Added Weex reference 2026-04-10 11:54:32 +02:00
Jkorf
a4b7b273dc Updated to version 11.1.1 2026-04-10 10:05:14 +02:00
Jkorf
c92eeb2ec8 Added Weex client reference 2026-04-10 10:03:42 +02:00
Jkorf
4d4b0576ee Fix for rate limiter resetting 2026-04-10 10:01:13 +02:00
Jkorf
9add5e0adc Added Reset functionality to rate limiter implementation, added reset of rate limit per connection when connection is disconnected 2026-04-10 09:39:19 +02:00
Jkorf
93d92beea6 Updated to version 11.1.0 2026-04-09 10:41:33 +02:00
Jkorf
a955ccbc5c Added check to EnumConverter for large enums to prevent optimistic checking 2026-04-08 14:52:26 +02:00
Jan Korf
4e2dc564dd
Socket routing improvements, unit test cleanup (#276)
Updated WebSocket message routing improving performance for scenarios with multiple different subscriptions and topics
Added AddCommaSeparated helper for Enum value arrays to ParameterCollection
Improved EnumConverter performance and removed string allocation for happy path
Fixed CreateParamString extension method for ArrayParametersSerialization.Json
Fixed Shared GetOrderBookOptions and GetRecentTradeOptions base validations not being called
2026-04-08 13:04:18 +02:00
86 changed files with 3512 additions and 1641 deletions

View File

@ -96,7 +96,7 @@ namespace CryptoExchange.Net.UnitTests
waiters.Add(evnt.WaitAsync()); waiters.Add(evnt.WaitAsync());
} }
List<bool> results = null; List<bool>? results = null;
var resultsWaiter = Task.Run(async () => var resultsWaiter = Task.Run(async () =>
{ {
await Task.WhenAll(waiters); await Task.WhenAll(waiters);
@ -112,7 +112,7 @@ namespace CryptoExchange.Net.UnitTests
await resultsWaiter; await resultsWaiter;
Assert.That(10 == results.Count(r => r)); Assert.That(10 == results?.Count(r => r));
} }
[Test] [Test]

View File

@ -0,0 +1,43 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using CryptoExchange.Net;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Converters.SystemTextJson;
namespace CryptoExchange.Net.UnitTests
{
internal class BodySerializationTests
{
[Test]
public void ToFormData_SerializesBasicValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = parameters.ToFormData();
Assert.That(parameterString, Is.EqualTo("a=1&b=2&c=True"));
}
[Test]
public void JsonSerializer_SerializesBasicValuesCorrectly()
{
var serializer = new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = serializer.Serialize(parameters);
Assert.That(parameterString, Is.EqualTo("{\"a\":\"1\",\"b\":2,\"c\":true}"));
}
}
}

View File

@ -3,7 +3,6 @@ using CryptoExchange.Net.Objects.Errors;
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy; using NUnit.Framework.Legacy;
using System; using System;
using System.Collections.Generic;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
@ -17,7 +16,7 @@ namespace CryptoExchange.Net.UnitTests
{ {
var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown)); var result = new CallResult(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError"); ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
} }
@ -37,7 +36,7 @@ namespace CryptoExchange.Net.UnitTests
{ {
var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown)); var result = new CallResult<object>(new ServerError("TestError", ErrorInfo.Unknown));
ClassicAssert.AreSame(result.Error.ErrorCode, "TestError"); ClassicAssert.AreSame(result.Error!.ErrorCode, "TestError");
ClassicAssert.IsNull(result.Data); ClassicAssert.IsNull(result.Data);
ClassicAssert.IsFalse(result); ClassicAssert.IsFalse(result);
ClassicAssert.IsFalse(result.Success); ClassicAssert.IsFalse(result.Success);
@ -74,7 +73,7 @@ namespace CryptoExchange.Net.UnitTests
var asResult = result.As<TestObject2>(default); var asResult = result.As<TestObject2>(default);
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError"); ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@ -87,7 +86,7 @@ namespace CryptoExchange.Net.UnitTests
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown)); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2"); ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@ -100,7 +99,7 @@ namespace CryptoExchange.Net.UnitTests
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown)); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
ClassicAssert.AreSame(asResult.Error.ErrorCode, "TestError2"); ClassicAssert.AreSame(asResult.Error!.ErrorCode, "TestError2");
ClassicAssert.IsNull(asResult.Data); ClassicAssert.IsNull(asResult.Data);
ClassicAssert.IsFalse(asResult); ClassicAssert.IsFalse(asResult);
ClassicAssert.IsFalse(asResult.Success); ClassicAssert.IsFalse(asResult.Success);
@ -127,7 +126,7 @@ namespace CryptoExchange.Net.UnitTests
var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown)); var asResult = result.AsError<TestObject2>(new ServerError("TestError2", ErrorInfo.Unknown));
ClassicAssert.IsNotNull(asResult.Error); ClassicAssert.IsNotNull(asResult.Error);
Assert.That(asResult.Error.ErrorCode == "TestError2"); Assert.That(asResult.Error!.ErrorCode == "TestError2");
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK); Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1)); Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
Assert.That(asResult.RequestUrl == "https://test.com/api"); Assert.That(asResult.RequestUrl == "https://test.com/api");

View File

@ -1,23 +1,22 @@
using NUnit.Framework; using NUnit.Framework;
using NUnit.Framework.Legacy;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests.ClientTests
{ {
[TestFixture()] [TestFixture()]
public class BaseClientTests public class BaseClientTests
{ {
[TestCase] //[TestCase]
public void DeserializingValidJson_Should_GiveSuccessfulResult() //public void DeserializingValidJson_Should_GiveSuccessfulResult()
{ //{
// arrange // // arrange
var client = new TestBaseClient(); // var client = new TestBaseClient();
// act // // act
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}"); // var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
// assert // // assert
Assert.That(result.Success); // Assert.That(result.Success);
} //}
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")] [TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")] [TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]

View File

@ -0,0 +1,151 @@
using CryptoExchange.Net.Objects;
using NUnit.Framework;
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Threading;
using NUnit.Framework.Legacy;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Text.Json;
using CryptoExchange.Net.UnitTests.Implementations;
using CryptoExchange.Net.Testing;
namespace CryptoExchange.Net.UnitTests.ClientTests
{
[TestFixture()]
public class RestClientTests
{
[TestCase]
public async Task RequestingData_Should_ResultInData()
{
// arrange
var client = new TestRestClient();
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
client.ApiClient1.SetNextResponse(strData, System.Net.HttpStatusCode.OK);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
Assert.That(result.Success);
Assert.That(TestHelpers.AreEqual(expected, result.Data));
}
[TestCase]
public async Task ReceivingInvalidData_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"property\": 123", System.Net.HttpStatusCode.OK);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorCode_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("Invalid request", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
{
// arrange
var client = new TestRestClient();
var response = "<html>...</html>";
client.ApiClient1.SetNextResponse(response, System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is DeserializeError);
Assert.That(result.Error!.Message!.Contains(response));
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{
// arrange
var client = new TestRestClient();
client.ApiClient1.SetNextResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.ApiClient1.GetResponseAsync<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error!.ErrorCode == "123");
Assert.That(result.Error.Message == "Invalid request");
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
[TestCase("POST", HttpMethodParameterPosition.InBody)]
[TestCase("POST", HttpMethodParameterPosition.InUri)]
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
{
// arrange
// act
var client = new TestRestClient();
var httpMethod = new HttpMethod(method);
client.ApiClient1.SetParameterPosition(httpMethod, pos);
client.ApiClient1.SetNextResponse("{}", System.Net.HttpStatusCode.OK);
var result = await client.ApiClient1.GetResponseAsync<TestObject>(httpMethod, new ParameterCollection
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
});
// assert
Assert.That(result.RequestMethod == new HttpMethod(method));
Assert.That(result.RequestBody?.Contains("TestParam1") == true == (pos == HttpMethodParameterPosition.InBody));
Assert.That((result.RequestUrl?.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That(result.RequestBody?.Contains("TestParam2") == true == (pos == HttpMethodParameterPosition.InBody));
Assert.That((result.RequestUrl?.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
}
}
}

View File

@ -0,0 +1,177 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Testing;
using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework;
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.ClientTests
{
[TestFixture]
public class SocketClientTests
{
[TestCase]
public void SettingOptions_Should_ResultInOptionsSet()
{
//arrange
//act
var client = new TestSocketClient(options =>
{
options.ExchangeOptions.MaxSocketConnections = 1;
});
//assert
Assert.That(1 == client.ApiClient1.ApiOptions.MaxSocketConnections);
}
[TestCase(true)]
[TestCase(false)]
public async Task ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
{
//arrange
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
socket.CanConnect = canConnect;
//act
var connectResult = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
//assert
Assert.That(connectResult.Success == canConnect);
}
[TestCase]
public async Task SocketMessages_Should_BeProcessedInDataHandlers()
{
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
TestObject? received = null;
var resetEvent = new AsyncResetEvent(false);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x =>
{
received = x.Data;
resetEvent.Set();
}, false, default);
socket.InvokeMessage(strData);
await resetEvent.WaitAsync(TimeSpan.FromSeconds(1));
Assert.That(received != null);
}
[TestCase(false)]
[TestCase(true)]
public async Task SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
options.ExchangeOptions.OutputOriginalData = enabled;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
var strData = JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() });
string? originalData = null;
var resetEvent = new AsyncResetEvent(false);
await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x =>
{
originalData = x.OriginalData;
resetEvent.Set();
}, false, default);
socket.InvokeMessage(strData);
await resetEvent.WaitAsync(TimeSpan.FromSeconds(1));
// assert
Assert.That(originalData == (enabled ? strData : null));
}
[TestCase()]
public async Task UnsubscribingStream_Should_CloseTheSocket()
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => {}, false, default);
// act
await client.UnsubscribeAsync(result.Data);
// assert
Assert.That(socket.Connected == false);
}
[TestCase()]
public async Task UnsubscribingAll_Should_CloseAllSockets()
{
// arrange
var client = new TestSocketClient(options =>
{
options.ReconnectInterval = TimeSpan.Zero;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
var socket2 = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var result2 = await client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, false, default);
// act
await client.UnsubscribeAllAsync();
// assert
Assert.That(socket.Connected == false);
Assert.That(socket2.Connected == false);
}
[TestCase()]
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
{
// arrange
var client = new TestSocketClient(opt =>
{
opt.OutputOriginalData = true;
});
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var subTask = client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, true, default);
socket.InvokeMessage(JsonSerializer.Serialize(new TestSocketMessage { Id = 1, Data = "ErrorWithSub" }));
var result = await subTask;
// assert
Assert.That(result.Success == false);
Assert.That(result.Error!.Message!.Contains("ErrorWithSub"));
}
[TestCase()]
public async Task SuccessResponse_Should_ConfirmSubscription()
{
var client = new TestSocketClient();
var socket = TestHelpers.ConfigureSocketClient(client, "wss://localhost");
var subTask = client.ApiClient1.SubscribeToUpdatesAsync<TestObject>(x => { }, true, default);
socket.InvokeMessage(JsonSerializer.Serialize(new TestSocketMessage { Id = 1, Data = "OK" }));
var result = await subTask;
var subscription = client.ApiClient1._socketConnections.Single().Value.Subscriptions.Single();
Assert.That(subscription.Status == SubscriptionStatus.Subscribed);
}
}
}

View File

@ -0,0 +1,109 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class ArrayConverterTests
{
[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 TestSerializerContext()
};
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"));
}
}
[JsonConverter(typeof(ArrayConverter<Test>))]
public 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>))]
public record Test2
{
[ArrayProperty(0)]
public int Prop21 { get; set; }
[ArrayProperty(1)]
public string? Prop22 { get; set; }
}
public record Test3
{
[JsonPropertyName("prop31")]
public int Prop31 { get; set; }
[JsonPropertyName("prop32")]
public string? Prop32 { get; set; }
}
}

View File

@ -0,0 +1,57 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class BoolConverterTests
{
[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 TestSerializerContext()));
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 TestSerializerContext()));
Assert.That(output!.Value == expected);
}
}
public class STJBoolObject
{
public bool? Value { get; set; }
}
public class NotNullableSTJBoolObject
{
public bool Value { get; set; }
}
}

View File

@ -0,0 +1,119 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class DateTimeConverterTests
{
[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);
}
}
public class STJTimeObject
{
[JsonConverter(typeof(DateTimeConverter))]
[JsonPropertyName("time")]
public DateTime? Time { get; set; }
}
}

View File

@ -0,0 +1,49 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using NUnit.Framework;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class DecimalConverterTests
{
[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));
}
}
public class STJDecimalObject
{
[JsonConverter(typeof(DecimalConverter))]
[JsonPropertyName("test")]
public decimal? Test { get; set; }
}
}

View File

@ -0,0 +1,147 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Testing;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
public class EnumConverterTests
{
[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 TestSerializerContext()));
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)(-9))]
[TestCase(null, (TestEnum)(-9))]
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);
}
[Test]
public void TestEnumConverterMapsUndefinedValueCorrectlyIfDefaultIsDefined()
{
var output = JsonSerializer.Deserialize<TestEnum2>($"\"TestUndefined\"");
Assert.That((int)output == -99);
}
[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);
}
[Test]
public void TestEnumConverterParseNullOnNonNullableOnlyLogsOnce()
{
LibraryHelpers.StaticLogger = new TraceLogger();
var listener = new EnumValueTraceListener();
Trace.Listeners.Add(listener);
EnumConverter<TestEnum>.Reset();
try
{
Assert.Throws<Exception>(() =>
{
var result = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new TestSerializerContext()));
});
Assert.DoesNotThrow(() =>
{
var result2 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new TestSerializerContext()));
});
}
finally
{
Trace.Listeners.Remove(listener);
}
}
}
public class STJEnumObject
{
public TestEnum? Value { get; set; }
}
public class NotNullableSTJEnumObject
{
public TestEnum Value { get; set; }
}
[JsonConverter(typeof(EnumConverter<TestEnum>))]
public enum TestEnum
{
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
[JsonConverter(typeof(EnumConverter<TestEnum2>))]
public enum TestEnum2
{
[Map("-9")]
Minus9 = -9,
[Map("1")]
One,
[Map("2")]
Two,
[Map("three", "3")]
Three,
Four
}
}

View File

@ -0,0 +1,46 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.SharedApis;
using NUnit.Framework;
using System;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.ConverterTests
{
[TestFixture()]
public class SharedModelConversionTests
{
[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));
}
}
}

View File

@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -326,7 +326,7 @@ namespace CryptoExchange.Net.UnitTests
// assert // assert
Assert.That(result, Is.Not.Null); Assert.That(result, Is.Not.Null);
Assert.That(result.BaseAsset, Is.EqualTo("BTC")); Assert.That(result!.BaseAsset, Is.EqualTo("BTC"));
Assert.That(result.QuoteAsset, Is.EqualTo("USDT")); Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot)); Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT")); Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));

View File

@ -0,0 +1,20 @@
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Objects;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestAuthenticationProvider : AuthenticationProvider<TestCredentials, TestCredentials>
{
public TestAuthenticationProvider(TestCredentials credentials) : base(credentials, credentials)
{
}
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{
requestConfig.Headers ??= new Dictionary<string, string>();
requestConfig.Headers["Authorization"] = Credential.Key;
}
}
}

View File

@ -0,0 +1,30 @@
using CryptoExchange.Net.Authentication;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestCredentials : HMACCredential
{
public TestCredentials() { }
public TestCredentials(string key, string secret) : base(key, secret)
{
}
public TestCredentials(HMACCredential credential) : base(credential.Key, credential.Secret)
{
}
public TestCredentials WithHMAC(string key, string secret)
{
if (!string.IsNullOrEmpty(Key)) throw new InvalidOperationException("Credentials already set");
Key = key;
Secret = secret;
return this;
}
/// <inheritdoc />
public override ApiCredentials Copy() => new TestCredentials(this);
}
}

View File

@ -0,0 +1,64 @@
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestEnvironment : TradeEnvironment
{
public string RestClientAddress { get; }
public string SocketClientAddress { get; }
internal TestEnvironment(
string name,
string restAddress,
string streamAddress) :
base(name)
{
RestClientAddress = restAddress;
SocketClientAddress = streamAddress;
}
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public TestEnvironment() : base(TradeEnvironmentNames.Live)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
{ }
/// <summary>
/// Get the environment by name
/// </summary>
public static TestEnvironment? GetEnvironmentByName(string? name)
=> name switch
{
TradeEnvironmentNames.Live => Live,
"" => Live,
null => Live,
_ => default
};
/// <summary>
/// Available environment names
/// </summary>
/// <returns></returns>
public static string[] All => [Live.Name];
/// <summary>
/// Live environment
/// </summary>
public static TestEnvironment Live { get; }
= new TestEnvironment(TradeEnvironmentNames.Live,
"https://localhost",
"wss://localhost");
/// <summary>
/// Create a custom environment
/// </summary>
/// <param name="name"></param>
/// <param name="spotRestAddress"></param>
/// <param name="spotSocketStreamsAddress"></param>
/// <returns></returns>
public static TestEnvironment CreateCustom(
string name,
string spotRestAddress,
string spotSocketStreamsAddress)
=> new TestEnvironment(name, spotRestAddress, spotSocketStreamsAddress);
}
}

View File

@ -1,11 +1,11 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.Implementations
{ {
public class TestObject public class TestObject
{ {
[JsonPropertyName("other")] [JsonPropertyName("other")]
public string StringData { get; set; } public string StringData { get; set; } = string.Empty;
[JsonPropertyName("intData")] [JsonPropertyName("intData")]
public int IntData { get; set; } public int IntData { get; set; }
[JsonPropertyName("decimalData")] [JsonPropertyName("decimalData")]

View File

@ -0,0 +1,25 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestQuery : Query<TestSocketMessage>
{
public TestQuery(TestSocketMessage request, bool authenticated) : base(request, authenticated, 1)
{
MessageRouter = MessageRouter.CreateWithoutTopicFilter<TestSocketMessage>(request.Id.ToString(), HandleMessage);
}
private CallResult? HandleMessage(SocketConnection connection, DateTime time, string? arg3, TestSocketMessage message)
{
if (message.Data != "OK")
return new CallResult(new ServerError(ErrorInfo.Unknown with { Message = message.Data }));
return CallResult.SuccessResult;
}
}
}

View File

@ -0,0 +1,64 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Testing.Implementations;
using Microsoft.Extensions.Logging;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestApiClient : RestApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApiClient(ILogger logger, HttpClient? httpClient, TestRestOptions options)
: base(logger, httpClient, options.Environment.RestClientAddress, options, options.ExchangeOptions)
{
}
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null) =>
baseAsset + quoteAsset;
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
internal void SetNextResponse(string data, HttpStatusCode code)
{
var expectedBytes = Encoding.UTF8.GetBytes(data);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new TestResponse(code, responseStream);
var request = new TestRequest(response);
var factory = new TestRequestFactory(request);
RequestFactory = factory;
}
internal async Task<WebCallResult<T>> GetResponseAsync<T>(HttpMethod? httpMethod = null, ParameterCollection? collection = null)
{
var definition = new RequestDefinition("/path", httpMethod ?? HttpMethod.Get)
{
Weight = 0
};
return await SendAsync<T>(BaseAddress, definition, collection ?? new ParameterCollection(), default);
}
internal void SetParameterPosition(HttpMethod httpMethod, HttpMethodParameterPosition pos)
{
ParameterPositions[httpMethod] = pos;
}
}
}

View File

@ -0,0 +1,27 @@
using CryptoExchange.Net.Clients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Net.Http;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestClient : BaseRestClient<TestEnvironment, TestCredentials>
{
public TestRestApiClient ApiClient1 { get; set; }
public TestRestApiClient ApiClient2 { get; set; }
public TestRestClient(Action<TestRestOptions>? optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(HttpClient? httpClient, ILoggerFactory? loggerFactory, IOptions<TestRestOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
ApiClient2 = AddApiClient(new TestRestApiClient(_logger, httpClient, options.Value));
}
}
}

View File

@ -0,0 +1,33 @@
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System.IO;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestMessageHandler : JsonRestMessageHandler
{
public override JsonSerializerOptions Options { get; } = SerializerOptions.WithConverters(new TestSerializerContext());
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var (jsonError, jsonDocument) = await GetJsonDocument(responseStream).ConfigureAwait(false);
if (jsonError != null)
return jsonError;
int? code = jsonDocument!.RootElement.TryGetProperty("errorCode", out var codeProp) ? codeProp.GetInt32() : null;
var msg = jsonDocument.RootElement.TryGetProperty("errorMessage", out var msgProp) ? msgProp.GetString() : null;
if (msg == null)
return new ServerError(ErrorInfo.Unknown);
if (code == null)
return new ServerError(ErrorInfo.Unknown with { Message = msg });
return new ServerError(code.Value, new ErrorInfo(ErrorType.Unknown, false, "Error") with { Message = msg });
}
}
}

View File

@ -0,0 +1,27 @@
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestRestOptions : RestExchangeOptions<TestEnvironment, TestCredentials>
{
internal static TestRestOptions Default { get; set; } = new TestRestOptions()
{
Environment = TestEnvironment.Live,
AutoTimestamp = true
};
public TestRestOptions()
{
Default?.Set(this);
}
public RestApiOptions ExchangeOptions { get; private set; } = new RestApiOptions();
internal TestRestOptions Set(TestRestOptions targetOptions)
{
targetOptions = base.Set<TestRestOptions>(targetOptions);
targetOptions.ExchangeOptions = ExchangeOptions.Set(targetOptions.ExchangeOptions);
return targetOptions;
}
}
}

View File

@ -0,0 +1,29 @@
using CryptoExchange.Net.UnitTests.ConverterTests;
using CryptoExchange.Net.UnitTests.Implementations;
using System.Collections.Generic;
using System.Text.Json.Serialization;
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))]
[JsonSerializable(typeof(TestSocketMessage))]
[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 TestSerializerContext : JsonSerializerContext
{
}
}

View File

@ -0,0 +1,44 @@
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using System;
using System.Net.Http;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketApiClient : SocketApiClient<TestEnvironment, TestAuthenticationProvider, TestCredentials>
{
public TestSocketApiClient(ILogger logger, TestSocketOptions options)
: base(logger, options.Environment.SocketClientAddress, options, options.ExchangeOptions)
{
}
public TestSocketApiClient(ILogger logger, HttpClient httpClient, string baseAddress, TestSocketOptions options, SocketApiOptions apiOptions)
: base(logger, baseAddress, options, apiOptions)
{
}
public override ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType) => new TestSocketMessageHandler();
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(SerializerOptions.WithConverters(new TestSerializerContext()));
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null) =>
baseAsset + quoteAsset;
protected override TestAuthenticationProvider CreateAuthenticationProvider(TestCredentials credentials) =>
new TestAuthenticationProvider(credentials);
public async Task<CallResult<UpdateSubscription>> SubscribeToUpdatesAsync<T>(Action<DataEvent<T>> handler, bool subQuery, CancellationToken ct)
{
return await base.SubscribeAsync(new TestSubscription<T>(_logger, handler, subQuery, false), ct);
}
}
}

View File

@ -0,0 +1,26 @@
using CryptoExchange.Net.Clients;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketClient : BaseSocketClient<TestEnvironment, TestCredentials>
{
public TestSocketApiClient ApiClient1 { get; set; }
public TestSocketApiClient ApiClient2 { get; set; }
public TestSocketClient(Action<TestSocketOptions>? optionsDelegate = null)
: this(null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestSocketClient(ILoggerFactory? loggerFactory, IOptions<TestSocketOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
ApiClient1 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
ApiClient2 = AddApiClient(new TestSocketApiClient(_logger, options.Value));
}
}
}

View File

@ -0,0 +1,16 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.Json.Serialization;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal record TestSocketMessage
{
[JsonPropertyName("id")]
public int Id { get; set; }
[JsonPropertyName("data")]
public string Data { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,33 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketMessageHandler : JsonSocketMessageHandler
{
public override JsonSerializerOptions Options { get; } = SerializerOptions.WithConverters(new TestSerializerContext());
public TestSocketMessageHandler()
{
}
protected override MessageTypeDefinition[] TypeEvaluators { get; } = [
new MessageTypeDefinition {
ForceIfFound = true,
Fields = [
new PropertyFieldReference("id")
],
TypeIdentifierCallback = (doc) => doc.FieldValue("id")!
},
new MessageTypeDefinition {
Fields = [
],
StaticIdentifier = "test"
},
];
}
}

View File

@ -0,0 +1,27 @@
using CryptoExchange.Net.Objects.Options;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSocketOptions : SocketExchangeOptions<TestEnvironment, TestCredentials>
{
internal static TestSocketOptions Default { get; set; } = new TestSocketOptions()
{
Environment = TestEnvironment.Live,
AutoTimestamp = true
};
public TestSocketOptions()
{
Default?.Set(this);
}
public SocketApiOptions ExchangeOptions { get; private set; } = new SocketApiOptions();
internal TestSocketOptions Set(TestSocketOptions targetOptions)
{
targetOptions = base.Set<TestSocketOptions>(targetOptions);
targetOptions.ExchangeOptions = ExchangeOptions.Set(targetOptions.ExchangeOptions);
return targetOptions;
}
}
}

View File

@ -0,0 +1,50 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Text;
namespace CryptoExchange.Net.UnitTests.Implementations
{
internal class TestSubscription<T> : Subscription
{
private readonly Action<DataEvent<T>> _handler;
private bool _subQuery;
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler, bool subQuery, bool authenticated) : base(logger, authenticated, true)
{
_handler = handler;
_subQuery = subQuery;
MessageRouter = MessageRouter.CreateWithoutTopicFilter<T>("test", HandleUpdate);
}
protected override Query? GetSubQuery(SocketConnection connection)
{
if (!_subQuery)
return null;
return new TestQuery(new TestSocketMessage { Id = 1, Data = "Sub" }, false);
}
protected override Query? GetUnsubQuery(SocketConnection connection)
{
if (!_subQuery)
return null;
return new TestQuery(new TestSocketMessage { Id = 2, Data = "Unsub" }, false);
}
private CallResult? HandleUpdate(SocketConnection connection, DateTime time, string? originalData, T data)
{
_handler(new DataEvent<T>("Test", data, time, originalData));
return CallResult.SuccessResult;
}
}
}

View File

@ -1,7 +1,7 @@
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options; using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.UnitTests.Implementations;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
@ -11,9 +11,9 @@ namespace CryptoExchange.Net.UnitTests
public class OptionsTests public class OptionsTests
{ {
[TearDown] [TearDown]
public void Init() public void TearDown()
{ {
TestClientOptions.Default = new TestClientOptions TestRestOptions.Default = new TestRestOptions
{ {
}; };
} }
@ -31,9 +31,9 @@ namespace CryptoExchange.Net.UnitTests
// assert // assert
Assert.Throws(typeof(ArgumentException), Assert.Throws(typeof(ArgumentException),
() => { () => {
var opts = new RestExchangeOptions<TestEnvironment, HMACCredential>() var opts = new TestRestOptions()
{ {
ApiCredentials = new HMACCredential(key, secret) ApiCredentials = new TestCredentials(key, secret)
}; };
opts.ApiCredentials.Validate(); opts.ApiCredentials.Validate();
}); });
@ -43,14 +43,14 @@ namespace CryptoExchange.Net.UnitTests
public void TestBasicOptionsAreSet() public void TestBasicOptionsAreSet()
{ {
// arrange, act // arrange, act
var options = new TestClientOptions var options = new TestRestOptions
{ {
ApiCredentials = new HMACCredential("123", "456"), ApiCredentials = new TestCredentials("123", "456"),
ReceiveWindow = TimeSpan.FromSeconds(10) RequestTimeout = TimeSpan.FromSeconds(10)
}; };
// assert // assert
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10)); Assert.That(options.RequestTimeout == TimeSpan.FromSeconds(10));
Assert.That(options.ApiCredentials.Key == "123"); Assert.That(options.ApiCredentials.Key == "123");
Assert.That(options.ApiCredentials.Secret == "456"); Assert.That(options.ApiCredentials.Secret == "456");
} }
@ -65,88 +65,88 @@ namespace CryptoExchange.Net.UnitTests
Proxy = new ApiProxy("http://testproxy", 1234) Proxy = new ApiProxy("http://testproxy", 1234)
}); });
Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null); Assert.That(client.ApiClient1.ClientOptions.Proxy, Is.Not.Null);
Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy")); Assert.That(client.ApiClient1.ClientOptions.Proxy!.Host, Is.EqualTo("http://testproxy"));
Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234)); Assert.That(client.ApiClient1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2))); Assert.That(client.ApiClient1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
} }
[Test] [Test]
public void TestSetOptionsRestWithCredentials() public void TestSetOptionsRestWithCredentials()
{ {
var client = new TestRestClient(); var client = new TestRestClient();
client.SetOptions(new UpdateOptions<HMACCredential> client.SetOptions(new UpdateOptions<TestCredentials>
{ {
ApiCredentials = new HMACCredential("123", "456"), ApiCredentials = new TestCredentials("123", "456"),
RequestTimeout = TimeSpan.FromSeconds(2), RequestTimeout = TimeSpan.FromSeconds(2),
Proxy = new ApiProxy("http://testproxy", 1234) Proxy = new ApiProxy("http://testproxy", 1234)
}); });
Assert.That(client.Api1.ApiCredentials, Is.Not.Null); Assert.That(client.ApiClient1.ApiCredentials, Is.Not.Null);
Assert.That(client.Api1.ApiCredentials.Key, Is.EqualTo("123")); Assert.That(client.ApiClient1.ApiCredentials!.Key, Is.EqualTo("123"));
Assert.That(client.Api1.ClientOptions.Proxy, Is.Not.Null); Assert.That(client.ApiClient1.ClientOptions.Proxy, Is.Not.Null);
Assert.That(client.Api1.ClientOptions.Proxy.Host, Is.EqualTo("http://testproxy")); Assert.That(client.ApiClient1.ClientOptions.Proxy!.Host, Is.EqualTo("http://testproxy"));
Assert.That(client.Api1.ClientOptions.Proxy.Port, Is.EqualTo(1234)); Assert.That(client.ApiClient1.ClientOptions.Proxy.Port, Is.EqualTo(1234));
Assert.That(client.Api1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2))); Assert.That(client.ApiClient1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
} }
[Test] [Test]
public void TestWhenUpdatingSettingsExistingClientsAreNotAffected() public void TestWhenUpdatingSettingsExistingClientsAreNotAffected()
{ {
TestClientOptions.Default = new TestClientOptions TestRestOptions.Default = new TestRestOptions
{ {
ApiCredentials = new HMACCredential("111", "222"), ApiCredentials = new TestCredentials("111", "222"),
RequestTimeout = TimeSpan.FromSeconds(1), RequestTimeout = TimeSpan.FromSeconds(1),
}; };
var client1 = new TestRestClient(); var client1 = new TestRestClient();
Assert.That(client1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(1))); Assert.That(client1.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(1)));
Assert.That(client1.ClientOptions.ApiCredentials.Key, Is.EqualTo("111")); Assert.That(client1.ClientOptions.ApiCredentials!.Key, Is.EqualTo("111"));
TestClientOptions.Default.ApiCredentials = new HMACCredential("333", "444"); TestRestOptions.Default.ApiCredentials = new TestCredentials("333", "444");
TestClientOptions.Default.RequestTimeout = TimeSpan.FromSeconds(2); TestRestOptions.Default.RequestTimeout = TimeSpan.FromSeconds(2);
var client2 = new TestRestClient(); var client2 = new TestRestClient();
Assert.That(client2.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2))); Assert.That(client2.ClientOptions.RequestTimeout, Is.EqualTo(TimeSpan.FromSeconds(2)));
Assert.That(client2.ClientOptions.ApiCredentials.Key, Is.EqualTo("333")); Assert.That(client2.ClientOptions.ApiCredentials!.Key, Is.EqualTo("333"));
} }
} }
public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential> //public class TestClientOptions: RestExchangeOptions<TestEnvironment, HMACCredential>
{ //{
/// <summary> // /// <summary>
/// Default options for the futures client // /// Default options for the futures client
/// </summary> // /// </summary>
public static TestClientOptions Default { get; set; } = new TestClientOptions() // public static TestClientOptions Default { get; set; } = new TestClientOptions()
{ // {
Environment = new TestEnvironment("test", "https://test.com") // Environment = new TestEnvironment("test", "https://test.com")
}; // };
/// <summary> // /// <summary>
/// ctor // /// ctor
/// </summary> // /// </summary>
public TestClientOptions() // public TestClientOptions()
{ // {
Default?.Set(this); // Default?.Set(this);
} // }
/// <summary> // /// <summary>
/// The default receive window for requests // /// The default receive window for requests
/// </summary> // /// </summary>
public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5); // public TimeSpan ReceiveWindow { get; set; } = TimeSpan.FromSeconds(5);
public RestApiOptions Api1Options { get; private set; } = new RestApiOptions(); // public RestApiOptions Api1Options { get; private set; } = new RestApiOptions();
public RestApiOptions Api2Options { get; set; } = new RestApiOptions(); // public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
internal TestClientOptions Set(TestClientOptions targetOptions) // internal TestClientOptions Set(TestClientOptions targetOptions)
{ // {
targetOptions = base.Set<TestClientOptions>(targetOptions); // targetOptions = base.Set<TestClientOptions>(targetOptions);
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options); // targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options); // targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
return targetOptions; // return targetOptions;
} // }
} //}
} }

View File

@ -0,0 +1,331 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.ConverterTests;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.UnitTests
{
internal class ParameterCollectionTests
{
[Test]
public void AddingBasicValue_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.Add("test", "value");
Assert.That(parameters["test"], Is.EqualTo("value"));
}
[Test]
public void AddingBasicNullValue_ThrowsException()
{
var parameters = new ParameterCollection();
Assert.Throws<ArgumentNullException>(() => parameters.Add("test", null!));
}
[Test]
public void AddingOptionalBasicValue_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptional("test", "value");
Assert.That(parameters["test"], Is.EqualTo("value"));
}
[Test]
public void AddingOptionalBasicNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptional("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingDecimalValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 0.1m);
Assert.That(parameters["test"], Is.EqualTo("0.1"));
}
[Test]
public void AddingOptionalDecimalValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 0.1m);
Assert.That(parameters["test"], Is.EqualTo("0.1"));
}
[Test]
public void AddingOptionalDecimalNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (decimal?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingIntValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 1);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalIntValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 1);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalIntNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (int?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingLongValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddString("test", 1L);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongValueAsString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", 1L);
Assert.That(parameters["test"], Is.EqualTo("1"));
}
[Test]
public void AddingOptionalLongNullValueAsString_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalString("test", (long?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingMillisecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
}
[Test]
public void AddingOptionalMillisecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMilliseconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo(1735689600000));
}
[Test]
public void AddingOptionalMillisecondNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMilliseconds("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingMillisecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
}
[Test]
public void AddingOptionalMillisecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMillisecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo("1735689600000"));
}
[Test]
public void AddingOptionalMillisecondStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalMillisecondsString("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingSecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo(1735689600));
}
[Test]
public void AddingOptionalSecondTimestamp_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSeconds("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo(1735689600));
}
[Test]
public void AddingSecondNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSeconds("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingSecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
}
[Test]
public void AddingOptionalSecondTimestampString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSecondsString("test", new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc));
Assert.That(parameters["test"], Is.EqualTo("1735689600"));
}
[Test]
public void AddingSecondStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalSecondsString("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddEnum("test", TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo("2"));
}
[Test]
public void AddingOptionalEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnum("test", (TestEnum?)TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo("2"));
}
[Test]
public void AddingOptionalEnumNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnum("test", (TestEnum?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingEnumAsInt_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddEnumAsInt("test", TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo(2));
}
[Test]
public void AddingOptionalEnumAsInt_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnumAsInt("test", (TestEnum?)TestEnum.Two);
Assert.That(parameters["test"], Is.EqualTo(2));
}
[Test]
public void AddingOptionalEnumAsIntNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalEnumAsInt("test", (TestEnum?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingCommaSeparated_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddCommaSeparated("test", ["1", "2"]);
Assert.That(parameters["test"], Is.EqualTo("1,2"));
}
[Test]
public void AddingOptionalCommaSeparated_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", ["1", "2"]);
Assert.That(parameters["test"], Is.EqualTo("1,2"));
}
[Test]
public void AddingOptionalCommaSeparatedNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", (string[]?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingCommaSeparatedEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
Assert.That(parameters["test"], Is.EqualTo("2,1"));
}
[Test]
public void AddingOptionalCommaSeparatedEnum_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", [TestEnum.Two, TestEnum.One]);
Assert.That(parameters["test"], Is.EqualTo("2,1"));
}
[Test]
public void AddingOptionalCommaSeparatedEnumNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalCommaSeparated("test", (TestEnum[]?)null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
[Test]
public void AddingBoolString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddBoolString("test", true);
Assert.That(parameters["test"], Is.EqualTo("true"));
}
[Test]
public void AddingOptionalBoolString_SetValueCorrectly()
{
var parameters = new ParameterCollection();
parameters.AddOptionalBoolString("test", true);
Assert.That(parameters["test"], Is.EqualTo("true"));
}
[Test]
public void AddingOptionalBoolStringNullValue_DoesntSetValue()
{
var parameters = new ParameterCollection();
parameters.AddOptionalBoolString("test", null);
Assert.That(parameters.ContainsKey("test"), Is.False);
}
}
}

View File

@ -1,178 +1,21 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.UnitTests.TestImplementations; using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Interfaces;
using NUnit.Framework; using NUnit.Framework;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Text;
using System.Threading; using System.Threading;
using NUnit.Framework.Legacy; using System.Threading.Tasks;
using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Guards;
using CryptoExchange.Net.RateLimiting.Filters;
using CryptoExchange.Net.RateLimiting.Interfaces;
using System.Text.Json;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
[TestFixture()] [TestFixture()]
public class RestClientTests public class RateLimitTests
{ {
[TestCase]
public void RequestingData_Should_ResultInData()
{
// arrange
var client = new TestRestClient();
var expected = new TestObject() { DecimalData = 1.23M, IntData = 10, StringData = "Some data" };
client.SetResponse(JsonSerializer.Serialize(expected, new JsonSerializerOptions { TypeInfoResolver = new TestSerializerContext() }), out _);
// act
var result = client.Api1.Request<TestObject>().Result;
// assert
Assert.That(result.Success);
Assert.That(TestHelpers.AreEqual(expected, result.Data));
}
[TestCase]
public void ReceivingInvalidData_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.SetResponse("{\"property\": 123", out _);
// act
var result = client.Api1.Request<TestObject>().Result;
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorCode_Should_ResultInError()
{
// arrange
var client = new TestRestClient();
client.SetErrorWithoutResponse(System.Net.HttpStatusCode.BadRequest, "Invalid request");
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingError_Should_ResultInFlatError()
{
// arrange
var client = new TestRestClient();
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
}
[TestCase]
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
{
// arrange
var client = new TestRestClient();
var response = "<html>...</html>";
client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api1.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is DeserializeError);
Assert.That(result.Error.Message.Contains(response));
}
[TestCase]
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
{
// arrange
var client = new ParseErrorTestRestClient();
client.SetErrorWithResponse("{\"errorMessage\": \"Invalid request\", \"errorCode\": 123}", System.Net.HttpStatusCode.BadRequest);
// act
var result = await client.Api2.Request<TestObject>();
// assert
ClassicAssert.IsFalse(result.Success);
Assert.That(result.Error != null);
Assert.That(result.Error is ServerError);
Assert.That(result.Error.ErrorCode == "123");
Assert.That(result.Error.Message == "Invalid request");
}
[TestCase]
public void SettingOptions_Should_ResultInOptionsSet()
{
// arrange
// act
var options = new TestClientOptions();
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
options.Api1Options.OutputOriginalData = true;
options.RequestTimeout = TimeSpan.FromMinutes(1);
var client = new TestBaseClient(options);
// assert
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
}
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
[TestCase("POST", HttpMethodParameterPosition.InBody)]
[TestCase("POST", HttpMethodParameterPosition.InUri)]
[TestCase("DELETE", HttpMethodParameterPosition.InBody)]
[TestCase("DELETE", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InUri)]
[TestCase("PUT", HttpMethodParameterPosition.InBody)]
public async Task Setting_Should_ResultInOptionsSet(string method, HttpMethodParameterPosition pos)
{
// arrange
// act
var client = new TestRestClient();
client.Api1.SetParameterPosition(new HttpMethod(method), pos);
client.SetResponse("{}", out var request);
await client.Api1.RequestWithParams<TestObject>(new HttpMethod(method), new ParameterCollection
{
{ "TestParam1", "Value1" },
{ "TestParam2", 2 },
},
new Dictionary<string, string>
{
{ "TestHeader", "123" }
});
// assert
Assert.That(request.Method == new HttpMethod(method));
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
Assert.That(request.GetHeaders().First().Key == "TestHeader");
Assert.That(request.GetHeaders().First().Value.Contains("123"));
}
[TestCase(1, 0.1)] [TestCase(1, 0.1)]
[TestCase(2, 0.1)] [TestCase(2, 0.1)]
[TestCase(5, 1)] [TestCase(5, 1)]
@ -188,12 +31,12 @@ namespace CryptoExchange.Net.UnitTests
for (var i = 0; i < requests + 1; i++) for (var i = 0; i < requests + 1; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(i == requests? triggered : !triggered); Assert.That(i == requests ? triggered : !triggered);
} }
triggered = false; triggered = false;
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10); await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
Assert.That(!triggered); Assert.That(!triggered);
} }
@ -209,12 +52,12 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get); var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? expectLimiting ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
} }
@ -231,7 +74,7 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get); var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get); var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
@ -271,15 +114,15 @@ namespace CryptoExchange.Net.UnitTests
{ {
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get); var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
} }
@ -294,12 +137,12 @@ namespace CryptoExchange.Net.UnitTests
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get); var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
for (var i = 0; i < 2; i++) for (var i = 0; i < 2; i++)
{ {
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null; bool expected = i == 1 ? expectLimited ? evnt?.DelayTime > TimeSpan.Zero : evnt == null : evnt == null;
Assert.That(expected); Assert.That(expected);
} }
} }
@ -318,7 +161,7 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null }; var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null }; var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
@ -337,7 +180,7 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get); var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true }; var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
@ -357,7 +200,7 @@ namespace CryptoExchange.Net.UnitTests
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get); var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true }; var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
@ -374,7 +217,7 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default); var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
@ -389,7 +232,7 @@ namespace CryptoExchange.Net.UnitTests
var rateLimiter = new RateLimitGate("Test"); var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed)); rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
RateLimitEvent evnt = null; RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; }; rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2)); var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
@ -397,5 +240,50 @@ namespace CryptoExchange.Net.UnitTests
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token); var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>()); Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
} }
[Test]
public async Task RateLimiterReset_Should_AllowNextRequestForSameDefinition()
{
// arrange
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
var definition = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
// act
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition, "https://test.com", null, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, ct.Token);
// assert
Assert.That(evnt, Is.Null);
}
[Test]
public async Task RateLimiterReset_Should_NotAllowNextRequestForDifferentDefinition()
{
// arrange
var rateLimiter = new RateLimitGate("Test");
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerConnection, new LimitItemTypeFilter(RateLimitItemType.Request), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
var definition1 = new RequestDefinition("1", HttpMethod.Get) { ConnectionId = 1 };
var definition2 = new RequestDefinition("2", HttpMethod.Get) { ConnectionId = 2 };
RateLimitEvent? evnt = null;
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
// act
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition1, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
await rateLimiter.ResetAsync(RateLimitItemType.Request, definition1, "https://test.com", null, null, default);
var result3 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, definition2, "https://test.com", null, 1, RateLimitingBehaviour.Fail, null, default);
// assert
Assert.That(evnt, Is.Not.Null);
}
} }
} }

View File

@ -1,6 +1,5 @@
using CryptoExchange.Net.SharedApis; using CryptoExchange.Net.SharedApis;
using NUnit.Framework; using NUnit.Framework;
using System;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {

View File

@ -1,234 +0,0 @@
//using CryptoExchange.Net.Objects;
//using CryptoExchange.Net.Objects.Sockets;
//using CryptoExchange.Net.Sockets;
//using CryptoExchange.Net.Testing.Implementations;
//using CryptoExchange.Net.UnitTests.TestImplementations;
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
//using Microsoft.Extensions.Logging;
//using Moq;
//using NUnit.Framework;
//using NUnit.Framework.Legacy;
//using System;
//using System.Collections.Generic;
//using System.Net.Sockets;
//using System.Text.Json;
//using System.Threading;
//using System.Threading.Tasks;
//namespace CryptoExchange.Net.UnitTests
//{
// [TestFixture]
// public class SocketClientTests
// {
// [TestCase]
// public void SettingOptions_Should_ResultInOptionsSet()
// {
// //arrange
// //act
// var client = new TestSocketClient(options =>
// {
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
// options.SubOptions.MaxSocketConnections = 1;
// });
// //assert
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
// }
// [TestCase(true)]
// [TestCase(false)]
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
// {
// //arrange
// var client = new TestSocketClient();
// var socket = client.CreateSocket();
// socket.CanConnect = canConnect;
// //act
// var connectResult = client.SubClient.ConnectSocketSub(
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// //assert
// Assert.That(connectResult.Success == canConnect);
// }
// [TestCase]
// public void SocketMessages_Should_BeProcessedInDataHandlers()
// {
// // arrange
// var client = new TestSocketClient(options => {
// options.ReconnectInterval = TimeSpan.Zero;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var rstEvent = new ManualResetEvent(false);
// Dictionary<string, string> result = null;
// client.SubClient.ConnectSocketSub(sub);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// {
// result = messageEvent.Data;
// rstEvent.Set();
// });
// sub.AddSubscription(subObj);
// // act
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
// rstEvent.WaitOne(1000);
// // assert
// Assert.That(result["property"] == "123");
// }
// [TestCase(false)]
// [TestCase(true)]
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
// {
// // arrange
// var client = new TestSocketClient(options =>
// {
// options.ReconnectInterval = TimeSpan.Zero;
// options.SubOptions.OutputOriginalData = enabled;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var rstEvent = new ManualResetEvent(false);
// string original = null;
// client.SubClient.ConnectSocketSub(sub);
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
// {
// original = messageEvent.OriginalData;
// rstEvent.Set();
// });
// sub.AddSubscription(subObj);
// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
// // act
// socket.InvokeMessage(msgToSend);
// rstEvent.WaitOne(1000);
// // assert
// Assert.That(original == (enabled ? msgToSend : null));
// }
// [TestCase()]
// public void UnsubscribingStream_Should_CloseTheSocket()
// {
// // arrange
// var client = new TestSocketClient(options =>
// {
// options.ReconnectInterval = TimeSpan.Zero;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// client.SubClient.ConnectSocketSub(sub);
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// var ups = new UpdateSubscription(sub, subscription);
// sub.AddSubscription(subscription);
// // act
// client.UnsubscribeAsync(ups).Wait();
// // assert
// Assert.That(socket.Connected == false);
// }
// [TestCase()]
// public void UnsubscribingAll_Should_CloseAllSockets()
// {
// // arrange
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
// var socket1 = client.CreateSocket();
// var socket2 = client.CreateSocket();
// socket1.CanConnect = true;
// socket2.CanConnect = true;
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// client.SubClient.ConnectSocketSub(sub1);
// client.SubClient.ConnectSocketSub(sub2);
// var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
// sub1.AddSubscription(subscription1);
// sub2.AddSubscription(subscription2);
// var ups1 = new UpdateSubscription(sub1, subscription1);
// var ups2 = new UpdateSubscription(sub2, subscription2);
// // act
// client.UnsubscribeAllAsync().Wait();
// // assert
// Assert.That(socket1.Connected == false);
// Assert.That(socket2.Connected == false);
// }
// [TestCase()]
// public void FailingToConnectSocket_Should_ReturnError()
// {
// // arrange
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
// var socket = client.CreateSocket();
// socket.CanConnect = false;
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
// // act
// var connectResult = client.SubClient.ConnectSocketSub(sub1);
// // assert
// ClassicAssert.IsFalse(connectResult.Success);
// }
// [TestCase()]
// public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
// {
// // arrange
// var channel = "trade_btcusd";
// var client = new TestSocketClient(opt =>
// {
// opt.OutputOriginalData = true;
// opt.SocketSubscriptionsCombineTarget = 1;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// // act
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
// await sub;
// // assert
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
// }
// [TestCase()]
// public async Task SuccessResponse_Should_ConfirmSubscription()
// {
// // arrange
// var channel = "trade_btcusd";
// var client = new TestSocketClient(opt =>
// {
// opt.OutputOriginalData = true;
// opt.SocketSubscriptionsCombineTarget = 1;
// });
// var socket = client.CreateSocket();
// socket.CanConnect = true;
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
// // act
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
// await sub;
// // assert
// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
// }
// }
//}

View File

@ -0,0 +1,235 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class QueryRouterTests
{
[Test]
public void BuildFromRoutes_Should_GroupRoutesByTypeIdentifier_AndSetDeserializationType()
{
// arrange
var routes = new MessageRoute[]
{
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
};
var router = new QueryRouter(routes);
// act
var type1Routes = router.GetRoutes("type1");
var type2Routes = router.GetRoutes("type2");
var missingRoutes = router.GetRoutes("missing");
// assert
Assert.That(type1Routes, Is.Not.Null);
Assert.That(type2Routes, Is.Not.Null);
Assert.That(missingRoutes, Is.Null);
Assert.That(type1Routes, Is.TypeOf<QueryRouteCollection>());
Assert.That(type2Routes, Is.TypeOf<QueryRouteCollection>());
Assert.That(type1Routes!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type2Routes!.DeserializationType, Is.EqualTo(typeof(int)));
}
[Test]
public void AddRoute_Should_SetMultipleReaders_WhenAnyRouteAllowsMultipleReaders()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
// act
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) => null));
var beforeMultipleReaders = collection.MultipleReaders;
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null, true));
var afterMultipleReaders = collection.MultipleReaders;
// assert
Assert.That(beforeMultipleReaders, Is.False);
Assert.That(afterMultipleReaders, Is.True);
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
}
[Test]
public void Handle_Should_ReturnFalse_WhenNoRoutesMatch()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.Null);
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
}
[Test]
public void Handle_Should_StopAfterFirstNonNullMatchingResult_WhenMultipleReadersIsFalse()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return new CallResult(null);
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first" }));
}
[Test]
public void Handle_Should_ContinueAfterNonNullMatchingResult_WhenMultipleReadersIsTrue()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return expectedResult;
}, true));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return new CallResult(null);
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_ContinueUntilNonNullResult_WhenEarlierMatchingRoutesReturnNull()
{
// arrange
var calls = new List<string>();
var expectedResult = CallResult.SuccessResult;
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return expectedResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("third");
return new CallResult(null);
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(expectedResult));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_ReturnHandledTrue_WhenMatchingRoutesReturnNull()
{
// arrange
var collection = new QueryRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.Null);
}
}
}

View File

@ -0,0 +1,167 @@
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.Sockets.Interfaces;
using NUnit.Framework;
using System;
using System.Linq;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class RoutingTableTests
{
[Test]
public void Update_Should_CreateEntriesPerTypeIdentifier_WithCorrectDeserializationTypeAndHandlers()
{
// arrange
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)));
var table = new RoutingTable();
// act
table.Update(new IMessageProcessor[] { processor1, processor2 });
var type1Entry = table.GetRouteTableEntry("type1");
var type2Entry = table.GetRouteTableEntry("type2");
var missingEntry = table.GetRouteTableEntry("missing");
// assert
Assert.That(type1Entry, Is.Not.Null);
Assert.That(type2Entry, Is.Not.Null);
Assert.That(missingEntry, Is.Null);
Assert.That(type1Entry!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type1Entry.IsStringOutput, Is.True);
Assert.That(type1Entry.Handlers, Has.Count.EqualTo(1));
Assert.That(type1Entry.Handlers.Single(), Is.SameAs(processor1));
Assert.That(type2Entry!.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(type2Entry.IsStringOutput, Is.False);
Assert.That(type2Entry.Handlers, Has.Count.EqualTo(1));
Assert.That(type2Entry.Handlers.Single(), Is.SameAs(processor2));
}
[Test]
public void Update_Should_AddMultipleProcessors_ForSameTypeIdentifier()
{
// arrange
var processor1 = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
var processor2 = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null)));
var table = new RoutingTable();
// act
table.Update(new IMessageProcessor[] { processor1, processor2 });
var entry = table.GetRouteTableEntry("type1");
// assert
Assert.That(entry, Is.Not.Null);
Assert.That(entry!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(entry.Handlers, Has.Count.EqualTo(2));
Assert.That(entry.Handlers, Does.Contain(processor1));
Assert.That(entry.Handlers, Does.Contain(processor2));
}
[Test]
public void Update_Should_ReplacePreviousEntries()
{
// arrange
var initialProcessor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
var replacementProcessor = new TestMessageProcessor(
2,
MessageRouter.Create(
MessageRoute<int>.CreateWithoutTopicFilter("type2", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { initialProcessor });
// act
table.Update(new IMessageProcessor[] { replacementProcessor });
var oldEntry = table.GetRouteTableEntry("type1");
var newEntry = table.GetRouteTableEntry("type2");
// assert
Assert.That(oldEntry, Is.Null);
Assert.That(newEntry, Is.Not.Null);
Assert.That(newEntry!.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(newEntry.Handlers, Has.Count.EqualTo(1));
Assert.That(newEntry.Handlers.Single(), Is.SameAs(replacementProcessor));
}
[Test]
public void Update_WithEmptyProcessors_Should_ClearEntries()
{
// arrange
var processor = new TestMessageProcessor(
1,
MessageRouter.Create(
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null)));
var table = new RoutingTable();
table.Update(new IMessageProcessor[] { processor });
// act
table.Update(Array.Empty<IMessageProcessor>());
// assert
Assert.That(table.GetRouteTableEntry("type1"), Is.Null);
}
[Test]
public void TypeRoutingCollection_Should_SetIsStringOutput_BasedOnDeserializationType()
{
// arrange & act
var stringCollection = new TypeRoutingCollection(typeof(string));
var intCollection = new TypeRoutingCollection(typeof(int));
// assert
Assert.That(stringCollection.IsStringOutput, Is.True);
Assert.That(stringCollection.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(stringCollection.Handlers, Is.Empty);
Assert.That(intCollection.IsStringOutput, Is.False);
Assert.That(intCollection.DeserializationType, Is.EqualTo(typeof(int)));
Assert.That(intCollection.Handlers, Is.Empty);
}
private sealed class TestMessageProcessor : IMessageProcessor
{
public int Id { get; }
public MessageRouter MessageRouter { get; }
public TestMessageProcessor(int id, MessageRouter messageRouter)
{
Id = id;
MessageRouter = messageRouter;
}
public event Action? OnMessageRouterUpdated;
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection socketConnection, DateTime receiveTime, string? originalData, object result)
{
return true;
}
}
}
}

View File

@ -0,0 +1,160 @@
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default.Routing;
using NUnit.Framework;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.UnitTests.SocketRoutingTests
{
[TestFixture]
public class SubscriptionRouterTests
{
[Test]
public void BuildFromRoutes_Should_GroupRoutesByTypeIdentifier_AndSetDeserializationType()
{
// arrange
var routes = new MessageRoute[]
{
MessageRoute<string>.CreateWithoutTopicFilter("type1", (_, _, _, _) => null),
MessageRoute<string>.CreateWithTopicFilter("type1", "topic1", (_, _, _, _) => null),
MessageRoute<int>.CreateWithTopicFilter("type2", "topic2", (_, _, _, _) => null)
};
var router = new SubscriptionRouter(routes);
// act
var type1Routes = router.GetRoutes("type1");
var type2Routes = router.GetRoutes("type2");
var missingRoutes = router.GetRoutes("missing");
// assert
Assert.That(type1Routes, Is.Not.Null);
Assert.That(type2Routes, Is.Not.Null);
Assert.That(missingRoutes, Is.Null);
Assert.That(type1Routes, Is.TypeOf<SubscriptionRouteCollection>());
Assert.That(type2Routes, Is.TypeOf<SubscriptionRouteCollection>());
Assert.That(type1Routes!.DeserializationType, Is.EqualTo(typeof(string)));
Assert.That(type2Routes!.DeserializationType, Is.EqualTo(typeof(int)));
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(calls, Is.EqualTo(new[] { "no-topic" }));
}
[Test]
public void Handle_Should_ReturnFalse_WhenNoRoutesMatch()
{
// arrange
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("other-topic", MessageRoute<string>.CreateWithTopicFilter("type", "other-topic", (_, _, _, _) => null));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
}
[Test]
public void Handle_Should_InvokeRoutesWithoutTopicFilter_AndMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute(null, MessageRoute<string>.CreateWithoutTopicFilter("type", (_, _, _, _) =>
{
calls.Add("no-topic");
return null;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(calls, Is.EqualTo(new[] { "no-topic", "topic" }));
}
[Test]
public void Handle_Should_InvokeAllMatchingTopicRoutes()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("first");
return CallResult.SuccessResult;
}));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("second");
return null;
}));
collection.Build();
// act
var handled = collection.Handle("topic", null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.True);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(calls, Is.EqualTo(new[] { "first", "second" }));
}
[Test]
public void Handle_Should_NotInvokeTopicRoutes_WhenTopicFilterIsNull()
{
// arrange
var calls = new List<string>();
var collection = new SubscriptionRouteCollection(typeof(string));
collection.AddRoute("topic", MessageRoute<string>.CreateWithTopicFilter("type", "topic", (_, _, _, _) =>
{
calls.Add("topic");
return null;
}));
collection.Build();
// act
var handled = collection.Handle(null, null!, DateTime.UtcNow, "original", "data", out var result);
// assert
Assert.That(handled, Is.False);
Assert.That(result, Is.SameAs(CallResult.SuccessResult));
Assert.That(calls, Is.Empty);
}
}
}

View File

@ -1,490 +0,0 @@
using CryptoExchange.Net.Attributes;
using CryptoExchange.Net.Converters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Testing;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using System;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
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)(-9))]
[TestCase(null, (TestEnum)(-9))]
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);
}
[Test]
public void TestEnumConverterMapsUndefinedValueCorrectlyIfDefaultIsDefined()
{
var output = JsonSerializer.Deserialize<TestEnum2>($"\"TestUndefined\"");
Assert.That((int)output == -99);
}
[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);
}
[Test]
public void TestEnumConverterParseNullOnNonNullableOnlyLogsOnce()
{
LibraryHelpers.StaticLogger = new TraceLogger();
var listener = new EnumValueTraceListener();
Trace.Listeners.Add(listener);
EnumConverter<TestEnum>.Reset();
try
{
Assert.Throws<Exception>(() =>
{
var result = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new SerializationContext()));
});
Assert.DoesNotThrow(() =>
{
var result2 = JsonSerializer.Deserialize<NotNullableSTJEnumObject>("{\"Value\": null}", SerializerOptions.WithConverters(new SerializationContext()));
});
}
finally
{
Trace.Listeners.Remove(listener);
}
}
[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
}
[JsonConverter(typeof(EnumConverter<TestEnum2>))]
public enum TestEnum2
{
[Map("-9")]
Minus9 = -9,
[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
{
}
}

View File

@ -1,88 +0,0 @@
using System;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.SharedApis;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
namespace CryptoExchange.Net.UnitTests
{
public class TestBaseClient: BaseClient
{
public TestSubClient SubClient { get; }
public TestBaseClient(): base(null, "Test")
{
var options = new TestClientOptions();
_logger = NullLogger.Instance;
Initialize(options);
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
}
public TestBaseClient(TestClientOptions exchangeOptions) : base(null, "Test")
{
_logger = NullLogger.Instance;
Initialize(exchangeOptions);
SubClient = AddApiClient(new TestSubClient(exchangeOptions, new RestApiOptions()));
}
public void Log(LogLevel verbosity, string data)
{
_logger.Log(verbosity, data);
}
}
public class TestSubClient : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
{
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
public TestSubClient(RestExchangeOptions<TestEnvironment, HMACCredential> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
{
}
public CallResult<T> Deserialize<T>(string data)
{
return new CallResult<T>(JsonSerializer.Deserialize<T>(data));
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials) => throw new NotImplementedException();
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
}
public class TestAuthProvider : AuthenticationProvider<HMACCredential, HMACCredential>
{
public TestAuthProvider(HMACCredential credentials) : base(credentials, credentials)
{
}
public override void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig)
{
}
public string GetKey() => Credential.Key;
public string GetSecret() => Credential.Secret;
}
public class TestEnvironment : TradeEnvironment
{
public string TestAddress { get; }
public TestEnvironment(string name, string url) : base(name)
{
TestAddress = url;
}
}
}

View File

@ -1,49 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestHelpers
{
[ExcludeFromCodeCoverage]
public static bool AreEqual<T>(T self, T to, params string[] ignore) where T : class
{
if (self != null && to != null)
{
var type = self.GetType();
var ignoreList = new List<string>(ignore);
foreach (var pi in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (ignoreList.Contains(pi.Name))
{
continue;
}
var selfValue = type.GetProperty(pi.Name).GetValue(self, null);
var toValue = type.GetProperty(pi.Name).GetValue(to, null);
if (pi.PropertyType.IsClass && !pi.PropertyType.Module.ScopeName.Equals("System.Private.CoreLib.dll"))
{
// Check of "CommonLanguageRuntimeLibrary" is needed because string is also a class
if (AreEqual(selfValue, toValue, ignore))
{
continue;
}
return false;
}
if (selfValue != toValue && (selfValue == null || !selfValue.Equals(toValue)))
{
return false;
}
}
return true;
}
return self == to;
}
}
}

View File

@ -1,213 +0,0 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using Moq;
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using CryptoExchange.Net.Clients;
using Microsoft.Extensions.Options;
using System.Linq;
using CryptoExchange.Net.Converters.SystemTextJson;
using System.Text.Json.Serialization;
using System.Net.Http.Headers;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
public class TestRestClient: BaseRestClient<TestEnvironment, HMACCredential>
{
public TestRestApi1Client Api1 { get; }
public TestRestApi2Client Api2 { get; }
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
{
}
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
{
Initialize(options.Value);
Api1 = AddApiClient(new TestRestApi1Client(options.Value));
Api2 = AddApiClient(new TestRestApi2Client(options.Value));
}
public void SetResponse(string responseData, out IRequest requestObj)
{
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
var headers = new HttpRequestMessage().Headers;
var request = new Mock<IRequest>();
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.SetContent(It.IsAny<string>(), It.IsAny<Encoding>(), It.IsAny<string>())).Callback(new Action<string, Encoding, string>((content, encoding, 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 string[] { val }));
request.Setup(c => c.GetHeaders()).Returns(() => headers);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
})
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
{
request.Setup(a => a.Uri).Returns(uri);
request.Setup(a => a.Method).Returns(method);
})
.Returns(request.Object);
requestObj = request.Object;
}
public void SetErrorWithoutResponse(HttpStatusCode code, string message)
{
var we = new HttpRequestException();
typeof(HttpRequestException).GetField("_message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).SetValue(we, message);
var request = new Mock<IRequest>();
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object);
}
public void SetErrorWithResponse(string responseData, HttpStatusCode code)
{
var expectedBytes = Encoding.UTF8.GetBytes(responseData);
var responseStream = new MemoryStream();
responseStream.Write(expectedBytes, 0, expectedBytes.Length);
responseStream.Seek(0, SeekOrigin.Begin);
var response = new Mock<IResponse>();
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
var headers = new List<KeyValuePair<string, string[]>>();
var request = new Mock<IRequest>();
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.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(new HttpRequestMessage().Headers);
var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
factory = Mock.Get(Api2.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
.Returns(request.Object);
}
}
public class TestRestApi1Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
/// <inheritdoc />
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
{
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, ParameterCollection parameters, Dictionary<string, string> headers) where T : class
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", method) { Weight = 0 }, parameters, default, additionalHeaders: headers);
}
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
{
ParameterPositions[method] = position;
}
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
=> new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
{
throw new NotImplementedException();
}
}
public class TestRestApi2Client : RestApiClient<TestEnvironment, TestAuthProvider, HMACCredential>
{
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
{
RequestFactory = new Mock<IRequestFactory>().Object;
}
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
/// <inheritdoc />
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
{
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
}
protected override TestAuthProvider CreateAuthenticationProvider(HMACCredential credentials)
=> new TestAuthProvider(credentials);
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync()
{
throw new NotImplementedException();
}
}
public class TestError
{
[JsonPropertyName("errorCode")]
public int ErrorCode { get; set; }
[JsonPropertyName("errorMessage")]
public string ErrorMessage { get; set; }
}
public class ParseErrorTestRestClient: TestRestClient
{
public ParseErrorTestRestClient() { }
}
}

View File

@ -1,32 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestRestMessageHandler : JsonRestMessageHandler
{
private ErrorMapping _errorMapping = new ErrorMapping([]);
public override JsonSerializerOptions Options => new JsonSerializerOptions();
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
if (result.Item1 != null)
return result.Item1;
var errorData = result.Item2.Deserialize<TestError>();
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
}
}
}

View File

@ -1,17 +0,0 @@
using CryptoExchange.Net.UnitTests.TestImplementations;
using System.Collections.Generic;
using System.Text.Json.Serialization;
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
{
}
}

View File

@ -0,0 +1,105 @@
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Text;
using CryptoExchange.Net;
using CryptoExchange.Net.Objects;
namespace CryptoExchange.Net.UnitTests
{
internal class UriSerializationTests
{
[Test]
public void CreateParamString_SerializesBasicValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", "1" },
{ "b", 2 },
{ "c", true }
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a=1&b=2&c=True"));
}
[Test]
public void CreateParamString_SerializesArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a[]=1&a[]=2"));
}
[Test]
public void CreateParamStringEncoded_SerializesArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.Array);
Assert.That(parameterString, Is.EqualTo("a[]=1%2B2&a[]=2%2B3"));
}
[Test]
public void CreateParamString_SerializesJsonArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.JsonArray);
Assert.That(parameterString, Is.EqualTo("a=[1,2]"));
}
[Test]
public void CreateParamStringEncoded_SerializesJsonArrayValuesCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.JsonArray);
Assert.That(parameterString, Is.EqualTo("a=[1%2B2,2%2B3]"));
}
[Test]
public void CreateParamString_SerializesMultipleValuesArrayCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1", "2" } },
};
var parameterString = parameters.CreateParamString(false, ArrayParametersSerialization.MultipleValues);
Assert.That(parameterString, Is.EqualTo("a=1&a=2"));
}
[Test]
public void CreateParamStringEncoded_SerializesMultipleValuesArrayCorrectly()
{
var parameters = new Dictionary<string, object>()
{
{ "a", new [] { "1+2", "2+3" } },
};
var parameterString = parameters.CreateParamString(true, ArrayParametersSerialization.MultipleValues);
Assert.That(parameterString, Is.EqualTo("a=1%2B2&a=2%2B3"));
}
}
}

View File

@ -82,14 +82,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
private static FrozenSet<EnumMapping>? _mappingToEnum = null; private static FrozenSet<EnumMapping>? _mappingToEnum = null;
private static FrozenDictionary<T, string>? _mappingToString = null; private static FrozenDictionary<T, string>? _mappingToString = null;
private static bool RunOptimistic => true;
#else #else
private static List<EnumMapping>? _mappingToEnum = null; private static List<EnumMapping>? _mappingToEnum = null;
private static Dictionary<T, string>? _mappingToString = null; private static Dictionary<T, string>? _mappingToString = null;
// In NetStandard the `ValueTextEquals` method used is slower than just string comparing
// so only bother in newer frameworks
private static bool RunOptimistic => false;
#endif #endif
private NullableEnumConverter? _nullableEnumConverter = null; private NullableEnumConverter? _nullableEnumConverter = null;
private static Type _enumType = typeof(T);
private static T? _undefinedEnumValue; private static T? _undefinedEnumValue;
private static bool _hasFlagsAttribute = _enumType.IsDefined(typeof(FlagsAttribute));
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>(); private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
private static ConcurrentBag<string> _notOptimalValuesWarned = new ConcurrentBag<string>();
private const int _optimisticValueCountThreshold = 6;
internal class NullableEnumConverter : JsonConverter<T?> internal class NullableEnumConverter : JsonConverter<T?>
{ {
@ -153,10 +164,18 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull) private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyStringOrNull)
{ {
isEmptyStringOrNull = false; isEmptyStringOrNull = false;
var enumType = typeof(T);
if (_mappingToEnum == null) if (_mappingToEnum == null)
CreateMapping(); CreateMapping();
bool optimisticCheckDone = false;
if (RunOptimistic)
{
var resultOptimistic = GetValueOptimistic(ref reader, ref optimisticCheckDone);
if (resultOptimistic != null)
return resultOptimistic.Value;
}
var isNumber = reader.TokenType == JsonTokenType.Number;
var stringValue = reader.TokenType switch var stringValue = reader.TokenType switch
{ {
JsonTokenType.String => reader.GetString(), JsonTokenType.String => reader.GetString(),
@ -173,8 +192,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return null; return null;
} }
if (!GetValue(enumType, stringValue, out var result)) if (!GetValue(stringValue, optimisticCheckDone, out var result))
{ {
// Note: checking this here and before the GetValue seems redundant but it allows enum mapping for empty strings
if (string.IsNullOrWhiteSpace(stringValue)) if (string.IsNullOrWhiteSpace(stringValue))
{ {
isEmptyStringOrNull = true; isEmptyStringOrNull = true;
@ -185,13 +205,22 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (!_unknownValuesWarned.Contains(stringValue)) if (!_unknownValuesWarned.Contains(stringValue))
{ {
_unknownValuesWarned.Add(stringValue!); _unknownValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should added please open an issue on the Github repo"); LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should be added please open an issue on the Github repo");
} }
} }
return null; return null;
} }
if (optimisticCheckDone)
{
if (!_notOptimalValuesWarned.Contains(stringValue))
{
_notOptimalValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogTrace($"Enum mapping sub-optimal. EnumType: {_enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]");
}
}
return result; return result;
} }
@ -202,18 +231,50 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteStringValue(stringValue); writer.WriteStringValue(stringValue);
} }
private static bool GetValue(Type objectType, string value, out T? result) /// <summary>
/// Try to get the enum value based on the string value using the Utf8JsonReader's ValueTextEquals method.
/// This is an optimization to avoid string allocations when possible, but can only match case sensitively
/// </summary>
private static T? GetValueOptimistic(ref Utf8JsonReader reader, ref bool optimisticCheckDone)
{
if (reader.TokenType != JsonTokenType.String)
{
optimisticCheckDone = false;
return null;
}
if (_mappingToEnum!.Count >= _optimisticValueCountThreshold)
{
optimisticCheckDone = false;
return null;
}
optimisticCheckDone = true;
foreach (var item in _mappingToEnum!)
{
if (reader.ValueTextEquals(item.StringValue))
return item.Value;
}
return null;
}
private static bool GetValue(string value, bool optimisticCheckDone, out T? result)
{ {
if (_mappingToEnum != null) if (_mappingToEnum != null)
{ {
EnumMapping? mapping = null; EnumMapping? mapping = null;
// Try match on full equals // If we tried the optimistic path first we already know its not case match
foreach (var item in _mappingToEnum) if (!optimisticCheckDone)
{ {
if (item.StringValue.Equals(value, StringComparison.Ordinal)) // Try match on full equals
foreach (var item in _mappingToEnum)
{ {
mapping = item; if (item.StringValue.Equals(value, StringComparison.Ordinal))
break; {
mapping = item;
break;
}
} }
} }
@ -237,10 +298,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
} }
if (objectType.IsDefined(typeof(FlagsAttribute))) if (_hasFlagsAttribute)
{ {
var intValue = int.Parse(value); var intValue = int.Parse(value);
result = (T)Enum.ToObject(objectType, intValue); result = (T)Enum.ToObject(_enumType, intValue);
return true; return true;
} }
@ -262,8 +323,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
// If no explicit mapping is found try to parse string // If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true); #if NET8_0_OR_GREATER
if (!Enum.IsDefined(objectType, result)) result = Enum.Parse<T>(value, true);
#else
result = (T)Enum.Parse(_enumType, value, true);
#endif
if (!Enum.IsDefined(_enumType, result))
{ {
result = default; result = default;
return false; return false;
@ -280,11 +345,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static void CreateMapping() private static void CreateMapping()
{ {
var mappingToEnum = new List<EnumMapping>(); var mappingStringToEnum = new List<EnumMapping>();
var mappingToString = new Dictionary<T, string>(); var mappingEnumToString = new Dictionary<T, string>();
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); #pragma warning disable IL2080
var enumMembers = enumType.GetFields(); var enumMembers = _enumType.GetFields();
#pragma warning restore IL2080
foreach (var member in enumMembers) foreach (var member in enumMembers)
{ {
var maps = member.GetCustomAttributes(typeof(MapAttribute), false); var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
@ -292,23 +358,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
foreach (var value in attribute.Values) foreach (var value in attribute.Values)
{ {
var enumVal = (T)Enum.Parse(enumType, member.Name); #if NET8_0_OR_GREATER
mappingToEnum.Add(new EnumMapping(enumVal, value)); var enumVal = Enum.Parse<T>(member.Name);
if (!mappingToString.ContainsKey(enumVal)) #else
mappingToString.Add(enumVal, value); var enumVal = (T)Enum.Parse(_enumType, member.Name);
#endif
mappingStringToEnum.Add(new EnumMapping(enumVal, value));
if (!mappingEnumToString.ContainsKey(enumVal))
mappingEnumToString.Add(enumVal, value);
} }
} }
} }
#if NET8_0_OR_GREATER #if NET8_0_OR_GREATER
_mappingToEnum = mappingToEnum.ToFrozenSet(); _mappingToEnum = mappingStringToEnum.ToFrozenSet();
_mappingToString = mappingToString.ToFrozenDictionary(); _mappingToString = mappingEnumToString.ToFrozenDictionary();
#else #else
_mappingToEnum = mappingToEnum; _mappingToEnum = mappingStringToEnum;
_mappingToString = mappingToString; _mappingToString = mappingEnumToString;
#endif #endif
} }
// For testing purposes only, allows resetting the static mapping and warnings
internal static void Reset() internal static void Reset()
{ {
_undefinedEnumValue = null; _undefinedEnumValue = null;
@ -336,7 +408,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <returns></returns> /// <returns></returns>
public static T? ParseString(string value) public static T? ParseString(string value)
{ {
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (_mappingToEnum == null) if (_mappingToEnum == null)
CreateMapping(); CreateMapping();
@ -369,8 +440,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
try try
{ {
// If no explicit mapping is found try to parse string #if NET8_0_OR_GREATER
return (T)Enum.Parse(type, value, true); return Enum.Parse<T>(value, true);
#else
return (T)Enum.Parse(_enumType, value, true);
#endif
} }
catch (Exception) catch (Exception)
{ {

View File

@ -24,7 +24,7 @@ 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), options); return JsonDocument.Parse(value!).Deserialize<T>(options);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -6,9 +6,9 @@
<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>11.0.3</PackageVersion> <PackageVersion>11.1.1</PackageVersion>
<AssemblyVersion>11.0.3</AssemblyVersion> <AssemblyVersion>11.1.1</AssemblyVersion>
<FileVersion>11.0.3</FileVersion> <FileVersion>11.1.1</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;CryptoExchange.Net</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;CryptoExchange.Net</PackageTags>
<RepositoryType>git</RepositoryType> <RepositoryType>git</RepositoryType>

View File

@ -107,7 +107,7 @@ namespace CryptoExchange.Net
} }
else else
{ {
uriString.Append('['); uriString.Append($"{parameter.Key}=[");
var firstArrayEntry = true; var firstArrayEntry = true;
foreach (var entry in (Array)parameter.Value) foreach (var entry in (Array)parameter.Value)
{ {

View File

@ -55,6 +55,7 @@ namespace CryptoExchange.Net
{ "Kucoin.SpotKey", "f8ae62cb-2b3d-420c-8c98-e1c17dd4e30a" }, { "Kucoin.SpotKey", "f8ae62cb-2b3d-420c-8c98-e1c17dd4e30a" },
{ "Mexc", "EASYT" }, { "Mexc", "EASYT" },
{ "OKX", "1425d83a94fbBCDE" }, { "OKX", "1425d83a94fbBCDE" },
{ "Weex", "b-WEEX111124-" },
{ "XT", "4XWeqN10M1fcoI5L" }, { "XT", "4XWeqN10M1fcoI5L" },
}; };

View File

@ -278,6 +278,35 @@ namespace CryptoExchange.Net.Objects
base.Add(key, string.Join(",", values)); base.Add(key, string.Join(",", values));
} }
/// <summary>
/// Add key as comma separated values
/// </summary>
#if NET5_0_OR_GREATER
public void AddCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T> values)
#else
public void AddCommaSeparated<T>(string key, IEnumerable<T> values)
#endif
where T : struct, Enum
{
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
}
/// <summary>
/// Add key as comma separated values if there are values provided
/// </summary>
#if NET5_0_OR_GREATER
public void AddOptionalCommaSeparated<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string key, IEnumerable<T>? values)
#else
public void AddOptionalCommaSeparated<T>(string key, IEnumerable<T>? values)
#endif
where T : struct, Enum
{
if (values == null || !values.Any())
return;
base.Add(key, string.Join(",", values.Select(x => EnumConverter.GetString(x))));
}
/// <summary> /// <summary>
/// Add key as boolean lower case value /// Add key as boolean lower case value
/// </summary> /// </summary>

View File

@ -3,6 +3,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.RateLimiting.Trackers; using CryptoExchange.Net.RateLimiting.Trackers;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
namespace CryptoExchange.Net.RateLimiting.Guards namespace CryptoExchange.Net.RateLimiting.Guards
@ -126,7 +127,6 @@ namespace CryptoExchange.Net.RateLimiting.Guards
_trackers.Add(key, tracker); _trackers.Add(key, tracker);
} }
var delay = tracker.GetWaitTime(requestWeight); var delay = tracker.GetWaitTime(requestWeight);
if (delay == default) if (delay == default)
return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current); return LimitCheck.NotNeeded(Limit, TimeSpan, tracker.Current);
@ -172,6 +172,33 @@ namespace CryptoExchange.Net.RateLimiting.Guards
return RateLimitState.Applied(Limit, TimeSpan, tracker.Current); return RateLimitState.Applied(Limit, TimeSpan, tracker.Current);
} }
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
{
foreach (var filter in _filters)
{
if (!filter.Passes(type, definition, host, apiKey))
return;
}
if (SharedGuard)
_sharedGuardSemaphore!.Wait();
try
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
return;
tracker.Reset();
}
finally
{
if (SharedGuard)
_sharedGuardSemaphore!.Release();
}
}
/// <summary> /// <summary>
/// Create a new WindowTracker /// Create a new WindowTracker
/// </summary> /// </summary>

View File

@ -65,5 +65,11 @@ namespace CryptoExchange.Net.RateLimiting.Guards
/// </summary> /// </summary>
/// <param name="after"></param> /// <param name="after"></param>
public void UpdateAfter(DateTime after) => After = after; public void UpdateAfter(DateTime after) => After = after;
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
{
After = DateTime.UtcNow;
}
} }
} }

View File

@ -88,5 +88,15 @@ namespace CryptoExchange.Net.RateLimiting.Guards
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) : : _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided")); new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
} }
/// <inheritdoc />
public void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix)
{
var key = _keySelector(definition, host, apiKey) + keySuffix;
if (!_trackers.TryGetValue(key, out var tracker))
return;
tracker.Reset();
}
} }
} }

View File

@ -74,5 +74,22 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="ct">Cancelation token</param> /// <param name="ct">Cancelation token</param>
/// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns> /// <returns>Error if RateLimitingBehaviour is Fail and rate limit is hit</returns>
ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct); ValueTask<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, string? keySuffix, CancellationToken ct);
/// <summary>
/// Reset the limit for the specified parameters
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <param name="ct">Cancelation token</param>
Task ResetAsync(
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
string? keySuffix,
CancellationToken ct);
} }
} }

View File

@ -40,5 +40,15 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param> /// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
/// <returns></returns> /// <returns></returns>
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix); RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, string? keySuffix);
/// <summary>
/// Reset the limit for the specified parameters
/// </summary>
/// <param name="type">The rate limit item type</param>
/// <param name="definition">The request definition</param>
/// <param name="host">The host address</param>
/// <param name="apiKey">The API key</param>
/// <param name="keySuffix">An additional optional suffix for the key selector. Can be used to make rate limiting work based on parameters.</param>
void Reset(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, string? keySuffix);
} }
} }

View File

@ -30,5 +30,9 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
/// </summary> /// </summary>
/// <param name="weight">Request weight</param> /// <param name="weight">Request weight</param>
void ApplyWeight(int weight); void ApplyWeight(int weight);
/// <summary>
/// Reset the limit counter for this tracker
/// </summary>
void Reset();
} }
} }

View File

@ -192,5 +192,27 @@ namespace CryptoExchange.Net.RateLimiting
_semaphore.Release(); _semaphore.Release();
} }
} }
/// <inheritdoc />
public async Task ResetAsync(
RateLimitItemType type,
RequestDefinition definition,
string host,
string? apiKey,
string? keySuffix,
CancellationToken ct)
{
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
try
{
foreach (var guard in _guards)
guard.Reset(type, definition, host, apiKey, keySuffix);
}
finally
{
_semaphore.Release();
}
}
} }
} }

View File

@ -26,6 +26,13 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
DecreaseRate = decayRate; DecreaseRate = decayRate;
} }
/// <inheritdoc />
public void Reset()
{
_currentWeight = 0;
_lastDecrease = DateTime.UtcNow;
}
/// <inheritdoc /> /// <inheritdoc />
public TimeSpan GetWaitTime(int weight) public TimeSpan GetWaitTime(int weight)
{ {

View File

@ -29,6 +29,14 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
_entries = new Queue<LimitEntry>(); _entries = new Queue<LimitEntry>();
} }
/// <inheritdoc />
public void Reset()
{
_entries.Clear();
_currentWeight = 0;
_nextReset = null;
}
public TimeSpan GetWaitTime(int weight) public TimeSpan GetWaitTime(int weight)
{ {
// Remove requests no longer in time period from the history // Remove requests no longer in time period from the history

View File

@ -28,6 +28,13 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
_entries = new Queue<LimitEntry>(); _entries = new Queue<LimitEntry>();
} }
/// <inheritdoc />
public void Reset()
{
_entries.Clear();
_currentWeight = 0;
}
/// <inheritdoc /> /// <inheritdoc />
public TimeSpan GetWaitTime(int weight) public TimeSpan GetWaitTime(int weight)
{ {

View File

@ -28,6 +28,13 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
_entries = new List<LimitEntry>(); _entries = new List<LimitEntry>();
} }
/// <inheritdoc />
public void Reset()
{
_entries.Clear();
_currentWeight = 0;
}
/// <inheritdoc /> /// <inheritdoc />
public TimeSpan GetWaitTime(int weight) public TimeSpan GetWaitTime(int weight)
{ {

View File

@ -45,7 +45,7 @@ namespace CryptoExchange.Net.SharedApis
public override Error? ValidateRequest(string exchange, GetOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes) public override Error? ValidateRequest(string exchange, GetOrderBookRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
if (request.Limit == null) if (request.Limit == null)
return null; return base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
if (MaxLimit.HasValue && request.Limit.Value > MaxLimit) if (MaxLimit.HasValue && request.Limit.Value > MaxLimit)
return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}"); return ArgumentError.Invalid(nameof(GetOrderBookRequest.Limit), $"Max limit is {MaxLimit}");

View File

@ -22,8 +22,12 @@ namespace CryptoExchange.Net.SharedApis
} }
/// <inheritdoc /> /// <inheritdoc />
public Error? Validate(GetRecentTradesRequest request) public override Error? ValidateRequest(string exchange, GetRecentTradesRequest request, TradingMode? tradingMode, TradingMode[] supportedApiTypes)
{ {
var baseError = base.ValidateRequest(exchange, request, tradingMode, supportedApiTypes);
if (baseError != null)
return baseError;
if (request.Limit > MaxLimit) if (request.Limit > MaxLimit)
return ArgumentError.Invalid(nameof(GetRecentTradesRequest.Limit), $"Only the most recent {MaxLimit} trades are available"); return ArgumentError.Invalid(nameof(GetRecentTradesRequest.Limit), $"Only the most recent {MaxLimit} trades are available");

View File

@ -48,6 +48,7 @@ namespace CryptoExchange.Net.Sockets.Default
private readonly string _baseAddress; private readonly string _baseAddress;
private int _reconnectAttempt; private int _reconnectAttempt;
private readonly int _receiveBufferSize; private readonly int _receiveBufferSize;
private readonly RequestDefinition _requestDefinition;
private const int _sendBufferSize = 4096; private const int _sendBufferSize = 4096;
@ -137,6 +138,7 @@ namespace CryptoExchange.Net.Sockets.Default
_sendBuffer = new ConcurrentQueue<SendItem>(); _sendBuffer = new ConcurrentQueue<SendItem>();
_ctsSource = new CancellationTokenSource(); _ctsSource = new CancellationTokenSource();
_receiveBufferSize = websocketParameters.ReceiveBufferSize ?? 65536; _receiveBufferSize = websocketParameters.ReceiveBufferSize ?? 65536;
_requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
_closeSem = new SemaphoreSlim(1, 1); _closeSem = new SemaphoreSlim(1, 1);
_socket = CreateSocket(); _socket = CreateSocket();
@ -206,8 +208,7 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
if (Parameters.RateLimiter != null) if (Parameters.RateLimiter != null)
{ {
var definition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id }; var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, _requestDefinition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, Id, RateLimitItemType.Connection, definition, _baseAddress, null, 1, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return new CallResult(new ClientRateLimitError("Connection limit reached")); return new CallResult(new ClientRateLimitError("Connection limit reached"));
} }
@ -296,6 +297,9 @@ namespace CryptoExchange.Net.Sockets.Default
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false); await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
} }
if (Parameters.RateLimiter != null)
await Parameters.RateLimiter.ResetAsync(RateLimitItemType.Request, _requestDefinition, _baseAddress, null, null, default).ConfigureAwait(false);
// Delay here to prevent very rapid looping when a connection to the server is accepted and immediately disconnected // Delay here to prevent very rapid looping when a connection to the server is accepted and immediately disconnected
var initialDelay = GetReconnectDelay(); var initialDelay = GetReconnectDelay();
await Task.Delay(initialDelay).ConfigureAwait(false); await Task.Delay(initialDelay).ConfigureAwait(false);
@ -496,7 +500,6 @@ namespace CryptoExchange.Net.Sockets.Default
/// <returns></returns> /// <returns></returns>
private async Task SendLoopAsync() private async Task SendLoopAsync()
{ {
var requestDefinition = new RequestDefinition(Uri.AbsolutePath, HttpMethod.Get) { ConnectionId = Id };
try try
{ {
while (true) while (true)
@ -520,7 +523,7 @@ namespace CryptoExchange.Net.Sockets.Default
{ {
try try
{ {
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false); var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, _requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehavior, null, _ctsSource.Token).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
{ {
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false); await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);

View File

@ -0,0 +1,104 @@
using CryptoExchange.Net.Objects;
using System;
namespace CryptoExchange.Net.Sockets.Default.Routing
{
/// <summary>
/// Message route
/// </summary>
public abstract class MessageRoute
{
/// <summary>
/// Type identifier
/// </summary>
public string TypeIdentifier { get; set; }
/// <summary>
/// Optional topic filter
/// </summary>
public string? TopicFilter { get; set; }
/// <summary>
/// Whether responses to this route might be read by multiple listeners
/// </summary>
public bool MultipleReaders { get; set; } = false;
/// <summary>
/// Deserialization type
/// </summary>
public abstract Type DeserializationType { get; }
/// <summary>
/// ctor
/// </summary>
public MessageRoute(string typeIdentifier, string? topicFilter)
{
TypeIdentifier = typeIdentifier;
TopicFilter = topicFilter;
}
/// <summary>
/// Message handler
/// </summary>
public abstract CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data);
}
/// <summary>
/// Message route
/// </summary>
public class MessageRoute<TMessage> : MessageRoute
{
private Func<SocketConnection, DateTime, string?, TMessage, CallResult?> _handler;
/// <inheritdoc />
public override Type DeserializationType { get; } = typeof(TMessage);
/// <summary>
/// ctor
/// </summary>
internal MessageRoute(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
: base(typeIdentifier, topicFilter)
{
_handler = handler;
MultipleReaders = multipleReaders;
}
/// <summary>
/// Create route without topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithoutTopicFilter(string typeIdentifier, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
{
return new MessageRoute<TMessage>(typeIdentifier, null, handler)
{
MultipleReaders = multipleReaders
};
}
/// <summary>
/// Create route with optional topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
{
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler)
{
MultipleReaders = multipleReaders
};
}
/// <summary>
/// Create route with topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
{
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler)
{
MultipleReaders = multipleReaders
};
}
/// <inheritdoc />
public override CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data)
{
return _handler(connection, receiveTime, originalData, (TMessage)data);
}
}
}

View File

@ -1,16 +1,17 @@
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace CryptoExchange.Net.Sockets namespace CryptoExchange.Net.Sockets.Default.Routing
{ {
/// <summary> /// <summary>
/// Message router /// Message router
/// </summary> /// </summary>
public class MessageRouter public class MessageRouter
{ {
private ProcessorRouter? _routingTable;
/// <summary> /// <summary>
/// The routes registered for this router /// The routes registered for this router
/// </summary> /// </summary>
@ -24,12 +25,40 @@ namespace CryptoExchange.Net.Sockets
Routes = routes; Routes = routes;
} }
/// <summary>
/// Build the route mapping
/// </summary>
public void BuildQueryRouter()
{
_routingTable = new QueryRouter(Routes);
}
/// <summary>
/// Build the route mapping
/// </summary>
public void BuildSubscriptionRouter()
{
_routingTable = new SubscriptionRouter(Routes);
}
/// <summary>
/// Handle message
/// </summary>
public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
{
var routeCollection = (_routingTable ?? throw new NullReferenceException("Routing table not build before handling")).GetRoutes(typeIdentifier);
if (routeCollection == null)
throw new InvalidOperationException($"No routes for {typeIdentifier} message type");
return routeCollection.Handle(topicFilter, connection, receiveTime, originalData, data, out result);
}
/// <summary> /// <summary>
/// Create message router without specific message handler /// Create message router without specific message handler
/// </summary> /// </summary>
public static MessageRouter CreateWithoutHandler<T>(string typeIdentifier, bool multipleReaders = false) public static MessageRouter CreateWithoutHandler<T>(string typeIdentifier, bool multipleReaders = false)
{ {
return new MessageRouter(new MessageRoute<T>(typeIdentifier, (string?)null, (con, receiveTime, originalData, msg) => new CallResult<T>(default, null, null), multipleReaders)); return new MessageRouter(new MessageRoute<T>(typeIdentifier, null, (con, receiveTime, originalData, msg) => new CallResult<T>(default, null, null), multipleReaders));
} }
/// <summary> /// <summary>
@ -165,104 +194,4 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public bool ContainsCheck(MessageRoute route) => Routes.Any(x => x.TypeIdentifier == route.TypeIdentifier && x.TopicFilter == route.TopicFilter); public bool ContainsCheck(MessageRoute route) => Routes.Any(x => x.TypeIdentifier == route.TypeIdentifier && x.TopicFilter == route.TopicFilter);
} }
/// <summary>
/// Message route
/// </summary>
public abstract class MessageRoute
{
/// <summary>
/// Type identifier
/// </summary>
public string TypeIdentifier { get; set; }
/// <summary>
/// Optional topic filter
/// </summary>
public string? TopicFilter { get; set; }
/// <summary>
/// Whether responses to this route might be read by multiple listeners
/// </summary>
public bool MultipleReaders { get; set; } = false;
/// <summary>
/// Deserialization type
/// </summary>
public abstract Type DeserializationType { get; }
/// <summary>
/// ctor
/// </summary>
public MessageRoute(string typeIdentifier, string? topicFilter)
{
TypeIdentifier = typeIdentifier;
TopicFilter = topicFilter;
}
/// <summary>
/// Message handler
/// </summary>
public abstract CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data);
}
/// <summary>
/// Message route
/// </summary>
public class MessageRoute<TMessage> : MessageRoute
{
private Func<SocketConnection, DateTime, string?, TMessage, CallResult?> _handler;
/// <inheritdoc />
public override Type DeserializationType { get; } = typeof(TMessage);
/// <summary>
/// ctor
/// </summary>
internal MessageRoute(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
: base(typeIdentifier, topicFilter)
{
_handler = handler;
MultipleReaders = multipleReaders;
}
/// <summary>
/// Create route without topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithoutTopicFilter(string typeIdentifier, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
{
return new MessageRoute<TMessage>(typeIdentifier, null, handler)
{
MultipleReaders = multipleReaders
};
}
/// <summary>
/// Create route with optional topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
{
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler)
{
MultipleReaders = multipleReaders
};
}
/// <summary>
/// Create route with topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult?> handler, bool multipleReaders = false)
{
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler)
{
MultipleReaders = multipleReaders
};
}
/// <inheritdoc />
public override CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data)
{
return _handler(connection, receiveTime, originalData, (TMessage)data);
}
}
} }

View File

@ -0,0 +1,36 @@
using System.Collections.Generic;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
namespace CryptoExchange.Net.Sockets.Default.Routing
{
internal abstract class ProcessorRouter
{
public abstract RouteCollection? GetRoutes(string identifier);
}
internal abstract class ProcessorRouter<T> : ProcessorRouter
where T : RouteCollection
{
#if NET8_0_OR_GREATER
private FrozenDictionary<string, T> _routeMap;
#else
private Dictionary<string, T> _routeMap;
#endif
public ProcessorRouter(IEnumerable<MessageRoute> routes)
{
var map = BuildFromRoutes(routes);
#if NET8_0_OR_GREATER
_routeMap = map.ToFrozenDictionary();
#else
_routeMap = map;
#endif
}
public abstract Dictionary<string, T> BuildFromRoutes(IEnumerable<MessageRoute> routes);
public override RouteCollection? GetRoutes(string identifier) => _routeMap.TryGetValue(identifier, out var routes) ? routes : null;
}
}

View File

@ -0,0 +1,90 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Sockets.Default.Routing
{
internal class QueryRouter : ProcessorRouter<QueryRouteCollection>
{
public QueryRouter(IEnumerable<MessageRoute> routes) : base(routes)
{
}
public override Dictionary<string, QueryRouteCollection> BuildFromRoutes(IEnumerable<MessageRoute> routes)
{
var newMap = new Dictionary<string, QueryRouteCollection>();
foreach (var route in routes)
{
if (!newMap.TryGetValue(route.TypeIdentifier, out var typeMap))
{
typeMap = new QueryRouteCollection(route.DeserializationType);
newMap.Add(route.TypeIdentifier, typeMap);
}
typeMap.AddRoute(route.TopicFilter, route);
}
foreach (var subEntry in newMap.Values)
subEntry.Build();
return newMap;
}
}
internal class QueryRouteCollection : RouteCollection
{
public bool MultipleReaders { get; private set; }
public QueryRouteCollection(Type routeType) : base(routeType)
{
}
public override void AddRoute(string? topicFilter, MessageRoute route)
{
base.AddRoute(topicFilter, route);
if (route.MultipleReaders)
MultipleReaders = true;
}
public override bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
{
result = null;
// Routes without topic filter handle both when the message topic is empty and when it is not, so we always call them
var handled = false;
foreach (var route in _routesWithoutTopicFilter)
{
var thisResult = route.Handle(connection, receiveTime, originalData, data);
if (thisResult != null)
result ??= thisResult;
handled = true;
}
// Forward to routes with matching topic filter, if any
if (topicFilter == null)
return handled;
var matchingTopicRoutes = GetRoutesWithMatchingTopicFilter(topicFilter);
if (matchingTopicRoutes == null)
return handled;
foreach (var route in matchingTopicRoutes)
{
var thisResult = route.Handle(connection, receiveTime, originalData, data);
handled = true;
if (thisResult != null)
{
result ??= thisResult;
if (!MultipleReaders)
break;
}
}
return handled;
}
}
}

View File

@ -0,0 +1,65 @@
using CryptoExchange.Net.Objects;
using System;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
using System.Collections.Generic;
namespace CryptoExchange.Net.Sockets.Default.Routing
{
internal abstract class RouteCollection
{
protected List<MessageRoute> _routesWithoutTopicFilter;
protected Dictionary<string, List<MessageRoute>> _routesWithTopicFilter;
#if NET8_0_OR_GREATER
protected FrozenDictionary<string, List<MessageRoute>>? _routesWithTopicFilterFrozen;
#endif
public Type DeserializationType { get; }
public RouteCollection(Type routeType)
{
_routesWithoutTopicFilter = new List<MessageRoute>();
_routesWithTopicFilter = new Dictionary<string, List<MessageRoute>>();
DeserializationType = routeType;
}
public virtual void AddRoute(string? topicFilter, MessageRoute route)
{
if (string.IsNullOrEmpty(topicFilter))
{
_routesWithoutTopicFilter.Add(route);
}
else
{
if (!_routesWithTopicFilter.TryGetValue(topicFilter!, out var list))
{
list = new List<MessageRoute>();
_routesWithTopicFilter.Add(topicFilter!, list);
}
list.Add(route);
}
}
public void Build()
{
#if NET8_0_OR_GREATER
_routesWithTopicFilterFrozen = _routesWithTopicFilter.ToFrozenDictionary();
#endif
}
protected List<MessageRoute>? GetRoutesWithMatchingTopicFilter(string topicFilter)
{
#if NET8_0_OR_GREATER
_routesWithTopicFilterFrozen!.TryGetValue(topicFilter, out var matchingTopicRoutes);
#else
_routesWithTopicFilter.TryGetValue(topicFilter, out var matchingTopicRoutes);
#endif
return matchingTopicRoutes;
}
public abstract bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result);
}
}

View File

@ -0,0 +1,111 @@
using CryptoExchange.Net.Sockets.Interfaces;
using System;
using System.Collections.Generic;
using System.Text;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
namespace CryptoExchange.Net.Sockets.Default.Routing
{
/// <summary>
/// Routing table
/// </summary>
public class RoutingTable
{
#if NET8_0_OR_GREATER
private FrozenDictionary<string, TypeRoutingCollection> _typeRoutingCollections = new Dictionary<string, TypeRoutingCollection>().ToFrozenDictionary();
#else
private Dictionary<string, TypeRoutingCollection> _typeRoutingCollections = new();
#endif
/// <summary>
/// Update the routing table
/// </summary>
/// <param name="processors"></param>
public void Update(IEnumerable<IMessageProcessor> processors)
{
var newTypeMap = new Dictionary<string, TypeRoutingCollection>();
foreach (var entry in processors)
{
foreach (var route in entry.MessageRouter.Routes)
{
if (!newTypeMap.ContainsKey(route.TypeIdentifier))
newTypeMap.Add(route.TypeIdentifier, new TypeRoutingCollection(route.DeserializationType));
if (!newTypeMap[route.TypeIdentifier].Handlers.Contains(entry))
newTypeMap[route.TypeIdentifier].Handlers.Add(entry);
}
}
#if NET8_0_OR_GREATER
_typeRoutingCollections = newTypeMap.ToFrozenDictionary();
#else
_typeRoutingCollections = newTypeMap;
#endif
}
/// <summary>
/// Get route table entry for a type identifier
/// </summary>
public TypeRoutingCollection? GetRouteTableEntry(string typeIdentifier)
{
return _typeRoutingCollections.TryGetValue(typeIdentifier, out var entry) ? entry : null;
}
/// <inheritdoc />
public override string ToString()
{
var sb = new StringBuilder();
foreach (var entry in _typeRoutingCollections)
{
sb.AppendLine($"{entry.Key}, {entry.Value.DeserializationType.Name}");
foreach(var item in entry.Value.Handlers)
{
sb.AppendLine($" - Processor {item.GetType().Name}");
foreach(var route in item.MessageRouter.Routes)
{
if (route.TypeIdentifier == entry.Key)
{
if (route.TopicFilter == null)
sb.AppendLine($" - Route without topic filter");
else
sb.AppendLine($" - Route with topic filter {route.TopicFilter}");
}
}
}
}
return sb.ToString();
}
}
/// <summary>
/// Routing table entry
/// </summary>
public record TypeRoutingCollection
{
/// <summary>
/// Whether the deserialization type is string
/// </summary>
public bool IsStringOutput { get; set; }
/// <summary>
/// The deserialization type
/// </summary>
public Type DeserializationType { get; set; }
/// <summary>
/// Message processors
/// </summary>
public List<IMessageProcessor> Handlers { get; set; }
/// <summary>
/// ctor
/// </summary>
public TypeRoutingCollection(Type deserializationType)
{
IsStringOutput = deserializationType == typeof(string);
DeserializationType = deserializationType;
Handlers = new List<IMessageProcessor>();
}
}
}

View File

@ -0,0 +1,69 @@
using CryptoExchange.Net.Objects;
using System;
using System.Collections.Generic;
namespace CryptoExchange.Net.Sockets.Default.Routing
{
internal class SubscriptionRouter : ProcessorRouter<SubscriptionRouteCollection>
{
public SubscriptionRouter(IEnumerable<MessageRoute> routes) : base(routes)
{
}
public override Dictionary<string, SubscriptionRouteCollection> BuildFromRoutes(IEnumerable<MessageRoute> routes)
{
var newMap = new Dictionary<string, SubscriptionRouteCollection>();
foreach (var route in routes)
{
if (!newMap.TryGetValue(route.TypeIdentifier, out var typeMap))
{
typeMap = new SubscriptionRouteCollection(route.DeserializationType);
newMap.Add(route.TypeIdentifier, typeMap);
}
typeMap.AddRoute(route.TopicFilter, route);
}
foreach (var subEntry in newMap.Values)
subEntry.Build();
return newMap;
}
}
internal class SubscriptionRouteCollection : RouteCollection
{
public SubscriptionRouteCollection(Type routeType) : base(routeType)
{
}
public override bool Handle(string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data, out CallResult? result)
{
result = CallResult.SuccessResult;
// Routes without topic filter handle both when the message topic is empty and when it is not, so we always call them
var handled = false;
foreach (var route in _routesWithoutTopicFilter)
{
route.Handle(connection, receiveTime, originalData, data);
handled = true;
}
// Forward to routes with matching topic filter, if any
if (topicFilter == null)
return handled;
var matchingTopicRoutes = GetRoutesWithMatchingTopicFilter(topicFilter);
if (matchingTopicRoutes == null)
return handled;
foreach (var route in matchingTopicRoutes)
{
route.Handle(connection, receiveTime, originalData, data);
handled = true;
}
return handled;
}
}
}

View File

@ -5,13 +5,12 @@ using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Sockets.Default.Interfaces; using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.Sockets.Interfaces; using CryptoExchange.Net.Sockets.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net.WebSockets; using System.Net.WebSockets;
using System.Text; using System.Text;
@ -262,6 +261,9 @@ namespace CryptoExchange.Net.Sockets.Default
#else #else
private readonly object _listenersLock = new object(); private readonly object _listenersLock = new object();
#endif #endif
private RoutingTable _routingTable = new RoutingTable();
private ReadOnlyCollection<IMessageProcessor> _listeners; private ReadOnlyCollection<IMessageProcessor> _listeners;
private readonly ILogger _logger; private readonly ILogger _logger;
private SocketStatus _status; private SocketStatus _status;
@ -489,6 +491,14 @@ namespace CryptoExchange.Net.Sockets.Default
/// </summary> /// </summary>
protected internal virtual void HandleStreamMessage2(WebSocketMessageType type, ReadOnlySpan<byte> data) protected internal virtual void HandleStreamMessage2(WebSocketMessageType type, ReadOnlySpan<byte> data)
{ {
// Forward message rules:
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
// | N | N | - | Y | No topic filter applied
// | N | Y | - | N | Route only listens to specific topic
// | Y | N | - | Y | Route listens to all message regardless of topic
// | Y | Y | Y | Y | Route listens to specific message topic
// | Y | Y | N | N | Route listens to different topic
var receiveTime = DateTime.UtcNow; var receiveTime = DateTime.UtcNow;
// 1. Decrypt/Preprocess if necessary // 1. Decrypt/Preprocess if necessary
@ -521,38 +531,22 @@ namespace CryptoExchange.Net.Sockets.Default
return; return;
} }
Type? deserializationType = null; var routingEntry = _routingTable.GetRouteTableEntry(typeIdentifier);
foreach (var subscription in _listeners) if (routingEntry == null)
{
foreach (var route in subscription.MessageRouter.Routes)
{
if (!route.TypeIdentifier.Equals(typeIdentifier, StringComparison.Ordinal))
continue;
deserializationType = route.DeserializationType;
break;
}
if (deserializationType != null)
break;
}
if (deserializationType == null)
{ {
if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data)) if (!ApiClient.HandleUnhandledMessage(this, typeIdentifier, data))
{ {
// No handler found for identifier either, can't process // No handler found for identifier either, can't process
_logger.LogWarning("Failed to determine message type for identifier {Identifier}. Data: {Message}", typeIdentifier, Encoding.UTF8.GetString(data.ToArray())); _logger.LogWarning("Failed to determine message type for identifier {Identifier}. Data: {Message}", typeIdentifier, Encoding.UTF8.GetString(data.ToArray()));
} }
return; return;
} }
object result; object result;
try try
{ {
if (deserializationType == typeof(string)) if (routingEntry.IsStringOutput)
{ {
#if NETSTANDARD2_0 #if NETSTANDARD2_0
result = Encoding.UTF8.GetString(data.ToArray()); result = Encoding.UTF8.GetString(data.ToArray());
@ -562,7 +556,7 @@ namespace CryptoExchange.Net.Sockets.Default
} }
else else
{ {
result = messageConverter.Deserialize(data, deserializationType); result = messageConverter.Deserialize(data, routingEntry.DeserializationType);
} }
} }
catch(Exception ex) catch(Exception ex)
@ -579,60 +573,12 @@ namespace CryptoExchange.Net.Sockets.Default
} }
var topicFilter = messageConverter.GetTopicFilter(result); var topicFilter = messageConverter.GetTopicFilter(result);
var processed = false;
bool processed = false; foreach (var handler in routingEntry.Handlers)
foreach (var processor in _listeners)
{ {
bool isQuery = false; var thisHandled = handler.Handle(typeIdentifier, topicFilter, this, receiveTime, originalData, result);
Query? query = null; if (thisHandled)
if (processor is Query cquery)
{
isQuery = true;
query = cquery;
}
var complete = false;
foreach (var route in processor.MessageRouter.Routes)
{
if (route.TypeIdentifier != typeIdentifier)
continue;
// Forward message rules:
// | Message Topic | Route Topic Filter | Topics Match | Forward | Description
// | N | N | - | Y | No topic filter applied
// | N | Y | - | N | Route only listens to specific topic
// | Y | N | - | Y | Route listens to all message regardless of topic
// | Y | Y | Y | Y | Route listens to specific message topic
// | Y | Y | N | N | Route listens to different topic
if (topicFilter == null)
{
if (route.TopicFilter != null)
// No topic on message, but route is filtering on topic
continue;
}
else
{
if (route.TopicFilter != null && !route.TopicFilter.Equals(topicFilter, StringComparison.Ordinal))
// Message has a topic, and the route has a filter for another topic
continue;
}
processed = true; processed = true;
if (isQuery && query!.Completed)
continue;
processor.Handle(this, receiveTime, originalData, result, route);
if (isQuery && !route.MultipleReaders)
{
complete = true;
break;
}
}
if (complete)
break;
} }
if (!processed) if (!processed)
@ -1193,13 +1139,26 @@ namespace CryptoExchange.Net.Sockets.Default
}); });
} }
private void UpdateRoutingTable()
{
_routingTable.Update(_listeners);
}
private void AddMessageProcessor(IMessageProcessor processor) private void AddMessageProcessor(IMessageProcessor processor)
{ {
lock (_listenersLock) lock (_listenersLock)
{ {
var updatedList = new List<IMessageProcessor>(_listeners); var updatedList = new List<IMessageProcessor>(_listeners);
updatedList.Add(processor); updatedList.Add(processor);
processor.OnMessageRouterUpdated += UpdateRoutingTable;
_listeners = updatedList.AsReadOnly(); _listeners = updatedList.AsReadOnly();
if (processor.MessageRouter.Routes.Length > 0)
{
UpdateRoutingTable();
#if DEBUG
_logger.LogTrace("Processor added, new routing table:\r\n" + _routingTable.ToString());
#endif
}
} }
} }
@ -1208,8 +1167,15 @@ namespace CryptoExchange.Net.Sockets.Default
lock (_listenersLock) lock (_listenersLock)
{ {
var updatedList = new List<IMessageProcessor>(_listeners); var updatedList = new List<IMessageProcessor>(_listeners);
updatedList.Remove(processor); processor.OnMessageRouterUpdated -= UpdateRoutingTable;
if (!updatedList.Remove(processor))
return; // If nothing removed nothing has changed
_listeners = updatedList.AsReadOnly(); _listeners = updatedList.AsReadOnly();
UpdateRoutingTable();
#if DEBUG
_logger.LogTrace("Processor removed, new routing table:\r\n" + _routingTable.ToString());
#endif
} }
} }
@ -1218,12 +1184,24 @@ namespace CryptoExchange.Net.Sockets.Default
lock (_listenersLock) lock (_listenersLock)
{ {
var updatedList = new List<IMessageProcessor>(_listeners); var updatedList = new List<IMessageProcessor>(_listeners);
var anyRemoved = false;
foreach (var processor in processors) foreach (var processor in processors)
updatedList.Remove(processor); {
processor.OnMessageRouterUpdated -= UpdateRoutingTable;
if (updatedList.Remove(processor))
anyRemoved = true;
}
if (!anyRemoved)
return; // If nothing removed nothing has changed
_listeners = updatedList.AsReadOnly(); _listeners = updatedList.AsReadOnly();
UpdateRoutingTable();
#if DEBUG
_logger.LogTrace("Processors removed, new routing table:\r\n" + _routingTable.ToString());
#endif
} }
} }
} }
} }

View File

@ -1,5 +1,6 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.Sockets.Interfaces; using CryptoExchange.Net.Sockets.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
@ -70,10 +71,21 @@ namespace CryptoExchange.Net.Sockets.Default
/// </summary> /// </summary>
public bool Authenticated { get; } public bool Authenticated { get; }
private MessageRouter _router;
/// <summary> /// <summary>
/// Router for this subscription /// Router for this subscription
/// </summary> /// </summary>
public MessageRouter MessageRouter { get; set; } public MessageRouter MessageRouter
{
get => _router;
set
{
_router = value;
_router.BuildSubscriptionRouter();
OnMessageRouterUpdated?.Invoke();
}
}
/// <summary> /// <summary>
/// Cancellation token registration /// Cancellation token registration
@ -109,6 +121,9 @@ namespace CryptoExchange.Net.Sockets.Default
/// </summary> /// </summary>
public int IndividualSubscriptionCount { get; set; } = 1; public int IndividualSubscriptionCount { get; set; } = 1;
/// <inheritdoc />
public event Action? OnMessageRouterUpdated;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@ -170,10 +185,11 @@ namespace CryptoExchange.Net.Sockets.Default
/// <summary> /// <summary>
/// Handle an update message /// Handle an update message
/// </summary> /// </summary>
public CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data, MessageRoute route) public bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object data)
{ {
ConnectionInvocations++; ConnectionInvocations++;
TotalInvocations++; TotalInvocations++;
if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed) if (SubscriptionQuery != null && !SubscriptionQuery.Completed && SubscriptionQuery.TimeoutBehavior == TimeoutBehavior.Succeed)
{ {
// The subscription query is one where it is successful if there is no error returned // The subscription query is one where it is successful if there is no error returned
@ -182,7 +198,7 @@ namespace CryptoExchange.Net.Sockets.Default
SubscriptionQuery.Timeout(); SubscriptionQuery.Timeout();
} }
return route.Handle(connection, receiveTime, originalData, data); return MessageRouter.Handle(typeIdentifier, topicFilter, connection, receiveTime, originalData, data, out _);
} }
/// <summary> /// <summary>

View File

@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default; using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using System; using System;
namespace CryptoExchange.Net.Sockets.Interfaces namespace CryptoExchange.Net.Sockets.Interfaces
@ -19,8 +20,12 @@ namespace CryptoExchange.Net.Sockets.Interfaces
/// </summary> /// </summary>
public MessageRouter MessageRouter { get; } public MessageRouter MessageRouter { get; }
/// <summary> /// <summary>
/// Event when the message router for this processor has been changed
/// </summary>
public event Action? OnMessageRouterUpdated;
/// <summary>
/// Handle a message /// Handle a message
/// </summary> /// </summary>
CallResult? Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageRoute route); bool Handle(string typeIdentifier, string? topicFilter, SocketConnection socketConnection, DateTime receiveTime, string? originalData, object result);
} }
} }

View File

@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Sockets.Default; using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Routing;
using CryptoExchange.Net.Sockets.Interfaces; using CryptoExchange.Net.Sockets.Interfaces;
using System; using System;
using System.Threading; using System.Threading;
@ -59,10 +60,20 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public object? Response { get; set; } public object? Response { get; set; }
private MessageRouter _router;
/// <summary> /// <summary>
/// Router for this query /// Router for this query
/// </summary> /// </summary>
public MessageRouter MessageRouter { get; set; } public MessageRouter MessageRouter
{
get => _router;
set
{
_router = value;
_router.BuildQueryRouter();
OnMessageRouterUpdated?.Invoke();
}
}
/// <summary> /// <summary>
/// The query request object /// The query request object
@ -99,6 +110,9 @@ namespace CryptoExchange.Net.Sockets
/// </summary> /// </summary>
public Action? OnComplete { get; set; } public Action? OnComplete { get; set; }
/// <inheritdoc />
public event Action? OnMessageRouterUpdated;
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@ -155,7 +169,7 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Handle a response message /// Handle a response message
/// </summary> /// </summary>
public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route); public abstract bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object message);
} }
@ -185,17 +199,23 @@ namespace CryptoExchange.Net.Sockets
} }
/// <inheritdoc /> /// <inheritdoc />
public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route) public override bool Handle(string typeIdentifier, string? topicFilter, SocketConnection connection, DateTime receiveTime, string? originalData, object message)
{ {
if (Completed)
return false;
CurrentResponses++; CurrentResponses++;
if (CurrentResponses == RequiredResponses) if (CurrentResponses == RequiredResponses)
Response = message; Response = message;
var handled = false;
if (Result?.Success != false) if (Result?.Success != false)
{ {
// If an error result is already set don't override that // If an error result is already set don't override that
Result = route.Handle(connection, receiveTime, originalData, message); MessageRouter.Handle(typeIdentifier, topicFilter, connection, receiveTime, originalData, message, out var result);
if (Result == null) Result = result;
handled = Result != null;
if (!handled)
// Null from Handle means it wasn't actually for this query // Null from Handle means it wasn't actually for this query
CurrentResponses -= 1; CurrentResponses -= 1;
} }
@ -207,7 +227,7 @@ namespace CryptoExchange.Net.Sockets
OnComplete?.Invoke(); OnComplete?.Invoke();
} }
return Result ?? CallResult.SuccessResult; return handled;
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -15,6 +15,9 @@ namespace CryptoExchange.Net.Testing
if (message.Contains("Received null or empty enum value")) if (message.Contains("Received null or empty enum value"))
throw new Exception("Enum null error: " + message); throw new Exception("Enum null error: " + message);
if (message.Contains("Enum mapping sub-optimal."))
throw new Exception("Enum mapping error: " + message);
} }
public override void WriteLine(string? message) public override void WriteLine(string? message)
@ -27,6 +30,9 @@ namespace CryptoExchange.Net.Testing
if (message.Contains("Received null or empty enum value")) if (message.Contains("Received null or empty enum value"))
throw new Exception("Enum null error: " + message); throw new Exception("Enum null error: " + message);
if (message.Contains("Enum mapping sub-optimal."))
throw new Exception("Enum mapping error: " + message);
} }
} }
} }

View File

@ -48,6 +48,7 @@ namespace CryptoExchange.Net.Testing
/// <param name="methodInvoke">Method invocation</param> /// <param name="methodInvoke">Method invocation</param>
/// <param name="name">Method name for looking up json test values</param> /// <param name="name">Method name for looking up json test values</param>
/// <param name="endpointOptions">Request options</param> /// <param name="endpointOptions">Request options</param>
/// <param name="validation">Callback to validate the response model</param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="Exception"></exception> /// <exception cref="Exception"></exception>
public Task ValidateAsync<TResponse>( public Task ValidateAsync<TResponse>(
@ -65,6 +66,7 @@ namespace CryptoExchange.Net.Testing
/// <param name="methodInvoke">Method invocation</param> /// <param name="methodInvoke">Method invocation</param>
/// <param name="name">Method name for looking up json test values</param> /// <param name="name">Method name for looking up json test values</param>
/// <param name="endpointOptions">Request options</param> /// <param name="endpointOptions">Request options</param>
/// <param name="validation">Callback to validate the response model</param>
/// <returns></returns> /// <returns></returns>
/// <exception cref="Exception"></exception> /// <exception cref="Exception"></exception>
public async Task ValidateAsync<TResponse, TActualResponse>( public async Task ValidateAsync<TResponse, TActualResponse>(

View File

@ -5,32 +5,33 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Binance.Net" Version="12.11.0" /> <PackageReference Include="Binance.Net" Version="12.11.3" />
<PackageReference Include="Bitfinex.Net" Version="10.10.1" /> <PackageReference Include="Bitfinex.Net" Version="10.10.2" />
<PackageReference Include="BitMart.Net" Version="3.9.1" /> <PackageReference Include="BitMart.Net" Version="3.10.0" />
<PackageReference Include="BloFin.Net" Version="2.10.1" /> <PackageReference Include="BloFin.Net" Version="2.10.2" />
<PackageReference Include="Bybit.Net" Version="6.10.0" /> <PackageReference Include="Bybit.Net" Version="6.11.0" />
<PackageReference Include="CoinEx.Net" Version="10.9.1" /> <PackageReference Include="CoinEx.Net" Version="10.9.2" />
<PackageReference Include="CoinW.Net" Version="2.9.1" /> <PackageReference Include="CoinW.Net" Version="2.9.2" />
<PackageReference Include="CryptoCom.Net" Version="3.9.1" /> <PackageReference Include="CryptoCom.Net" Version="3.10.0" />
<PackageReference Include="DeepCoin.Net" Version="3.9.1" /> <PackageReference Include="DeepCoin.Net" Version="3.9.2" />
<PackageReference Include="GateIo.Net" Version="3.10.1" /> <PackageReference Include="GateIo.Net" Version="3.10.2" />
<PackageReference Include="HyperLiquid.Net" Version="4.0.1" /> <PackageReference Include="HyperLiquid.Net" Version="4.3.0" />
<PackageReference Include="JK.BingX.Net" Version="3.9.1" /> <PackageReference Include="JK.BingX.Net" Version="3.10.0" />
<PackageReference Include="JK.Bitget.Net" Version="3.9.0" /> <PackageReference Include="JK.Bitget.Net" Version="3.10.0" />
<PackageReference Include="JK.Mexc.Net" Version="4.9.0" /> <PackageReference Include="JK.Mexc.Net" Version="5.0.1" />
<PackageReference Include="JK.OKX.Net" Version="4.10.1" /> <PackageReference Include="JK.OKX.Net" Version="4.12.0" />
<PackageReference Include="Jkorf.Aster.Net" Version="3.0.0" /> <PackageReference Include="Jkorf.Aster.Net" Version="3.1.0" />
<PackageReference Include="JKorf.BitMEX.Net" Version="3.9.1" /> <PackageReference Include="JKorf.BitMEX.Net" Version="3.9.2" />
<PackageReference Include="JKorf.Coinbase.Net" Version="3.9.1" /> <PackageReference Include="JKorf.Coinbase.Net" Version="3.9.2" />
<PackageReference Include="JKorf.HTX.Net" Version="8.9.0" /> <PackageReference Include="JKorf.HTX.Net" Version="8.9.1" />
<PackageReference Include="JKorf.Upbit.Net" Version="2.9.0" /> <PackageReference Include="JKorf.Upbit.Net" Version="2.9.2" />
<PackageReference Include="KrakenExchange.Net" Version="7.9.0" /> <PackageReference Include="KrakenExchange.Net" Version="7.9.1" />
<PackageReference Include="Kucoin.Net" Version="8.10.1" /> <PackageReference Include="Kucoin.Net" Version="8.11.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" /> <PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Toobit.Net" Version="3.9.1" /> <PackageReference Include="Toobit.Net" Version="3.9.2" />
<PackageReference Include="WhiteBit.Net" Version="3.9.1" /> <PackageReference Include="Weex.Net" Version="1.0.0" />
<PackageReference Include="XT.Net" Version="3.9.1" /> <PackageReference Include="WhiteBit.Net" Version="3.9.2" />
<PackageReference Include="XT.Net" Version="3.9.2" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -22,6 +22,7 @@
@inject IOKXRestClient okxClient @inject IOKXRestClient okxClient
@inject IToobitRestClient toobitClient @inject IToobitRestClient toobitClient
@inject IUpbitRestClient upbitClient @inject IUpbitRestClient upbitClient
@inject IWeexRestClient weexClient
@inject IWhiteBitRestClient whitebitClient @inject IWhiteBitRestClient whitebitClient
@inject IXTRestClient xtClient @inject IXTRestClient xtClient
@ -59,6 +60,7 @@
var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT"); var okxTask = okxClient.UnifiedApi.ExchangeData.GetTickerAsync("BTC-USDT");
var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT"); var toobitTask = toobitClient.SpotApi.ExchangeData.GetTickersAsync("BTCUSDT");
var upbitTask = upbitClient.SpotApi.ExchangeData.GetTickerAsync("USDT-BTC"); var upbitTask = upbitClient.SpotApi.ExchangeData.GetTickerAsync("USDT-BTC");
var weexTask = weexClient.SpotApi.ExchangeData.GetTickersAsync(["BTCUSDT"]);
var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync(); var whitebitTask = whitebitClient.V4Api.ExchangeData.GetTickersAsync();
var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt"); var xtTask = xtClient.SpotApi.ExchangeData.GetTickersAsync("btc_usdt");
@ -141,6 +143,9 @@
if (upbitTask.Result.Success) if (upbitTask.Result.Success)
_prices.Add("Upbit", upbitTask.Result.Data.LastPrice ?? 0); _prices.Add("Upbit", upbitTask.Result.Data.LastPrice ?? 0);
if (weexTask.Result.Success)
_prices.Add("Weex", weexTask.Result.Data.Single().LastPrice);
if (whitebitTask.Result.Success){ if (whitebitTask.Result.Success){
// WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side // WhiteBit API doesn't offer an endpoint to filter for a specific ticker, so we have to filter client side
var tickers = whitebitTask.Result.Data; var tickers = whitebitTask.Result.Data;

View File

@ -22,6 +22,7 @@
@inject IOKXSocketClient okxSocketClient @inject IOKXSocketClient okxSocketClient
@inject IToobitSocketClient toobitSocketClient @inject IToobitSocketClient toobitSocketClient
@inject IUpbitSocketClient upbitSocketClient @inject IUpbitSocketClient upbitSocketClient
@inject IWeexSocketClient weexSocketClient
@inject IWhiteBitSocketClient whitebitSocketClient @inject IWhiteBitSocketClient whitebitSocketClient
@inject IXTSocketClient xtSocketClient @inject IXTSocketClient xtSocketClient
@using System.Collections.Concurrent @using System.Collections.Concurrent

View File

@ -28,6 +28,7 @@
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using Upbit.Net.Interfaces; @using Upbit.Net.Interfaces;
@using Toobit.Net.Interfaces; @using Toobit.Net.Interfaces;
@using Weex.Net.Interfaces
@using WhiteBit.Net.Interfaces @using WhiteBit.Net.Interfaces
@using XT.Net.Interfaces @using XT.Net.Interfaces
@inject IAsterOrderBookFactory asterFactory @inject IAsterOrderBookFactory asterFactory
@ -53,6 +54,7 @@
@inject IOKXOrderBookFactory okxFactory @inject IOKXOrderBookFactory okxFactory
@inject IToobitOrderBookFactory toobitFactory @inject IToobitOrderBookFactory toobitFactory
@inject IUpbitOrderBookFactory upbitFactory @inject IUpbitOrderBookFactory upbitFactory
@inject IWeexOrderBookFactory weexFactory
@inject IWhiteBitOrderBookFactory whitebitFactory @inject IWhiteBitOrderBookFactory whitebitFactory
@inject IXTOrderBookFactory xtFactory @inject IXTOrderBookFactory xtFactory
@implements IDisposable @implements IDisposable
@ -112,6 +114,7 @@
{ "OKX", okxFactory.Create("ETH-BTC") }, { "OKX", okxFactory.Create("ETH-BTC") },
{ "Toobit", toobitFactory.CreateSpot("ETHUSDT") }, { "Toobit", toobitFactory.CreateSpot("ETHUSDT") },
{ "Upbit", upbitFactory.CreateSpot("BTC-ETH") }, { "Upbit", upbitFactory.CreateSpot("BTC-ETH") },
{ "Weex", weexFactory.CreateSpot("ETHUSDT") },
{ "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") }, { "WhiteBit", whitebitFactory.CreateV4("ETH_BTC") },
{ "XT", xtFactory.CreateSpot("eth_btc") }, { "XT", xtFactory.CreateSpot("eth_btc") },
}; };

View File

@ -28,6 +28,7 @@
@using OKX.Net.Interfaces; @using OKX.Net.Interfaces;
@using Upbit.Net.Interfaces; @using Upbit.Net.Interfaces;
@using Toobit.Net.Interfaces; @using Toobit.Net.Interfaces;
@using Weex.Net.Interfaces
@using WhiteBit.Net.Interfaces @using WhiteBit.Net.Interfaces
@using XT.Net.Interfaces @using XT.Net.Interfaces
@inject IAsterTrackerFactory asterFactory @inject IAsterTrackerFactory asterFactory
@ -53,6 +54,7 @@
@inject IOKXTrackerFactory okxFactory @inject IOKXTrackerFactory okxFactory
@inject IToobitTrackerFactory toobitFactory @inject IToobitTrackerFactory toobitFactory
@inject IUpbitTrackerFactory upbitFactory @inject IUpbitTrackerFactory upbitFactory
@inject IWeexTrackerFactory weexFactory
@inject IWhiteBitTrackerFactory whitebitFactory @inject IWhiteBitTrackerFactory whitebitFactory
@inject IXTTrackerFactory xtFactory @inject IXTTrackerFactory xtFactory
@implements IDisposable @implements IDisposable
@ -105,6 +107,7 @@
{ okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { okxFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ toobitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { toobitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ upbitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { upbitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ weexFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ whitebitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { whitebitFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
{ xtFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) }, { xtFactory.CreateTradeTracker(symbol, period: TimeSpan.FromMinutes(5)) },
}; };

View File

@ -56,6 +56,7 @@ namespace BlazorClient
services.AddOKX(); services.AddOKX();
services.AddToobit(); services.AddToobit();
services.AddUpbit(); services.AddUpbit();
services.AddWeex();
services.AddWhiteBit(); services.AddWhiteBit();
services.AddXT(); services.AddXT();
} }

View File

@ -31,6 +31,7 @@
@using OKX.Net.Interfaces.Clients; @using OKX.Net.Interfaces.Clients;
@using Upbit.Net.Interfaces.Clients; @using Upbit.Net.Interfaces.Clients;
@using Toobit.Net.Interfaces.Clients; @using Toobit.Net.Interfaces.Clients;
@using Weex.Net.Interfaces.Clients
@using WhiteBit.Net.Interfaces.Clients @using WhiteBit.Net.Interfaces.Clients
@using XT.Net.Interfaces.Clients @using XT.Net.Interfaces.Clients
@using CryptoExchange.Net.Interfaces; @using CryptoExchange.Net.Interfaces;

View File

@ -38,6 +38,7 @@ Full list of all libraries part of the CryptoExchange.Net ecosystem. Consider us
|![Polymarket](https://raw.githubusercontent.com/JKorf/Polymarket.Net/main/Polymarket.Net/Icon/icon.png)|Polymarket|DEX|[JKorf/Polymarket.Net](https://github.com/JKorf/Polymarket.Net)|[![Nuget version](https://img.shields.io/nuget/v/Polymarket.net.svg?style=flat-square)](https://www.nuget.org/packages/Polymarket.Net)|-|-| |![Polymarket](https://raw.githubusercontent.com/JKorf/Polymarket.Net/main/Polymarket.Net/Icon/icon.png)|Polymarket|DEX|[JKorf/Polymarket.Net](https://github.com/JKorf/Polymarket.Net)|[![Nuget version](https://img.shields.io/nuget/v/Polymarket.net.svg?style=flat-square)](https://www.nuget.org/packages/Polymarket.Net)|-|-|
|![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-| |![Toobit](https://raw.githubusercontent.com/JKorf/Toobit.Net/refs/heads/main/Toobit.Net/Icon/icon.png)|Toobit|CEX|[JKorf/Toobit.Net](https://github.com/JKorf/Toobit.Net)|[![Nuget version](https://img.shields.io/nuget/v/Toobit.net.svg?style=flat-square)](https://www.nuget.org/packages/Toobit.Net)|[Link](https://www.toobit.com/en-US/register?invite_code=zsV19h)|-|
|![Upbit](https://raw.githubusercontent.com/JKorf/Upbit.Net/refs/heads/main/Upbit.Net/Icon/icon.png)|Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Upbit.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-| |![Upbit](https://raw.githubusercontent.com/JKorf/Upbit.Net/refs/heads/main/Upbit.Net/Icon/icon.png)|Upbit|CEX|[JKorf/Upbit.Net](https://github.com/JKorf/Upbit.Net)|[![Nuget version](https://img.shields.io/nuget/v/JKorf.Upbit.net.svg?style=flat-square)](https://www.nuget.org/packages/JKorf.Upbit.Net)|-|-|
|![Weex](https://raw.githubusercontent.com/JKorf/Weex.Net/refs/heads/main/Weex.Net/Icon/icon.png)|Weex|CEX|[JKorf/Weex.Net](https://github.com/JKorf/Weex.Net)|[![Nuget version](https://img.shields.io/nuget/v/Weex.net.svg?style=flat-square)](https://www.nuget.org/packages/Weex.Net)|-|-|
|![WhiteBit](https://raw.githubusercontent.com/JKorf/WhiteBit.Net/refs/heads/main/WhiteBit.Net/Icon/icon.png)|WhiteBit|CEX|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](https://www.nuget.org/packages/WhiteBit.Net)|[Link](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|-| |![WhiteBit](https://raw.githubusercontent.com/JKorf/WhiteBit.Net/refs/heads/main/WhiteBit.Net/Icon/icon.png)|WhiteBit|CEX|[JKorf/WhiteBit.Net](https://github.com/JKorf/WhiteBit.Net)|[![Nuget version](https://img.shields.io/nuget/v/WhiteBit.net.svg?style=flat-square)](https://www.nuget.org/packages/WhiteBit.Net)|[Link](https://whitebit.com/referral/a8e59b59-186c-4662-824c-3095248e0edf)|-|
|![XT](https://raw.githubusercontent.com/JKorf/XT.Net/refs/heads/main/XT.Net/Icon/icon.png)|XT|CEX|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[![Nuget version](https://img.shields.io/nuget/v/XT.net.svg?style=flat-square)](https://www.nuget.org/packages/XT.Net)|[Link](https://www.xt.com/ru/accounts/register?ref=CZG39C)|25%| |![XT](https://raw.githubusercontent.com/JKorf/XT.Net/refs/heads/main/XT.Net/Icon/icon.png)|XT|CEX|[JKorf/XT.Net](https://github.com/JKorf/XT.Net)|[![Nuget version](https://img.shields.io/nuget/v/XT.net.svg?style=flat-square)](https://www.nuget.org/packages/XT.Net)|[Link](https://www.xt.com/ru/accounts/register?ref=CZG39C)|25%|
@ -68,6 +69,20 @@ Make a one time donation in a crypto currency of your choice. If you prefer to d
Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf). Alternatively, sponsor me on Github using [Github Sponsors](https://github.com/sponsors/JKorf).
## Release notes ## Release notes
* Version 11.1.1 - 10 Apr 2026
* Added Reset functionality to rate limiter implementation
* Added reset of rate limit per connection when connection is disconnected
* Version 11.1.0 - 09 Apr 2026
* Updated WebSocket message routing improving performance for scenarios with multiple different subscriptions and topics
* Added AddCommaSeparated helper for Enum value arrays to ParameterCollection
* Added SharedRestRequestValidator for testing Shared interface implementations
* Improved EnumConverter performance and removed string allocation for happy path
* Fixed concurrency issue when using rate limit guard for multiple gates
* Fixed CreateParamString extension method for ArrayParametersSerialization.Json
* Fixed Shared GetOrderBookOptions and GetRecentTradeOptions base validations not being called
* Fixed CallResult returning success result in AsDataless even if Error is set
* Version 11.0.3 - 30 Mar 2026 * Version 11.0.3 - 30 Mar 2026
* Updated Enum converter to only warn once per type for null/empty value for non-nullable enum property * Updated Enum converter to only warn once per type for null/empty value for non-nullable enum property