mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d89c2bde94 | |||
| 3e6bdaafc6 | |||
| 93e4722a81 | |||
| 355ecb03da | |||
| 994c527c1d | |||
| 69b2e2045e | |||
| 7fde8bf5da | |||
| 637070a7ae | |||
| 7be75f72a7 | |||
| e3fece41f3 | |||
| 87b0c8d7a2 | |||
| ca9a711f22 | |||
| 27597bc994 | |||
| 776d75170d | |||
| 949780a9ad | |||
| 2f64cd9f05 | |||
| 185dfeb6fb | |||
| 68067d6258 | |||
| b309deb0c4 | |||
| e1dafdf0dd | |||
| fd7b5f0f0f | |||
| 3b735d66fd | |||
| 3cd505ac8b | |||
| 81d856d78d | |||
| 11c1ad871a | |||
| ffcb7db8ff | |||
| 02432e5109 | |||
| a85bfb4432 | |||
| 17d85fdd85 | |||
| 5e0733d7f4 | |||
| 28d5287bd4 | |||
| ef5097589a | |||
| f287ec1fa4 | |||
| 28da93af9d | |||
| 0d5bdf5095 | |||
| 8dac3d7aa6 | |||
| 6951f31be7 | |||
| 630f85ec49 | |||
| 9ec4f2276f | |||
| 0a0c66541e | |||
| bb4199620e | |||
| 8a83cd2cb8 | |||
| fcfeaf568f | |||
| 25567ea434 | |||
| 1ab85d4c26 | |||
| be68115099 | |||
| ff0550b0fb | |||
| 1ab1e008fc | |||
| 6f30c72608 | |||
| e927bc3d20 | |||
| 09ed7d1436 | |||
| 6fed657ea6 | |||
| 1555f8da0c | |||
| 68b28fc875 | |||
| 5d50d8cde8 | |||
| 9ff673d8be | |||
| 3e5a34fb56 | |||
| 64ee50d98c | |||
| 6a105c6f8f | |||
| 287aadc720 | |||
| 7229438a0b | |||
| 444af98a15 | |||
| 70c6fa1bbb | |||
| d27f394b46 | |||
| c8c98e13d0 | |||
| 9fcd722991 | |||
| 8080ecccc0 | |||
| 4b6fa9a1b1 | |||
| 0b6dbde7d4 | |||
| fe4d63ba75 | |||
| 04bd3727ca | |||
| 7e6fcd03c2 | |||
| fde8d6353b | |||
| 41b996168a | |||
| b26f8fb900 | |||
| bdbbc61d86 | |||
| d64e200f2f | |||
| 71d54e2f9a | |||
| ba3975993f | |||
| 050286ecd1 | |||
| 96c9a55c48 | |||
| a20cbb2f1c | |||
| 2e957d7d9e | |||
| 18d0341056 | |||
| a2bfed2433 | |||
| 67299338a8 | |||
| 971c049c5f |
@@ -121,6 +121,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||||
@@ -150,6 +151,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||||
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
||||||
Assert.That(options.ApiCredentials.Key.GetString() == "123");
|
Assert.That(options.ApiCredentials.Key == "123");
|
||||||
Assert.That(options.ApiCredentials.Secret.GetString() == "456");
|
Assert.That(options.ApiCredentials.Secret == "456");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@@ -64,10 +64,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
Assert.That(options.Api1Options.ApiCredentials.Key.GetString() == "123");
|
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
|
||||||
Assert.That(options.Api1Options.ApiCredentials.Secret.GetString() == "456");
|
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
|
||||||
Assert.That(options.Api2Options.ApiCredentials.Key.GetString() == "789");
|
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
|
||||||
Assert.That(options.Api2Options.ApiCredentials.Secret.GetString() == "101");
|
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|||||||
@@ -176,12 +176,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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, 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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(!triggered);
|
Assert.That(!triggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, 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);
|
||||||
}
|
}
|
||||||
@@ -222,9 +222,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,12 +243,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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, 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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(!triggered);
|
Assert.That(!triggered);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, 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);
|
||||||
}
|
}
|
||||||
@@ -286,7 +286,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, 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);
|
||||||
}
|
}
|
||||||
@@ -309,9 +309,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
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?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2?.ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +328,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
@@ -348,9 +348,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,9 +365,9 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
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".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(evnt == null);
|
Assert.That(evnt == null);
|
||||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123".ToSecureString(), 1, RateLimitingBehaviour.Wait, default);
|
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, default);
|
||||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
options.ReconnectInterval = TimeSpan.Zero;
|
options.ReconnectInterval = TimeSpan.Zero;
|
||||||
});
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.ShouldReconnect = true;
|
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
socket.DisconnectTime = DateTime.UtcNow;
|
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
Dictionary<string, string> result = null;
|
Dictionary<string, string> result = null;
|
||||||
@@ -72,11 +70,10 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
result = messageEvent.Data;
|
result = messageEvent.Data;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
});
|
||||||
subObj.HandleUpdatesBeforeConfirmation = true;
|
|
||||||
sub.AddSubscription(subObj);
|
sub.AddSubscription(subObj);
|
||||||
|
|
||||||
// act
|
// act
|
||||||
socket.InvokeMessage("{\"property\": \"123\", \"topic\": \"topic\"}");
|
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
||||||
rstEvent.WaitOne(1000);
|
rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@@ -94,9 +91,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
options.SubOptions.OutputOriginalData = enabled;
|
options.SubOptions.OutputOriginalData = enabled;
|
||||||
});
|
});
|
||||||
var socket = client.CreateSocket();
|
var socket = client.CreateSocket();
|
||||||
socket.ShouldReconnect = true;
|
|
||||||
socket.CanConnect = true;
|
socket.CanConnect = true;
|
||||||
socket.DisconnectTime = DateTime.UtcNow;
|
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||||
var rstEvent = new ManualResetEvent(false);
|
var rstEvent = new ManualResetEvent(false);
|
||||||
string original = null;
|
string original = null;
|
||||||
@@ -107,9 +102,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
original = messageEvent.OriginalData;
|
original = messageEvent.OriginalData;
|
||||||
rstEvent.Set();
|
rstEvent.Set();
|
||||||
});
|
});
|
||||||
subObj.HandleUpdatesBeforeConfirmation = true;
|
|
||||||
sub.AddSubscription(subObj);
|
sub.AddSubscription(subObj);
|
||||||
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", property = 123 });
|
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", action = "update", property = 123 });
|
||||||
|
|
||||||
// act
|
// act
|
||||||
socket.InvokeMessage(msgToSend);
|
socket.InvokeMessage(msgToSend);
|
||||||
@@ -204,7 +198,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
// act
|
// act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "error" }));
|
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "error" }));
|
||||||
await sub;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
@@ -227,7 +221,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
// act
|
// act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, status = "confirmed" }));
|
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "confirmed" }));
|
||||||
await sub;
|
await sub;
|
||||||
|
|
||||||
// assert
|
// assert
|
||||||
|
|||||||
@@ -163,6 +163,20 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(output.Value == expected);
|
Assert.That(output.Value == expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase("1", TestEnum.One)]
|
||||||
|
[TestCase("2", TestEnum.Two)]
|
||||||
|
[TestCase("3", TestEnum.Three)]
|
||||||
|
[TestCase("three", TestEnum.Three)]
|
||||||
|
[TestCase("Four", TestEnum.Four)]
|
||||||
|
[TestCase("four", TestEnum.Four)]
|
||||||
|
[TestCase("Four1", TestEnum.One)]
|
||||||
|
[TestCase(null, TestEnum.One)]
|
||||||
|
public void TestEnumConverterParseStringTests(string value, TestEnum? expected)
|
||||||
|
{
|
||||||
|
var result = EnumConverter.ParseString<TestEnum>(value);
|
||||||
|
Assert.That(result == expected);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase("1", true)]
|
[TestCase("1", true)]
|
||||||
[TestCase("true", true)]
|
[TestCase("true", true)]
|
||||||
[TestCase("yes", true)]
|
[TestCase("yes", true)]
|
||||||
@@ -200,6 +214,41 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
|
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
|
||||||
Assert.That(output.Value == expected);
|
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("1E+2", 100)]
|
||||||
|
[TestCase("1E-2", 0.01)]
|
||||||
|
[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.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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class STJTimeObject
|
public class STJTimeObject
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
{
|
{
|
||||||
internal class SubResponse
|
internal class SubResponse
|
||||||
{
|
{
|
||||||
|
|
||||||
|
[JsonProperty("action")]
|
||||||
|
public string Action { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("channel")]
|
[JsonProperty("channel")]
|
||||||
public string Channel { get; set; } = null!;
|
public string Channel { get; set; } = null!;
|
||||||
|
|
||||||
@@ -19,6 +23,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
|
|
||||||
internal class UnsubResponse
|
internal class UnsubResponse
|
||||||
{
|
{
|
||||||
|
[JsonProperty("action")]
|
||||||
|
public string Action { get; set; } = null!;
|
||||||
|
|
||||||
[JsonProperty("status")]
|
[JsonProperty("status")]
|
||||||
public string Status { get; set; } = null!;
|
public string Status { get; set; } = null!;
|
||||||
}
|
}
|
||||||
@@ -29,7 +36,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
|
|
||||||
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||||
{
|
{
|
||||||
ListenerIdentifiers = new HashSet<string> { channel };
|
ListenerIdentifiers = new HashSet<string> { request + "-" + channel };
|
||||||
}
|
}
|
||||||
|
|
||||||
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|||||||
{
|
{
|
||||||
private readonly Action<DataEvent<T>> _handler;
|
private readonly Action<DataEvent<T>> _handler;
|
||||||
|
|
||||||
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "topic" };
|
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" };
|
||||||
|
|
||||||
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
return deserializeResult;
|
return deserializeResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
public override TimeSpan? GetTimeOffset() => null;
|
public override TimeSpan? GetTimeOffset() => null;
|
||||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||||
@@ -66,14 +68,11 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, Dictionary<string, object> providedParameters, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat, out SortedDictionary<string, object> uriParameters, out SortedDictionary<string, object> bodyParameters, out Dictionary<string, string> headers)
|
public override void AuthenticateRequest(RestApiClient apiClient, Uri uri, HttpMethod method, ref IDictionary<string, object> uriParams, ref IDictionary<string, object> bodyParams, ref Dictionary<string, string> headers, bool auth, ArrayParametersSerialization arraySerialization, HttpMethodParameterPosition parameterPosition, RequestBodyFormat bodyFormat)
|
||||||
{
|
{
|
||||||
bodyParameters = new SortedDictionary<string, object>();
|
|
||||||
uriParameters = new SortedDictionary<string, object>();
|
|
||||||
headers = new Dictionary<string, string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetKey() => _credentials.Key.GetString();
|
public string GetKey() => _credentials.Key;
|
||||||
public string GetSecret() => _credentials.Secret.GetString();
|
public string GetSecret() => _credentials.Secret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,6 +137,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
||||||
@@ -178,6 +181,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
{
|
{
|
||||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
||||||
|
|||||||
@@ -1,131 +1,132 @@
|
|||||||
using System;
|
//using System;
|
||||||
using System.IO;
|
//using System.IO;
|
||||||
using System.Net.WebSockets;
|
//using System.Net.WebSockets;
|
||||||
using System.Security.Authentication;
|
//using System.Security.Authentication;
|
||||||
using System.Text;
|
//using System.Text;
|
||||||
using System.Threading.Tasks;
|
//using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Interfaces;
|
//using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
//using CryptoExchange.Net.Objects;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
//namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
//{
|
||||||
public class TestSocket: IWebsocket
|
// public class TestSocket: IWebsocket
|
||||||
{
|
// {
|
||||||
public bool CanConnect { get; set; }
|
// public bool CanConnect { get; set; }
|
||||||
public bool Connected { get; set; }
|
// public bool Connected { get; set; }
|
||||||
|
|
||||||
public event Func<Task> OnClose;
|
// public event Func<Task> OnClose;
|
||||||
#pragma warning disable 0067
|
//#pragma warning disable 0067
|
||||||
public event Func<Task> OnReconnected;
|
// public event Func<Task> OnReconnected;
|
||||||
public event Func<Task> OnReconnecting;
|
// public event Func<Task> OnReconnecting;
|
||||||
public event Func<int, Task> OnRequestRateLimited;
|
// public event Func<int, Task> OnRequestRateLimited;
|
||||||
#pragma warning restore 0067
|
//#pragma warning restore 0067
|
||||||
public event Func<int, Task> OnRequestSent;
|
// public event Func<int, Task> OnRequestSent;
|
||||||
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
|
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
||||||
public event Func<Exception, Task> OnError;
|
// public event Func<Exception, Task> OnError;
|
||||||
public event Func<Task> OnOpen;
|
// public event Func<Task> OnOpen;
|
||||||
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
// public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
public int Id { get; }
|
// public int Id { get; }
|
||||||
public bool ShouldReconnect { get; set; }
|
// public bool ShouldReconnect { get; set; }
|
||||||
public TimeSpan Timeout { get; set; }
|
// public TimeSpan Timeout { get; set; }
|
||||||
public Func<string, string> DataInterpreterString { get; set; }
|
// public Func<string, string> DataInterpreterString { get; set; }
|
||||||
public Func<byte[], string> DataInterpreterBytes { get; set; }
|
// public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||||
public DateTime? DisconnectTime { get; set; }
|
// public DateTime? DisconnectTime { get; set; }
|
||||||
public string Url { get; }
|
// public string Url { get; }
|
||||||
public bool IsClosed => !Connected;
|
// public bool IsClosed => !Connected;
|
||||||
public bool IsOpen => Connected;
|
// public bool IsOpen => Connected;
|
||||||
public bool PingConnection { get; set; }
|
// public bool PingConnection { get; set; }
|
||||||
public TimeSpan PingInterval { get; set; }
|
// public TimeSpan PingInterval { get; set; }
|
||||||
public SslProtocols SSLProtocols { get; set; }
|
// public SslProtocols SSLProtocols { get; set; }
|
||||||
public Encoding Encoding { get; set; }
|
// public Encoding Encoding { get; set; }
|
||||||
|
|
||||||
public int ConnectCalls { get; private set; }
|
// public int ConnectCalls { get; private set; }
|
||||||
public bool Reconnecting { get; set; }
|
// public bool Reconnecting { get; set; }
|
||||||
public string Origin { get; set; }
|
// public string Origin { get; set; }
|
||||||
public int? RatelimitPerSecond { get; set; }
|
// public int? RatelimitPerSecond { get; set; }
|
||||||
|
|
||||||
public double IncomingKbps => throw new NotImplementedException();
|
// public double IncomingKbps => throw new NotImplementedException();
|
||||||
|
|
||||||
public Uri Uri => new Uri("");
|
// public Uri Uri => new Uri("");
|
||||||
|
|
||||||
public TimeSpan KeepAliveInterval { get; set; }
|
// public TimeSpan KeepAliveInterval { get; set; }
|
||||||
|
|
||||||
public static int lastId = 0;
|
// public static int lastId = 0;
|
||||||
public static object lastIdLock = new object();
|
// public static object lastIdLock = new object();
|
||||||
|
|
||||||
public TestSocket()
|
// public TestSocket()
|
||||||
{
|
// {
|
||||||
lock (lastIdLock)
|
// lock (lastIdLock)
|
||||||
{
|
// {
|
||||||
Id = lastId + 1;
|
// Id = lastId + 1;
|
||||||
lastId++;
|
// lastId++;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
public Task<CallResult> ConnectAsync()
|
// public Task<CallResult> ConnectAsync()
|
||||||
{
|
// {
|
||||||
Connected = CanConnect;
|
// Connected = CanConnect;
|
||||||
ConnectCalls++;
|
// ConnectCalls++;
|
||||||
if (CanConnect)
|
// if (CanConnect)
|
||||||
InvokeOpen();
|
// InvokeOpen();
|
||||||
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||||
}
|
// }
|
||||||
|
|
||||||
public void Send(int requestId, string data, int weight)
|
// public bool Send(int requestId, string data, int weight)
|
||||||
{
|
// {
|
||||||
if(!Connected)
|
// if(!Connected)
|
||||||
throw new Exception("Socket not connected");
|
// throw new Exception("Socket not connected");
|
||||||
OnRequestSent?.Invoke(requestId);
|
// OnRequestSent?.Invoke(requestId);
|
||||||
}
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
public void Reset()
|
// public void Reset()
|
||||||
{
|
// {
|
||||||
}
|
// }
|
||||||
|
|
||||||
public Task CloseAsync()
|
// public Task CloseAsync()
|
||||||
{
|
// {
|
||||||
Connected = false;
|
// Connected = false;
|
||||||
DisconnectTime = DateTime.UtcNow;
|
// DisconnectTime = DateTime.UtcNow;
|
||||||
OnClose?.Invoke();
|
// OnClose?.Invoke();
|
||||||
return Task.FromResult(0);
|
// return Task.FromResult(0);
|
||||||
}
|
// }
|
||||||
|
|
||||||
public void SetProxy(string host, int port)
|
// public void SetProxy(string host, int port)
|
||||||
{
|
// {
|
||||||
throw new NotImplementedException();
|
// throw new NotImplementedException();
|
||||||
}
|
// }
|
||||||
public void Dispose()
|
// public void Dispose()
|
||||||
{
|
// {
|
||||||
}
|
// }
|
||||||
|
|
||||||
public void InvokeClose()
|
// public void InvokeClose()
|
||||||
{
|
// {
|
||||||
Connected = false;
|
// Connected = false;
|
||||||
DisconnectTime = DateTime.UtcNow;
|
// DisconnectTime = DateTime.UtcNow;
|
||||||
Reconnecting = true;
|
// Reconnecting = true;
|
||||||
OnClose?.Invoke();
|
// OnClose?.Invoke();
|
||||||
}
|
// }
|
||||||
|
|
||||||
public void InvokeOpen()
|
// public void InvokeOpen()
|
||||||
{
|
// {
|
||||||
OnOpen?.Invoke();
|
// OnOpen?.Invoke();
|
||||||
}
|
// }
|
||||||
|
|
||||||
public void InvokeMessage(string data)
|
// public void InvokeMessage(string data)
|
||||||
{
|
// {
|
||||||
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data)));
|
// OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
||||||
}
|
// }
|
||||||
|
|
||||||
public void SetProxy(ApiProxy proxy)
|
// public void SetProxy(ApiProxy proxy)
|
||||||
{
|
// {
|
||||||
throw new NotImplementedException();
|
// throw new NotImplementedException();
|
||||||
}
|
// }
|
||||||
|
|
||||||
public void InvokeError(Exception error)
|
// public void InvokeError(Exception error)
|
||||||
{
|
// {
|
||||||
OnError?.Invoke(error);
|
// OnError?.Invoke(error);
|
||||||
}
|
// }
|
||||||
public Task ReconnectAsync() => Task.CompletedTask;
|
// public Task ReconnectAsync() => Task.CompletedTask;
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ using CryptoExchange.Net.Sockets;
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Moq;
|
using Moq;
|
||||||
using Newtonsoft.Json.Linq;
|
using CryptoExchange.Net.Testing.Implementations;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
public class TestSocketClient: BaseSocketClient
|
internal class TestSocketClient: BaseSocketClient
|
||||||
{
|
{
|
||||||
public TestSubSocketClient SubClient { get; }
|
public TestSubSocketClient SubClient { get; }
|
||||||
|
|
||||||
@@ -41,12 +41,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||||
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
SubClient.SocketFactory = new Mock<IWebsocketFactory>().Object;
|
||||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public TestSocket CreateSocket()
|
public TestSocket CreateSocket()
|
||||||
{
|
{
|
||||||
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket());
|
Mock.Get(SubClient.SocketFactory).Setup(f => f.CreateWebsocket(It.IsAny<ILogger>(), It.IsAny<WebSocketParameters>())).Returns(new TestSocket("https://test.com"));
|
||||||
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
return (TestSocket)SubClient.CreateSocketInternal("https://localhost:123/");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
public class TestSubSocketClient : SocketApiClient
|
public class TestSubSocketClient : SocketApiClient
|
||||||
{
|
{
|
||||||
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
||||||
|
private MessagePath _actionPath = MessagePath.Get().Property("action");
|
||||||
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
||||||
|
|
||||||
public Subscription TestSubscription { get; private set; } = null;
|
public Subscription TestSubscription { get; private set; } = null;
|
||||||
@@ -84,6 +85,9 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string FormatSymbol(string baseAsset, string quoteAsset) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
internal IWebsocket CreateSocketInternal(string address)
|
internal IWebsocket CreateSocketInternal(string address)
|
||||||
{
|
{
|
||||||
return CreateSocket(address);
|
return CreateSocket(address);
|
||||||
@@ -107,7 +111,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
var id = message.GetValue<string>(_channelPath);
|
var id = message.GetValue<string>(_channelPath);
|
||||||
id ??= message.GetValue<string>(_topicPath);
|
id ??= message.GetValue<string>(_topicPath);
|
||||||
|
|
||||||
return id;
|
return message.GetValue<string>(_actionPath) + "-" + id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
|
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
using CryptoExchange.Net.Converters.MessageParsing;
|
||||||
|
|
||||||
@@ -9,48 +8,23 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api credentials, used to sign requests accessing private endpoints
|
/// Api credentials, used to sign requests accessing private endpoints
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ApiCredentials: IDisposable
|
public class ApiCredentials
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api key to authenticate requests
|
/// The api key to authenticate requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SecureString? Key { get; }
|
public string Key { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api secret to authenticate requests
|
/// The api secret to authenticate requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SecureString? Secret { get; }
|
public string Secret { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Type of the credentials
|
/// Type of the credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiCredentialsType CredentialType { get; }
|
public ApiCredentialsType CredentialType { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create Api credentials providing an api key and secret for authentication
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The api key used for identification</param>
|
|
||||||
/// <param name="secret">The api secret used for signing</param>
|
|
||||||
public ApiCredentials(SecureString key, SecureString secret) : this(key, secret, ApiCredentialsType.Hmac)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create Api credentials providing an api key and secret for authentication
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="key">The api key used for identification</param>
|
|
||||||
/// <param name="secret">The api secret used for signing</param>
|
|
||||||
/// <param name="credentialsType">The type of credentials</param>
|
|
||||||
public ApiCredentials(SecureString key, SecureString secret, ApiCredentialsType credentialsType)
|
|
||||||
{
|
|
||||||
if (key == null || secret == null)
|
|
||||||
throw new ArgumentException("Key and secret can't be null/empty");
|
|
||||||
|
|
||||||
CredentialType = credentialsType;
|
|
||||||
Key = key;
|
|
||||||
Secret = secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create Api credentials providing an api key and secret for authentication
|
/// Create Api credentials providing an api key and secret for authentication
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -72,8 +46,8 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
throw new ArgumentException("Key and secret can't be null/empty");
|
throw new ArgumentException("Key and secret can't be null/empty");
|
||||||
|
|
||||||
CredentialType = credentialsType;
|
CredentialType = credentialsType;
|
||||||
Key = key.ToSecureString();
|
Key = key;
|
||||||
Secret = secret.ToSecureString();
|
Secret = secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -82,8 +56,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual ApiCredentials Copy()
|
public virtual ApiCredentials Copy()
|
||||||
{
|
{
|
||||||
// Use .GetString() to create a copy of the SecureString
|
return new ApiCredentials(Key, Secret, CredentialType);
|
||||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -103,19 +76,10 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
if (key == null || secret == null)
|
if (key == null || secret == null)
|
||||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||||
|
|
||||||
Key = key.ToSecureString();
|
Key = key;
|
||||||
Secret = secret.ToSecureString();
|
Secret = secret;
|
||||||
|
|
||||||
inputStream.Seek(0, SeekOrigin.Begin);
|
inputStream.Seek(0, SeekOrigin.Begin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Key?.Dispose();
|
|
||||||
Secret?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -13,18 +14,25 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for authentication providers
|
/// Base class for authentication providers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AuthenticationProvider : IDisposable
|
public abstract class AuthenticationProvider
|
||||||
{
|
{
|
||||||
|
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided credentials
|
/// Provided credentials
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly ApiCredentials _credentials;
|
protected internal readonly ApiCredentials _credentials;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Byte representation of the secret
|
/// Byte representation of the secret
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected byte[] _sBytes;
|
protected byte[] _sBytes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the API key of the current credentials
|
||||||
|
/// </summary>
|
||||||
|
public string ApiKey => _credentials.Key;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -35,7 +43,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
throw new ArgumentException("ApiKey/Secret needed");
|
throw new ArgumentException("ApiKey/Secret needed");
|
||||||
|
|
||||||
_credentials = credentials;
|
_credentials = credentials;
|
||||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -44,26 +52,24 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// <param name="apiClient">The Api client sending the request</param>
|
/// <param name="apiClient">The Api client sending the request</param>
|
||||||
/// <param name="uri">The uri for the request</param>
|
/// <param name="uri">The uri for the request</param>
|
||||||
/// <param name="method">The method of the request</param>
|
/// <param name="method">The method of the request</param>
|
||||||
/// <param name="providedParameters">The request parameters</param>
|
|
||||||
/// <param name="auth">If the requests should be authenticated</param>
|
/// <param name="auth">If the requests should be authenticated</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
|
||||||
/// <param name="requestBodyFormat">The formatting of the request body</param>
|
/// <param name="requestBodyFormat">The formatting of the request body</param>
|
||||||
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
|
||||||
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
|
||||||
/// <param name="headers">The headers that should be send with the request</param>
|
/// <param name="headers">The headers that should be send with the request</param>
|
||||||
|
/// <param name="parameterPosition">The position where the providedParameters should go</param>
|
||||||
public abstract void AuthenticateRequest(
|
public abstract void AuthenticateRequest(
|
||||||
RestApiClient apiClient,
|
RestApiClient apiClient,
|
||||||
Uri uri,
|
Uri uri,
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
Dictionary<string, object> providedParameters,
|
ref IDictionary<string, object>? uriParameters,
|
||||||
|
ref IDictionary<string, object>? bodyParameters,
|
||||||
|
ref Dictionary<string, string>? headers,
|
||||||
bool auth,
|
bool auth,
|
||||||
ArrayParametersSerialization arraySerialization,
|
ArrayParametersSerialization arraySerialization,
|
||||||
HttpMethodParameterPosition parameterPosition,
|
HttpMethodParameterPosition parameterPosition,
|
||||||
RequestBodyFormat requestBodyFormat,
|
RequestBodyFormat requestBodyFormat
|
||||||
out SortedDictionary<string, object> uriParameters,
|
|
||||||
out SortedDictionary<string, object> bodyParameters,
|
|
||||||
out Dictionary<string, string> headers
|
|
||||||
);
|
);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -365,7 +371,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
{
|
{
|
||||||
#if NETSTANDARD2_1_OR_GREATER
|
#if NETSTANDARD2_1_OR_GREATER
|
||||||
// Read from pem private key
|
// Read from pem private key
|
||||||
var key = _credentials.Secret!.GetString()
|
var key = _credentials.Secret!
|
||||||
.Replace("\n", "")
|
.Replace("\n", "")
|
||||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||||
.Replace("-----END PRIVATE KEY-----", "")
|
.Replace("-----END PRIVATE KEY-----", "")
|
||||||
@@ -380,7 +386,7 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
||||||
{
|
{
|
||||||
// Read from xml private key format
|
// Read from xml private key format
|
||||||
rsa.FromXmlString(_credentials.Secret!.GetString());
|
rsa.FromXmlString(_credentials.Secret!);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -418,9 +424,9 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
/// <param name="apiClient"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static DateTime GetTimestamp(RestApiClient apiClient)
|
protected DateTime GetTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return DateTime.UtcNow.Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -428,15 +434,23 @@ namespace CryptoExchange.Net.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
/// <param name="apiClient"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||||
{
|
{
|
||||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public void Dispose()
|
/// Return the serialized request body
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serializer"></param>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||||
{
|
{
|
||||||
_credentials?.Dispose();
|
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
|
||||||
|
return serializer.Serialize(parameters[Constants.BodyPlaceHolderKey]);
|
||||||
|
else
|
||||||
|
return serializer.Serialize(parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Caching
|
||||||
|
{
|
||||||
|
internal class MemoryCache
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="value">Cache value</param>
|
||||||
|
public void Add(string key, object value)
|
||||||
|
{
|
||||||
|
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
||||||
|
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a cached value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key identifier</param>
|
||||||
|
/// <param name="maxAge">The max age of the cached entry</param>
|
||||||
|
/// <returns>Cached value if it was in cache</returns>
|
||||||
|
public object? Get(string key, TimeSpan maxAge)
|
||||||
|
{
|
||||||
|
_cache.TryGetValue(key, out CacheItem value);
|
||||||
|
if (value == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - value.CacheTime > maxAge)
|
||||||
|
{
|
||||||
|
_cache.TryRemove(key, out _);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CacheItem
|
||||||
|
{
|
||||||
|
public DateTime CacheTime { get; }
|
||||||
|
public object Value { get; }
|
||||||
|
|
||||||
|
public CacheItem(DateTime cacheTime, object value)
|
||||||
|
{
|
||||||
|
CacheTime = cacheTime;
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,10 +65,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
BaseAddress = baseAddress;
|
BaseAddress = baseAddress;
|
||||||
|
|
||||||
if (apiCredentials != null)
|
if (apiCredentials != null)
|
||||||
{
|
|
||||||
AuthenticationProvider?.Dispose();
|
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -78,14 +75,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract string FormatSymbol(string baseAsset, string quoteAsset);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
{
|
{
|
||||||
if (credentials != null)
|
if (credentials != null)
|
||||||
{
|
|
||||||
AuthenticationProvider?.Dispose();
|
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -94,7 +91,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public virtual void Dispose()
|
public virtual void Dispose()
|
||||||
{
|
{
|
||||||
_disposing = true;
|
_disposing = true;
|
||||||
AuthenticationProvider?.Dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Net;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Caching;
|
||||||
using CryptoExchange.Net.Converters.JsonNet;
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
@@ -17,6 +18,7 @@ using CryptoExchange.Net.RateLimiting;
|
|||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
using CryptoExchange.Net.Requests;
|
using CryptoExchange.Net.Requests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients
|
namespace CryptoExchange.Net.Clients
|
||||||
{
|
{
|
||||||
@@ -40,23 +42,33 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request body content type
|
/// Request body content type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
protected internal RequestBodyFormat RequestBodyFormat = RequestBodyFormat.Json;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// How to serialize array parameters when making requests
|
/// How to serialize array parameters when making requests
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
protected internal ArrayParametersSerialization ArraySerialization = ArrayParametersSerialization.Array;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected string RequestBodyEmptyContent = "{}";
|
protected internal string RequestBodyEmptyContent = "{}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Request headers to be sent with each request
|
/// Request headers to be sent with each request
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether parameters need to be ordered
|
||||||
|
/// </summary>
|
||||||
|
protected internal bool OrderParameters { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parameter order comparer
|
||||||
|
/// </summary>
|
||||||
|
protected IComparer<string> ParameterOrderComparer { get; } = new OrderedStringComparer();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Where to put the parameters for requests with different Http methods
|
/// Where to put the parameters for requests with different Http methods
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -65,7 +77,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
|
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
|
||||||
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
|
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
|
||||||
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
|
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
|
||||||
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
|
{ HttpMethod.Put, HttpMethodParameterPosition.InBody },
|
||||||
|
{ new HttpMethod("Patch"), HttpMethodParameterPosition.InBody },
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -74,6 +87,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
public new RestApiOptions ApiOptions => (RestApiOptions)base.ApiOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory cache
|
||||||
|
/// </summary>
|
||||||
|
private static MemoryCache _cache = new MemoryCache();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -139,7 +156,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
protected virtual Task<WebCallResult<T>> SendAsync<T>(
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? parameters,
|
ParameterCollection? parameters,
|
||||||
@@ -147,16 +164,72 @@ namespace CryptoExchange.Net.Clients
|
|||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null) where T : class
|
int? weight = null) where T : class
|
||||||
{
|
{
|
||||||
|
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||||
|
return SendAsync<T>(
|
||||||
|
baseAddress,
|
||||||
|
definition,
|
||||||
|
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
|
||||||
|
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
|
||||||
|
cancellationToken,
|
||||||
|
additionalHeaders,
|
||||||
|
weight);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Send a request to the base address based on the request definition
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Response type</typeparam>
|
||||||
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
|
/// <param name="definition">Request definition</param>
|
||||||
|
/// <param name="uriParameters">Request query parameters</param>
|
||||||
|
/// <param name="bodyParameters">Request body parameters</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
|
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
|
||||||
|
string baseAddress,
|
||||||
|
RequestDefinition definition,
|
||||||
|
ParameterCollection? uriParameters,
|
||||||
|
ParameterCollection? bodyParameters,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
int? weight = null) where T : class
|
||||||
|
{
|
||||||
|
string? cacheKey = null;
|
||||||
|
if (ShouldCache(definition))
|
||||||
|
{
|
||||||
|
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
|
||||||
|
_logger.CheckingCache(cacheKey);
|
||||||
|
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
|
||||||
|
if (cachedValue != null)
|
||||||
|
{
|
||||||
|
_logger.CacheHit(cacheKey);
|
||||||
|
var original = (WebCallResult<T>)cachedValue;
|
||||||
|
return original.Cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CacheNotHit(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
currentTry++;
|
currentTry++;
|
||||||
var prepareResult = await PrepareAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
var requestId = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
|
var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
|
||||||
if (!prepareResult)
|
if (!prepareResult)
|
||||||
return new WebCallResult<T>(prepareResult.Error!);
|
return new WebCallResult<T>(prepareResult.Error!);
|
||||||
|
|
||||||
var request = CreateRequest(baseAddress, definition, parameters, additionalHeaders);
|
var request = CreateRequest(
|
||||||
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
requestId,
|
||||||
|
baseAddress,
|
||||||
|
definition,
|
||||||
|
uriParameters,
|
||||||
|
bodyParameters,
|
||||||
|
additionalHeaders);
|
||||||
|
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
|
||||||
TotalRequestsMade++;
|
TotalRequestsMade++;
|
||||||
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
|
||||||
if (!result)
|
if (!result)
|
||||||
@@ -167,6 +240,12 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(definition.RateLimitGate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (result.Success &&
|
||||||
|
ShouldCache(definition))
|
||||||
|
{
|
||||||
|
_cache.Add(cacheKey!, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,23 +253,22 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepare before sending a request. Sync time between client and server and check rate limits
|
/// Prepare before sending a request. Sync time between client and server and check rate limits
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="requestId">Request id</param>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="parameters">Request parameters</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
/// <param name="additionalHeaders">Additional headers for this request</param>
|
/// <param name="additionalHeaders">Additional headers for this request</param>
|
||||||
/// <param name="weight">Override the request weight for this request</param>
|
/// <param name="weight">Override the request weight for this request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
/// <exception cref="Exception"></exception>
|
/// <exception cref="Exception"></exception>
|
||||||
protected virtual async Task<CallResult> PrepareAsync(
|
protected virtual async Task<CallResult> PrepareAsync(
|
||||||
|
int requestId,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? parameters,
|
|
||||||
CancellationToken cancellationToken,
|
CancellationToken cancellationToken,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
int? weight = null)
|
int? weight = null)
|
||||||
{
|
{
|
||||||
var requestId = ExchangeHelpers.NextId();
|
|
||||||
var requestWeight = weight ?? definition.Weight;
|
var requestWeight = weight ?? definition.Weight;
|
||||||
|
|
||||||
// Time sync
|
// Time sync
|
||||||
@@ -225,21 +303,21 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return new CallResult(limitResult.Error!);
|
return new CallResult(limitResult.Error!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Endpoint specific rate limiting
|
// Endpoint specific rate limiting
|
||||||
if (definition.EndpointLimitCount != null && definition.EndpointLimitPeriod != null)
|
if (definition.LimitGuard != null && ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
if (definition.RateLimitGate == null)
|
if (definition.RateLimitGate == null)
|
||||||
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
|
throw new Exception("Ratelimit gate not set when endpoint limit is specified");
|
||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return new CallResult(limitResult.Error!);
|
return new CallResult(limitResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -251,40 +329,30 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a request object
|
/// Creates a request object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="requestId">Id of the request</param>
|
||||||
/// <param name="baseAddress">Host and schema</param>
|
/// <param name="baseAddress">Host and schema</param>
|
||||||
/// <param name="definition">Request definition</param>
|
/// <param name="definition">Request definition</param>
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
/// <param name="uriParameters">The query parameters of the request</param>
|
||||||
|
/// <param name="bodyParameters">The body parameters of the request</param>
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual IRequest CreateRequest(
|
protected virtual IRequest CreateRequest(
|
||||||
|
int requestId,
|
||||||
string baseAddress,
|
string baseAddress,
|
||||||
RequestDefinition definition,
|
RequestDefinition definition,
|
||||||
ParameterCollection? parameters,
|
ParameterCollection? uriParameters,
|
||||||
|
ParameterCollection? bodyParameters,
|
||||||
Dictionary<string, string>? additionalHeaders)
|
Dictionary<string, string>? additionalHeaders)
|
||||||
{
|
{
|
||||||
parameters ??= new ParameterCollection();
|
var uriParams = uriParameters == null ? null : CreateParameterDictionary(uriParameters);
|
||||||
|
var bodyParams = bodyParameters == null ? null : CreateParameterDictionary(bodyParameters);
|
||||||
|
|
||||||
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
var uri = new Uri(baseAddress.AppendPath(definition.Path));
|
||||||
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
|
||||||
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
|
||||||
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
|
||||||
var requestId = ExchangeHelpers.NextId();
|
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
|
||||||
|
|
||||||
for (var i = 0; i < parameters.Count; i++)
|
Dictionary<string, string>? headers = null;
|
||||||
{
|
|
||||||
var kvp = parameters.ElementAt(i);
|
|
||||||
if (kvp.Value is Func<object> delegateValue)
|
|
||||||
parameters[kvp.Key] = delegateValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
|
||||||
{
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
|
||||||
if (AuthenticationProvider != null)
|
if (AuthenticationProvider != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -293,14 +361,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
this,
|
this,
|
||||||
uri,
|
uri,
|
||||||
definition.Method,
|
definition.Method,
|
||||||
parameters,
|
ref uriParams,
|
||||||
|
ref bodyParams,
|
||||||
|
ref headers,
|
||||||
definition.Authenticated,
|
definition.Authenticated,
|
||||||
arraySerialization,
|
arraySerialization,
|
||||||
parameterPosition,
|
parameterPosition,
|
||||||
bodyFormat,
|
bodyFormat
|
||||||
out uriParameters,
|
);
|
||||||
out bodyParameters,
|
|
||||||
out headers);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -308,24 +376,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check
|
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||||
foreach (var param in parameters)
|
if (uriParams != null)
|
||||||
{
|
uri = uri.SetParameters(uriParams, arraySerialization);
|
||||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
|
||||||
{
|
|
||||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
|
||||||
$"should return provided parameters in either the uri or body parameters output");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
|
||||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
|
||||||
|
|
||||||
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
var request = RequestFactory.Create(definition.Method, uri, requestId);
|
||||||
request.Accept = Constants.JsonContentHeader;
|
request.Accept = Constants.JsonContentHeader;
|
||||||
|
|
||||||
foreach (var header in headers)
|
if (headers != null)
|
||||||
request.AddHeader(header.Key, header.Value);
|
{
|
||||||
|
foreach (var header in headers)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
if (additionalHeaders != null)
|
if (additionalHeaders != null)
|
||||||
{
|
{
|
||||||
@@ -346,8 +408,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
{
|
{
|
||||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
if (bodyParameters.Any())
|
if (bodyParams != null && bodyParams.Count != 0)
|
||||||
WriteParamBody(request, bodyParameters, contentType);
|
WriteParamBody(request, bodyParams, contentType);
|
||||||
else
|
else
|
||||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||||
}
|
}
|
||||||
@@ -420,6 +482,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="requestWeight">Credits used for the request</param>
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
/// <param name="gate">The ratelimit gate to use</param>
|
/// <param name="gate">The ratelimit gate to use</param>
|
||||||
|
/// <param name="preventCaching">Whether caching should be prevented for this request</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[return: NotNull]
|
[return: NotNull]
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||||
@@ -433,9 +496,25 @@ namespace CryptoExchange.Net.Clients
|
|||||||
ArrayParametersSerialization? arraySerialization = null,
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
int requestWeight = 1,
|
int requestWeight = 1,
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
IRateLimitGate? gate = null
|
IRateLimitGate? gate = null,
|
||||||
|
bool preventCaching = false
|
||||||
) where T : class
|
) where T : class
|
||||||
{
|
{
|
||||||
|
var key = uri.ToString() + method + signed + parameters?.ToFormData();
|
||||||
|
if (ShouldCache(method) && !preventCaching)
|
||||||
|
{
|
||||||
|
_logger.CheckingCache(key);
|
||||||
|
var cachedValue = _cache.Get(key, ClientOptions.CachingMaxAge);
|
||||||
|
if (cachedValue != null)
|
||||||
|
{
|
||||||
|
_logger.CacheHit(key);
|
||||||
|
var original = (WebCallResult<T>)cachedValue;
|
||||||
|
return original.Cached();
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.CacheNotHit(key);
|
||||||
|
}
|
||||||
|
|
||||||
int currentTry = 0;
|
int currentTry = 0;
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@@ -453,6 +532,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
if (await ShouldRetryRequestAsync(gate, result, currentTry).ConfigureAwait(false))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
if (result.Success &&
|
||||||
|
ShouldCache(method) &&
|
||||||
|
!preventCaching)
|
||||||
|
{
|
||||||
|
_cache.Add(key, result);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,7 +603,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
if (ClientOptions.RateLimiterEnabled)
|
if (ClientOptions.RateLimiterEnabled)
|
||||||
{
|
{
|
||||||
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, ApiOptions.ApiCredentials?.Key ?? ClientOptions.ApiCredentials?.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
var limitResult = await gate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, new RequestDefinition(uri.AbsolutePath.TrimStart('/'), method) { Authenticated = signed }, uri.Host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, cancellationToken).ConfigureAwait(false);
|
||||||
if (!limitResult)
|
if (!limitResult)
|
||||||
return new CallResult<IRequest>(limitResult.Error!);
|
return new CallResult<IRequest>(limitResult.Error!);
|
||||||
}
|
}
|
||||||
@@ -592,47 +678,47 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (error.Code == null || error.Code == 0)
|
if (error.Code == null || error.Code == 0)
|
||||||
error.Code = (int)response.StatusCode;
|
error.Code = (int)response.StatusCode;
|
||||||
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof(T) == typeof(object))
|
if (typeof(T) == typeof(object))
|
||||||
// Success status code and expected empty response, assume it's correct
|
// Success status code and expected empty response, assume it's correct
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
|
||||||
|
|
||||||
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
|
||||||
if (!valid)
|
if (!valid)
|
||||||
{
|
{
|
||||||
// Invalid json
|
// Invalid json
|
||||||
var error = new ServerError("Failed to parse response", accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
var error = new ServerError("Failed to parse response: " + valid.Error!.Message, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]");
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Json response received
|
// Json response received
|
||||||
var parsedError = TryParseError(accessor);
|
var parsedError = TryParseError(accessor);
|
||||||
if (parsedError != null)
|
if (parsedError != null)
|
||||||
// Success status code, but TryParseError determined it was an error response
|
// Success status code, but TryParseError determined it was an error response
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parsedError);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
var deserializeResult = accessor.Deserialize<T>();
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
|
||||||
}
|
}
|
||||||
catch (HttpRequestException requestException)
|
catch (HttpRequestException requestException)
|
||||||
{
|
{
|
||||||
// Request exception, can't reach server for instance
|
// Request exception, can't reach server for instance
|
||||||
var exceptionInfo = requestException.ToLogString();
|
var exceptionInfo = requestException.ToLogString();
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError(exceptionInfo));
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException canceledException)
|
catch (OperationCanceledException canceledException)
|
||||||
{
|
{
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
{
|
{
|
||||||
// Cancellation token canceled by caller
|
// Cancellation token canceled by caller
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Request timed out
|
// Request timed out
|
||||||
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"Request timed out"));
|
return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new WebError($"Request timed out"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -726,8 +812,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
var headers = new Dictionary<string, string>();
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : null;
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : null;
|
||||||
if (AuthenticationProvider != null)
|
if (AuthenticationProvider != null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -736,14 +822,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
this,
|
this,
|
||||||
uri,
|
uri,
|
||||||
method,
|
method,
|
||||||
parameters,
|
ref uriParameters,
|
||||||
|
ref bodyParameters,
|
||||||
|
ref headers,
|
||||||
signed,
|
signed,
|
||||||
arraySerialization,
|
arraySerialization,
|
||||||
parameterPosition,
|
parameterPosition,
|
||||||
bodyFormat,
|
bodyFormat
|
||||||
out uriParameters,
|
);
|
||||||
out bodyParameters,
|
|
||||||
out headers);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -751,24 +837,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanity check
|
|
||||||
foreach (var param in parameters)
|
|
||||||
{
|
|
||||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
|
||||||
{
|
|
||||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
|
||||||
$"should return provided parameters in either the uri or body parameters output");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
if (uriParameters != null)
|
||||||
|
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||||
|
|
||||||
var request = RequestFactory.Create(method, uri, requestId);
|
var request = RequestFactory.Create(method, uri, requestId);
|
||||||
request.Accept = Constants.JsonContentHeader;
|
request.Accept = Constants.JsonContentHeader;
|
||||||
|
|
||||||
foreach (var header in headers)
|
if (headers != null)
|
||||||
request.AddHeader(header.Key, header.Value);
|
{
|
||||||
|
foreach (var header in headers)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
if (additionalHeaders != null)
|
if (additionalHeaders != null)
|
||||||
{
|
{
|
||||||
@@ -789,7 +869,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
{
|
{
|
||||||
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
if (bodyParameters.Any())
|
if (bodyParameters?.Any() == true)
|
||||||
WriteParamBody(request, bodyParameters, contentType);
|
WriteParamBody(request, bodyParameters, contentType);
|
||||||
else
|
else
|
||||||
request.SetContent(RequestBodyEmptyContent, contentType);
|
request.SetContent(RequestBodyEmptyContent, contentType);
|
||||||
@@ -804,12 +884,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="request">The request to set the parameters on</param>
|
/// <param name="request">The request to set the parameters on</param>
|
||||||
/// <param name="parameters">The parameters to set</param>
|
/// <param name="parameters">The parameters to set</param>
|
||||||
/// <param name="contentType">The content type of the data</param>
|
/// <param name="contentType">The content type of the data</param>
|
||||||
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
protected virtual void WriteParamBody(IRequest request, IDictionary<string, object> parameters, string contentType)
|
||||||
{
|
{
|
||||||
if (contentType == Constants.JsonContentHeader)
|
if (contentType == Constants.JsonContentHeader)
|
||||||
{
|
{
|
||||||
// Write the parameters as json in the body
|
// Write the parameters as json in the body
|
||||||
var stringData = CreateSerializer().Serialize(parameters);
|
string stringData;
|
||||||
|
if (parameters.Count == 1 && parameters.ContainsKey(Constants.BodyPlaceHolderKey))
|
||||||
|
stringData = CreateSerializer().Serialize(parameters[Constants.BodyPlaceHolderKey]);
|
||||||
|
else
|
||||||
|
stringData = CreateSerializer().Serialize(parameters);
|
||||||
request.SetContent(stringData, contentType);
|
request.SetContent(stringData, contentType);
|
||||||
}
|
}
|
||||||
else if (contentType == Constants.FormContentHeader)
|
else if (contentType == Constants.FormContentHeader)
|
||||||
@@ -859,6 +943,19 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new ServerRateLimitError(message);
|
return new ServerRateLimitError(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create the parameter IDictionary
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected internal IDictionary<string, object> CreateParameterDictionary(IDictionary<string, object> parameters)
|
||||||
|
{
|
||||||
|
if (!OrderParameters)
|
||||||
|
return parameters;
|
||||||
|
|
||||||
|
return new SortedDictionary<string, object>(parameters, ParameterOrderComparer);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
/// Retrieve the server time for the purpose of syncing time between client and server to prevent authentication issues
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -869,14 +966,14 @@ namespace CryptoExchange.Net.Clients
|
|||||||
{
|
{
|
||||||
var timeSyncParams = GetTimeSyncInfo();
|
var timeSyncParams = GetTimeSyncInfo();
|
||||||
if (timeSyncParams == null)
|
if (timeSyncParams == null)
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
|
|
||||||
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
|
||||||
{
|
{
|
||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
var localTime = DateTime.UtcNow;
|
var localTime = DateTime.UtcNow;
|
||||||
@@ -905,7 +1002,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
timeSyncParams.TimeSyncState.Semaphore.Release();
|
timeSyncParams.TimeSyncState.Semaphore.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, true, null);
|
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool ShouldCache(RequestDefinition definition)
|
||||||
|
=> ClientOptions.CachingEnabled
|
||||||
|
&& definition.Method == HttpMethod.Get
|
||||||
|
&& !definition.PreventCaching;
|
||||||
|
|
||||||
|
private bool ShouldCache(HttpMethod method)
|
||||||
|
=> ClientOptions.CachingEnabled
|
||||||
|
&& method == HttpMethod.Get;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using CryptoExchange.Net.RateLimiting.Interfaces;
|
|||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -52,11 +53,6 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal bool UnhandledMessageExpected { get; set; }
|
protected internal bool UnhandledMessageExpected { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true a subscription will accept message before the confirmation of a subscription has been received
|
|
||||||
/// </summary>
|
|
||||||
protected bool HandleMessageBeforeConfirmation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The rate limiters
|
/// The rate limiters
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -72,6 +68,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
protected List<PeriodicTaskRegistration> PeriodicTaskRegistrations { get; set; } = new List<PeriodicTaskRegistration>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of address to keep an alive connection to
|
||||||
|
/// </summary>
|
||||||
|
protected List<DedicatedConnectionConfig> DedicatedConnectionConfigs { get; set; } = new List<DedicatedConnectionConfig>();
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public double IncomingKbps
|
public double IncomingKbps
|
||||||
{
|
{
|
||||||
@@ -136,6 +137,16 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
protected internal virtual IMessageSerializer CreateSerializer() => new JsonNetMessageSerializer();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keep an open connection to this url
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url"></param>
|
||||||
|
/// <param name="auth"></param>
|
||||||
|
protected virtual void SetDedicatedConnection(string url, bool auth)
|
||||||
|
{
|
||||||
|
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a query to periodically send on each connection
|
/// Add a query to periodically send on each connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -178,7 +189,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
|
|
||||||
if (subscription.Authenticated && AuthenticationProvider == null)
|
if (subscription.Authenticated && AuthenticationProvider == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Failed to subscribe, private subscription but no API credentials set");
|
||||||
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
|
||||||
|
}
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
SocketConnection socketConnection;
|
||||||
var released = false;
|
var released = false;
|
||||||
@@ -198,12 +212,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetSocketConnection(url, subscription.Authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
socketConnection = socketResult.Data;
|
||||||
subscription.HandleUpdatesBeforeConfirmation = subscription.HandleUpdatesBeforeConfirmation || HandleMessageBeforeConfirmation;
|
|
||||||
|
|
||||||
// Add a subscription on the socket connection
|
// Add a subscription on the socket connection
|
||||||
var success = socketConnection.AddSubscription(subscription);
|
var success = socketConnection.AddSubscription(subscription);
|
||||||
@@ -241,7 +254,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
return new CallResult<UpdateSubscription>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitEvent = new ManualResetEvent(false);
|
var waitEvent = new AsyncResetEvent(false);
|
||||||
var subQuery = subscription.GetSubQuery(socketConnection);
|
var subQuery = subscription.GetSubQuery(socketConnection);
|
||||||
if (subQuery != null)
|
if (subQuery != null)
|
||||||
{
|
{
|
||||||
@@ -250,11 +263,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!subResult)
|
if (!subResult)
|
||||||
{
|
{
|
||||||
waitEvent?.Set();
|
waitEvent?.Set();
|
||||||
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
var isTimeout = subResult.Error is CancellationRequestedError;
|
||||||
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
if (isTimeout && subscription.Confirmed)
|
||||||
var unsubscribe = subResult.Error is CancellationRequestedError;
|
{
|
||||||
await socketConnection.CloseAsync(subscription, unsubscribe).ConfigureAwait(false);
|
// No response received, but the subscription did receive updates. We'll assume success
|
||||||
return new CallResult<UpdateSubscription>(subResult.Error!);
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
|
||||||
|
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
|
||||||
|
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
|
||||||
|
return new CallResult<UpdateSubscription>(subResult.Error!);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
@@ -278,34 +298,41 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
/// Send a query on a socket connection to the BaseAddress and wait for the response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<T>> QueryAsync<T>(Query<T> query)
|
protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return QueryAsync(BaseAddress, query);
|
return QueryAsync(BaseAddress, query, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a query on a socket connection and wait for the response
|
/// Send a query on a socket connection and wait for the response
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The expected result type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="url">The url for the request</param>
|
/// <param name="url">The url for the request</param>
|
||||||
/// <param name="query">The query</param>
|
/// <param name="query">The query</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, Query<T> query)
|
protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
|
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||||
|
|
||||||
SocketConnection socketConnection;
|
SocketConnection socketConnection;
|
||||||
var released = false;
|
var released = false;
|
||||||
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<T>(default);
|
return socketResult.As<THandlerResponse>(default);
|
||||||
|
|
||||||
socketConnection = socketResult.Data;
|
socketConnection = socketResult.Data;
|
||||||
|
|
||||||
@@ -318,7 +345,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
|
|
||||||
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
|
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
|
||||||
if (!connectResult)
|
if (!connectResult)
|
||||||
return new CallResult<T>(connectResult.Error!);
|
return new CallResult<THandlerResponse>(connectResult.Error!);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -329,10 +356,13 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (socketConnection.PausedActivity)
|
if (socketConnection.PausedActivity)
|
||||||
{
|
{
|
||||||
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
|
||||||
return new CallResult<T>(new ServerError("Socket is paused"));
|
return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false);
|
if (ct.IsCancellationRequested)
|
||||||
|
return new CallResult<THandlerResponse>(new CancellationRequestedError());
|
||||||
|
|
||||||
|
return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -356,7 +386,11 @@ namespace CryptoExchange.Net.Clients
|
|||||||
if (!authenticated || socket.Authenticated)
|
if (!authenticated || socket.Authenticated)
|
||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
|
|
||||||
return await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
await socket.CloseAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -370,7 +404,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
return new CallResult(new NoApiCredentialsError());
|
return new CallResult(new NoApiCredentialsError());
|
||||||
|
|
||||||
_logger.AttemptingToAuthenticate(socket.SocketId);
|
_logger.AttemptingToAuthenticate(socket.SocketId);
|
||||||
var authRequest = GetAuthenticationRequest();
|
var authRequest = GetAuthenticationRequest(socket);
|
||||||
if (authRequest != null)
|
if (authRequest != null)
|
||||||
{
|
{
|
||||||
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
var result = await socket.SendAndWaitQueryAsync(authRequest).ConfigureAwait(false);
|
||||||
@@ -395,7 +429,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// Should return the request which can be used to authenticate a socket connection
|
/// Should return the request which can be used to authenticate a socket connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected internal virtual Query? GetAuthenticationRequest() => throw new NotImplementedException();
|
protected internal virtual Query? GetAuthenticationRequest(SocketConnection connection) => throw new NotImplementedException();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a system subscription. Used for example to reply to ping requests
|
/// Adds a system subscription. Used for example to reply to ping requests
|
||||||
@@ -444,19 +478,31 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="address">The address the socket is for</param>
|
/// <param name="address">The address the socket is for</param>
|
||||||
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
/// <param name="authenticated">Whether the socket should be authenticated</param>
|
||||||
|
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& s.Value.ApiClient.GetType() == GetType()
|
&& s.Value.ApiClient.GetType() == GetType()
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault();
|
&& (s.Value.Authenticated == authenticated || !authenticated)
|
||||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
&& s.Value.Connected);
|
||||||
if (result != null)
|
|
||||||
|
SocketConnection connection;
|
||||||
|
if (!dedicatedRequestConnection)
|
||||||
{
|
{
|
||||||
if (result.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
|
connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection).FirstOrDefault().Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection != null)
|
||||||
|
{
|
||||||
|
if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget))
|
||||||
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new
|
||||||
return new CallResult<SocketConnection>(result);
|
return new CallResult<SocketConnection>(connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
@@ -473,6 +519,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var socket = CreateSocket(connectionAddress.Data!);
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
var socketConnection = new SocketConnection(_logger, this, socket, address);
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
|
socketConnection.DedicatedRequestConnection = dedicatedRequestConnection;
|
||||||
|
|
||||||
foreach (var ptg in PeriodicTaskRegistrations)
|
foreach (var ptg in PeriodicTaskRegistrations)
|
||||||
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, ptg.QueryDelegate, ptg.Callback);
|
||||||
@@ -515,7 +562,7 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <param name="address">The address to connect to</param>
|
/// <param name="address">The address to connect to</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
protected virtual WebSocketParameters GetWebSocketParameters(string address)
|
||||||
=> new(new Uri(address), ClientOptions.AutoReconnect)
|
=> new(new Uri(address), ClientOptions.ReconnectPolicy)
|
||||||
{
|
{
|
||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
ReconnectInterval = ClientOptions.ReconnectInterval,
|
ReconnectInterval = ClientOptions.ReconnectInterval,
|
||||||
@@ -592,8 +639,8 @@ namespace CryptoExchange.Net.Clients
|
|||||||
var tasks = new List<Task>();
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
var socketList = socketConnections.Values;
|
var socketList = socketConnections.Values;
|
||||||
foreach (var sub in socketList)
|
foreach (var connection in socketList.Where(s => !s.DedicatedRequestConnection))
|
||||||
tasks.Add(sub.CloseAsync());
|
tasks.Add(connection.CloseAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
@@ -616,6 +663,23 @@ namespace CryptoExchange.Net.Clients
|
|||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual async Task<CallResult> PrepareConnectionsAsync()
|
||||||
|
{
|
||||||
|
foreach (var item in DedicatedConnectionConfigs)
|
||||||
|
{
|
||||||
|
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
|
||||||
|
if (!socketResult)
|
||||||
|
return socketResult.AsDataless();
|
||||||
|
|
||||||
|
var connectResult = await ConnectIfNeededAsync(socketResult.Data, item.Authenticated).ConfigureAwait(false);
|
||||||
|
if (!connectResult)
|
||||||
|
return new CallResult(connectResult.Error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Log the current state of connections and subscriptions
|
/// Log the current state of connections and subscriptions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -699,11 +763,18 @@ namespace CryptoExchange.Net.Clients
|
|||||||
public override void Dispose()
|
public override void Dispose()
|
||||||
{
|
{
|
||||||
_disposing = true;
|
_disposing = true;
|
||||||
if (socketConnections.Sum(s => s.Value.UserSubscriptionCount) > 0)
|
var tasks = new List<Task>();
|
||||||
{
|
{
|
||||||
_logger.DisposingSocketClient();
|
var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
|
||||||
_ = UnsubscribeAllAsync();
|
if (socketList.Any())
|
||||||
|
_logger.DisposingSocketClient();
|
||||||
|
|
||||||
|
foreach (var connection in socketList)
|
||||||
|
{
|
||||||
|
tasks.Add(connection.CloseAsync());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
semaphoreSlim?.Dispose();
|
semaphoreSlim?.Dispose();
|
||||||
base.Dispose();
|
base.Dispose();
|
||||||
}
|
}
|
||||||
@@ -718,9 +789,10 @@ namespace CryptoExchange.Net.Clients
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Preprocess a stream message
|
/// Preprocess a stream message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="connection"></param>
|
||||||
/// <param name="type"></param>
|
/// <param name="type"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.JsonNet
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
||||||
|
/// </summary>
|
||||||
|
public class BigDecimalConverter : JsonConverter
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanConvert(Type objectType)
|
||||||
|
{
|
||||||
|
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||||
|
return Nullable.GetUnderlyingType(objectType) == typeof(decimal);
|
||||||
|
return objectType == typeof(decimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonToken.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonToken.Float || reader.TokenType == JsonToken.Integer)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return decimal.Parse(reader.Value!.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
// Value doesn't fit decimal; set it to max value
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonToken.String)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var value = reader.Value!.ToString();
|
||||||
|
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
// Value doesn't fit decimal; set it to max value
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
|
||||||
|
{
|
||||||
|
writer.WriteValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Newtonsoft.Json;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@@ -38,14 +39,8 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
var longValue = (long)reader.Value;
|
var longValue = (long)reader.Value;
|
||||||
if (longValue == 0 || longValue == -1)
|
if (longValue == 0 || longValue == -1)
|
||||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
return ParseFromLong(longValue);
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonToken.Float)
|
else if (reader.TokenType is JsonToken.Float)
|
||||||
{
|
{
|
||||||
@@ -68,76 +63,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
return ParseFromString(stringValue);
|
||||||
{
|
|
||||||
// Parse 202303261200 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
|
||||||
{
|
|
||||||
// Parse 20211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 6)
|
|
||||||
{
|
|
||||||
// Parse 211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
|
||||||
{
|
|
||||||
// Parse 1637745563.000 format
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
if (doubleValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
|
||||||
if (doubleValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(stringValue.Length == 10)
|
|
||||||
{
|
|
||||||
// Parse 2021-11-03 format
|
|
||||||
var values = stringValue.Split('-');
|
|
||||||
if(!int.TryParse(values[0], out var year)
|
|
||||||
|| !int.TryParse(values[1], out var month)
|
|
||||||
|| !int.TryParse(values[2], out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + reader.Value);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
}
|
||||||
else if(reader.TokenType == JsonToken.Date)
|
else if(reader.TokenType == JsonToken.Date)
|
||||||
{
|
{
|
||||||
@@ -150,6 +76,102 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a long value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="longValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromLong(long longValue)
|
||||||
|
{
|
||||||
|
if (longValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(longValue);
|
||||||
|
if (longValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(longValue);
|
||||||
|
if (longValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(longValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a string value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromString(string stringValue)
|
||||||
|
{
|
||||||
|
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
||||||
|
{
|
||||||
|
// Parse 202303261200 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||||
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 8)
|
||||||
|
{
|
||||||
|
// Parse 20211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 6)
|
||||||
|
{
|
||||||
|
// Parse 211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
|
{
|
||||||
|
// Parse 1637745563.000 format
|
||||||
|
if (doubleValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
if (doubleValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds((long)doubleValue);
|
||||||
|
if (doubleValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds((long)doubleValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds((long)doubleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 10)
|
||||||
|
{
|
||||||
|
// Parse 2021-11-03 format
|
||||||
|
var values = stringValue.Split('-');
|
||||||
|
if (!int.TryParse(values[0], out var year)
|
||||||
|
|| !int.TryParse(values[1], out var month)
|
||||||
|
|| !int.TryParse(values[2], out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> Read(Stream stream, bool bufferStream)
|
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||||
{
|
{
|
||||||
if (bufferStream && stream is not MemoryStream)
|
if (bufferStream && stream is not MemoryStream)
|
||||||
{
|
{
|
||||||
@@ -252,14 +252,15 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
{
|
{
|
||||||
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string GetOriginalString()
|
public override string GetOriginalString()
|
||||||
@@ -290,7 +291,7 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
private ReadOnlyMemory<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Read(ReadOnlyMemory<byte> data)
|
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
_bytes = data;
|
_bytes = data;
|
||||||
|
|
||||||
@@ -305,14 +306,14 @@ namespace CryptoExchange.Net.Converters.JsonNet
|
|||||||
{
|
{
|
||||||
_token = JToken.Load(jsonTextReader);
|
_token = JToken.Load(jsonTextReader);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
||||||
public Type? JsonConverterType { get; set; }
|
public Type? JsonConverterType { get; set; }
|
||||||
public bool DefaultDeserialization { get; set; }
|
public bool DefaultDeserialization { get; set; }
|
||||||
|
public Type TargetType { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
private class ArrayConverterInner<T> : JsonConverter<T>
|
||||||
@@ -70,7 +71,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
ArrayProperty = att,
|
ArrayProperty = att,
|
||||||
PropertyInfo = property,
|
PropertyInfo = property,
|
||||||
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
|
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
|
||||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType
|
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
|
||||||
|
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +83,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType)
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
throw new Exception("1");
|
throw new Exception("Not an array");
|
||||||
|
|
||||||
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
||||||
attributes = CacheTypeAttributes(objectType);
|
attributes = CacheTypeAttributes(objectType);
|
||||||
@@ -92,9 +94,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
var attribute = attributes.SingleOrDefault(a => a.ArrayProperty.Index == index);
|
||||||
var targetType = attribute.PropertyInfo.PropertyType;
|
if (attribute == null)
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetType = attribute.TargetType;
|
||||||
object? value = null;
|
object? value = null;
|
||||||
if (attribute.JsonConverterType != null)
|
if (attribute.JsonConverterType != null)
|
||||||
{
|
{
|
||||||
@@ -121,7 +128,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, attribute.PropertyInfo.PropertyType, CultureInfo.InvariantCulture));
|
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||||
|
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
||||||
|
/// </summary>
|
||||||
|
public class BigDecimalConverter : JsonConverter<decimal>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return decimal.Parse(reader.GetString(), NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
catch(OverflowException)
|
||||||
|
{
|
||||||
|
// Value doesn't fit decimal, default to max value
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return reader.GetDecimal();
|
||||||
|
}
|
||||||
|
catch(FormatException)
|
||||||
|
{
|
||||||
|
// Format issue, assume value is too large
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteNumberValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -49,14 +50,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
var longValue = reader.GetDouble();
|
var longValue = reader.GetDouble();
|
||||||
if (longValue == 0 || longValue == -1)
|
if (longValue == 0 || longValue == -1)
|
||||||
return default;
|
return default;
|
||||||
if (longValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(longValue);
|
|
||||||
if (longValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds(longValue);
|
|
||||||
if (longValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds(longValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds(longValue);
|
return ParseFromDouble(longValue);
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonTokenType.String)
|
else if (reader.TokenType is JsonTokenType.String)
|
||||||
{
|
{
|
||||||
@@ -68,76 +63,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
|
return ParseFromString(stringValue!);
|
||||||
{
|
|
||||||
// Parse 202303261200 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
|
||||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
|
||||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 8)
|
|
||||||
{
|
|
||||||
// Parse 20211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 6)
|
|
||||||
{
|
|
||||||
// Parse 211103 format
|
|
||||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
|
||||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
|
||||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
|
||||||
{
|
|
||||||
// Parse 1637745563.000 format
|
|
||||||
if (doubleValue < 19999999999)
|
|
||||||
return ConvertFromSeconds(doubleValue);
|
|
||||||
if (doubleValue < 19999999999999)
|
|
||||||
return ConvertFromMilliseconds((long)doubleValue);
|
|
||||||
if (doubleValue < 19999999999999999)
|
|
||||||
return ConvertFromMicroseconds((long)doubleValue);
|
|
||||||
|
|
||||||
return ConvertFromNanoseconds((long)doubleValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stringValue.Length == 10)
|
|
||||||
{
|
|
||||||
// Parse 2021-11-03 format
|
|
||||||
var values = stringValue.Split('-');
|
|
||||||
if (!int.TryParse(values[0], out var year)
|
|
||||||
|| !int.TryParse(values[1], out var month)
|
|
||||||
|| !int.TryParse(values[2], out var day))
|
|
||||||
{
|
|
||||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -160,6 +86,104 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a long value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="longValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromDouble(double longValue)
|
||||||
|
{
|
||||||
|
if (longValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(longValue);
|
||||||
|
if (longValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(longValue);
|
||||||
|
if (longValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(longValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(longValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a string value to datetime
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="stringValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static DateTime ParseFromString(string stringValue)
|
||||||
|
{
|
||||||
|
if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
|
||||||
|
{
|
||||||
|
// Parse 202303261200 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||||
|
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||||
|
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 8)
|
||||||
|
{
|
||||||
|
// Parse 20211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 6)
|
||||||
|
{
|
||||||
|
// Parse 211103 format
|
||||||
|
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||||
|
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||||
|
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||||
|
{
|
||||||
|
// Parse 1637745563.000 format
|
||||||
|
if (doubleValue <= 0)
|
||||||
|
return default;
|
||||||
|
if (doubleValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(doubleValue);
|
||||||
|
if (doubleValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds((long)doubleValue);
|
||||||
|
if (doubleValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds((long)doubleValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds((long)doubleValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stringValue.Length == 10)
|
||||||
|
{
|
||||||
|
// Parse 2021-11-03 format
|
||||||
|
var values = stringValue.Split('-');
|
||||||
|
if (!int.TryParse(values[0], out var year)
|
||||||
|
|| !int.TryParse(values[1], out var month)
|
||||||
|
|| !int.TryParse(values[2], out var day))
|
||||||
|
{
|
||||||
|
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -19,13 +19,29 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (reader.TokenType == JsonTokenType.String)
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
{
|
{
|
||||||
var value = reader.GetString();
|
var value = reader.GetString();
|
||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value) || string.Equals("null", value))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
try
|
||||||
|
{
|
||||||
|
return decimal.Parse(value, NumberStyles.Float, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
catch(OverflowException)
|
||||||
|
{
|
||||||
|
// Value doesn't fit decimal, default to max value
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader.GetDecimal();
|
try
|
||||||
|
{
|
||||||
|
return reader.GetDecimal();
|
||||||
|
}
|
||||||
|
catch(FormatException)
|
||||||
|
{
|
||||||
|
// Format issue, assume value is too large
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -211,5 +211,37 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
|
|
||||||
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
return enumValue == null ? null : (mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the enum value from a string
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Enum type</typeparam>
|
||||||
|
/// <param name="value">String value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static T? ParseString<T>(string value) where T : Enum
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
if (!_mapping.TryGetValue(type, out var enumMapping))
|
||||||
|
enumMapping = AddMapping(type);
|
||||||
|
|
||||||
|
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||||
|
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
||||||
|
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
|
||||||
|
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
||||||
|
{
|
||||||
|
return (T)mapping.Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// If no explicit mapping is found try to parse string
|
||||||
|
return (T)Enum.Parse(type, value, true);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Int converter
|
||||||
|
/// </summary>
|
||||||
|
public class IntConverter : JsonConverter<int?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.GetInt32();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Int converter
|
||||||
|
/// </summary>
|
||||||
|
public class LongConverter : JsonConverter<long?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.GetInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Read string or number as string
|
||||||
|
/// </summary>
|
||||||
|
public class NumberStringConverter : JsonConverter<string?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.Number)
|
||||||
|
{
|
||||||
|
if (reader.TryGetInt64(out var value))
|
||||||
|
return value.ToString();
|
||||||
|
|
||||||
|
return reader.GetDecimal().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return reader.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
///
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T"></typeparam>
|
||||||
|
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
writer.WriteStringValue("");
|
||||||
|
|
||||||
|
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
new EnumConverter(),
|
new EnumConverter(),
|
||||||
new BoolConverter(),
|
new BoolConverter(),
|
||||||
new DecimalConverter(),
|
new DecimalConverter(),
|
||||||
|
new IntConverter(),
|
||||||
|
new LongConverter()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var info = $"Unknown exception: {ex.Message}";
|
||||||
|
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -118,6 +123,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
|
if (typeof(T) == typeof(string))
|
||||||
|
{
|
||||||
|
if (value.Value.ValueKind == JsonValueKind.Number)
|
||||||
|
return (T)(object)value.Value.GetInt64().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
return value.Value.Deserialize<T>();
|
return value.Value.Deserialize<T>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +199,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<bool> Read(Stream stream, bool bufferStream)
|
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||||
{
|
{
|
||||||
if (bufferStream && stream is not MemoryStream)
|
if (bufferStream && stream is not MemoryStream)
|
||||||
{
|
{
|
||||||
@@ -211,15 +222,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string GetOriginalString()
|
public override string GetOriginalString()
|
||||||
{
|
{
|
||||||
@@ -236,6 +248,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
{
|
{
|
||||||
_stream?.Dispose();
|
_stream?.Dispose();
|
||||||
_stream = null;
|
_stream = null;
|
||||||
|
_document?.Dispose();
|
||||||
_document = null;
|
_document = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,22 +262,30 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
private ReadOnlyMemory<byte> _bytes;
|
private ReadOnlyMemory<byte> _bytes;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Read(ReadOnlyMemory<byte> data)
|
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
_bytes = data;
|
_bytes = data;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var firstByte = data.Span[0];
|
||||||
|
if (firstByte != 0x7b && firstByte != 0x5b)
|
||||||
|
{
|
||||||
|
// Value doesn't start with `{` or `[`, prevent deserialization attempt as it's slow
|
||||||
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("Not a json value"));
|
||||||
|
}
|
||||||
|
|
||||||
_document = JsonDocument.Parse(data);
|
_document = JsonDocument.Parse(data);
|
||||||
IsJson = true;
|
IsJson = true;
|
||||||
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Not a json message
|
// Not a json message
|
||||||
IsJson = false;
|
IsJson = false;
|
||||||
|
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
return IsJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -283,6 +304,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|
|||||||
public override void Clear()
|
public override void Clear()
|
||||||
{
|
{
|
||||||
_bytes = null;
|
_bytes = null;
|
||||||
|
_document?.Dispose();
|
||||||
_document = null;
|
_document = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>7.3.3</PackageVersion>
|
<PackageVersion>7.11.1</PackageVersion>
|
||||||
<AssemblyVersion>7.3.3</AssemblyVersion>
|
<AssemblyVersion>7.11.1</AssemblyVersion>
|
||||||
<FileVersion>7.3.3</FileVersion>
|
<FileVersion>7.11.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</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
@@ -58,6 +58,6 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
|
||||||
<PackageReference Include="System.Text.Json" Version="8.0.3" />
|
<PackageReference Include="System.Text.Json" Version="8.0.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using System;
|
using System;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@@ -15,10 +16,6 @@ namespace CryptoExchange.Net
|
|||||||
/// The last used id, use NextId() to get the next id and up this
|
/// The last used id, use NextId() to get the next id and up this
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static int _lastId;
|
private static int _lastId;
|
||||||
/// <summary>
|
|
||||||
/// Lock for id generating
|
|
||||||
/// </summary>
|
|
||||||
private static object _idLock = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clamp a value between a min and max
|
/// Clamp a value between a min and max
|
||||||
@@ -135,24 +132,13 @@ namespace CryptoExchange.Net
|
|||||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static int NextId()
|
public static int NextId() => Interlocked.Increment(ref _lastId);
|
||||||
{
|
|
||||||
lock (_idLock)
|
|
||||||
{
|
|
||||||
_lastId += 1;
|
|
||||||
return _lastId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Return the last unique id that was generated
|
/// Return the last unique id that was generated
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static int LastId()
|
public static int LastId() => _lastId;
|
||||||
{
|
|
||||||
lock (_idLock)
|
|
||||||
return _lastId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generate a random string of specified length
|
/// Generate a random string of specified length
|
||||||
|
|||||||
@@ -96,6 +96,9 @@ namespace CryptoExchange.Net
|
|||||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||||
foreach (var kvp in parameters)
|
foreach (var kvp in parameters)
|
||||||
{
|
{
|
||||||
|
if (kvp.Value is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
if (kvp.Value.GetType().IsArray)
|
if (kvp.Value.GetType().IsArray)
|
||||||
{
|
{
|
||||||
var array = (Array)kvp.Value;
|
var array = (Array)kvp.Value;
|
||||||
@@ -110,92 +113,6 @@ namespace CryptoExchange.Net
|
|||||||
return formData.ToString();
|
return formData.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the string the secure string is representing
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source">The source secure string</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string GetString(this SecureString source)
|
|
||||||
{
|
|
||||||
lock (source)
|
|
||||||
{
|
|
||||||
string result;
|
|
||||||
var length = source.Length;
|
|
||||||
var pointer = IntPtr.Zero;
|
|
||||||
var chars = new char[length];
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
pointer = Marshal.SecureStringToBSTR(source);
|
|
||||||
Marshal.Copy(pointer, chars, 0, length);
|
|
||||||
|
|
||||||
result = string.Join("", chars);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (pointer != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Marshal.ZeroFreeBSTR(pointer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Are 2 secure strings equal
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="ss1">Source secure string</param>
|
|
||||||
/// <param name="ss2">Compare secure string</param>
|
|
||||||
/// <returns>True if equal by value</returns>
|
|
||||||
public static bool IsEqualTo(this SecureString ss1, SecureString ss2)
|
|
||||||
{
|
|
||||||
IntPtr bstr1 = IntPtr.Zero;
|
|
||||||
IntPtr bstr2 = IntPtr.Zero;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
bstr1 = Marshal.SecureStringToBSTR(ss1);
|
|
||||||
bstr2 = Marshal.SecureStringToBSTR(ss2);
|
|
||||||
int length1 = Marshal.ReadInt32(bstr1, -4);
|
|
||||||
int length2 = Marshal.ReadInt32(bstr2, -4);
|
|
||||||
if (length1 == length2)
|
|
||||||
{
|
|
||||||
for (int x = 0; x < length1; ++x)
|
|
||||||
{
|
|
||||||
byte b1 = Marshal.ReadByte(bstr1, x);
|
|
||||||
byte b2 = Marshal.ReadByte(bstr2, x);
|
|
||||||
if (b1 != b2) return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (bstr2 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr2);
|
|
||||||
if (bstr1 != IntPtr.Zero) Marshal.ZeroFreeBSTR(bstr1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a secure string from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="source"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static SecureString ToSecureString(this string source)
|
|
||||||
{
|
|
||||||
var secureString = new SecureString();
|
|
||||||
foreach (var c in source)
|
|
||||||
secureString.AppendChar(c);
|
|
||||||
secureString.MakeReadOnly();
|
|
||||||
return secureString;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Validates an int is one of the allowed values
|
/// Validates an int is one of the allowed values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -315,26 +232,6 @@ namespace CryptoExchange.Net
|
|||||||
return url.TrimEnd('/');
|
return url.TrimEnd('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fill parameters in a path. Parameters are specified by '{}' and should be specified in occuring sequence
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="path">The total path string</param>
|
|
||||||
/// <param name="values">The values to fill</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string FillPathParameters(this string path, params string[] values)
|
|
||||||
{
|
|
||||||
foreach (var value in values)
|
|
||||||
{
|
|
||||||
var index = path.IndexOf("{}", StringComparison.Ordinal);
|
|
||||||
if (index >= 0)
|
|
||||||
{
|
|
||||||
path = path.Remove(index, 2);
|
|
||||||
path = path.Insert(index, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new uri with the provided parameters as query
|
/// Create a new uri with the provided parameters as query
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -342,7 +239,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="baseUri"></param>
|
/// <param name="baseUri"></param>
|
||||||
/// <param name="arraySerialization"></param>
|
/// <param name="arraySerialization"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static Uri SetParameters(this Uri baseUri, SortedDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
||||||
{
|
{
|
||||||
var uriBuilder = new UriBuilder();
|
var uriBuilder = new UriBuilder();
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
uriBuilder.Scheme = baseUri.Scheme;
|
||||||
@@ -450,7 +347,7 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompress using Gzip
|
/// Decompress using GzipStream
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
@@ -464,6 +361,23 @@ namespace CryptoExchange.Net
|
|||||||
deflateStream.CopyTo(decompressedStream);
|
deflateStream.CopyTo(decompressedStream);
|
||||||
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decompress using DeflateStream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
|
||||||
|
{
|
||||||
|
var output = new MemoryStream();
|
||||||
|
|
||||||
|
using (var compressStream = new MemoryStream(input.ToArray()))
|
||||||
|
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
|
||||||
|
decompressor.CopyTo(output);
|
||||||
|
|
||||||
|
output.Position = 0;
|
||||||
|
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Time provider
|
||||||
|
/// </summary>
|
||||||
|
internal interface IAuthTimeProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get current time
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
DateTime GetTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,14 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
string BaseAddress { get; }
|
string BaseAddress { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Format a base and quote asset to an exchange accepted symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAsset">The base asset</param>
|
||||||
|
/// <param name="quoteAsset">The quote asset</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string FormatSymbol(string baseAsset, string quoteAsset);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the API credentials for this API client
|
/// Set the API credentials for this API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="stream"></param>
|
/// <param name="stream"></param>
|
||||||
/// <param name="bufferStream"></param>
|
/// <param name="bufferStream"></param>
|
||||||
Task<bool> Read(Stream stream, bool bufferStream);
|
Task<CallResult> Read(Stream stream, bool bufferStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -96,6 +96,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// Load a data message
|
/// Load a data message
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
bool Read(ReadOnlyMemory<byte> data);
|
CallResult Read(ReadOnlyMemory<byte> data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects.Sockets;
|
|||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces
|
namespace CryptoExchange.Net.Interfaces
|
||||||
{
|
{
|
||||||
@@ -16,10 +17,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; }
|
public int Id { get; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this listener can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData { get; }
|
|
||||||
/// <summary>
|
|
||||||
/// The identifiers for this processor
|
/// The identifiers for this processor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public HashSet<string> ListenerIdentifiers { get; }
|
public HashSet<string> ListenerIdentifiers { get; }
|
||||||
@@ -29,7 +26,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
CallResult Handle(SocketConnection connection, DataEvent<object> message);
|
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the type the message should be deserialized to
|
/// Get the type the message should be deserialized to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Factory for ISymbolOrderBook instances
|
||||||
|
/// </summary>
|
||||||
|
public interface IOrderBookFactory<TOptions> where TOptions : OrderBookOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new order book by symbol name
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">Symbol name</param>
|
||||||
|
/// <param name="options">Options for the order book</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null);
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new order book by base and quote asset names
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="baseAsset">Base asset name</param>
|
||||||
|
/// <param name="quoteAsset">Quote asset name</param>
|
||||||
|
/// <param name="options">Options for the order book</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,6 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="requestWeight">The weight of the request</param>
|
/// <param name="requestWeight">The weight of the request</param>
|
||||||
/// <param name="ct">Cancellation token to cancel waiting</param>
|
/// <param name="ct">Cancellation token to cancel waiting</param>
|
||||||
/// <returns>The time in milliseconds spend waiting</returns>
|
/// <returns>The time in milliseconds spend waiting</returns>
|
||||||
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, SecureString? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
Task<CallResult<int>> LimitRequestAsync(ILogger log, string endpoint, HttpMethod method, bool signed, string? apiKey, RateLimitingBehaviour limitBehaviour, int requestWeight, CancellationToken ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -59,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task UnsubscribeAsync(UpdateSubscription subscription);
|
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepare connections which can subsequently be used for sending websocket requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<CallResult> PrepareConnectionsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Websocket message received event
|
/// Websocket message received event
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event Action<WebSocketMessageType, ReadOnlyMemory<byte>> OnStreamMessage;
|
event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Websocket sent event, RequestId as parameter
|
/// Websocket sent event, RequestId as parameter
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -78,7 +78,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <param name="id"></param>
|
/// <param name="id"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <param name="weight"></param>
|
/// <param name="weight"></param>
|
||||||
void Send(int id, string data, int weight);
|
bool Send(int id, string data, int weight);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reconnect the socket
|
/// Reconnect the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
+28
-2
@@ -3,7 +3,8 @@ using System;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Logging.Extensions
|
namespace CryptoExchange.Net.Logging.Extensions
|
||||||
{
|
{
|
||||||
internal static class CryptoExchangeWebSocketClientLoggingExtension
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
public static class CryptoExchangeWebSocketClientLoggingExtension
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, int, Exception?> _connecting;
|
private static readonly Action<ILogger, int, Exception?> _connecting;
|
||||||
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
|
private static readonly Action<ILogger, int, string, Exception?> _connectionFailed;
|
||||||
@@ -24,6 +25,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
|
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
|
||||||
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
|
private static readonly Action<ILogger, int, Exception?> _sendLoopFinished;
|
||||||
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage;
|
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseMessage;
|
||||||
|
private static readonly Action<ILogger, int, string, string ,Exception?> _receivedCloseConfirmation;
|
||||||
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
|
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
|
||||||
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
|
private static readonly Action<ILogger, int, int, Exception?> _receivedSingleMessage;
|
||||||
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
|
private static readonly Action<ILogger, int, long, Exception?> _reassembledMessage;
|
||||||
@@ -32,6 +34,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished;
|
private static readonly Action<ILogger, int, Exception?> _receiveLoopFinished;
|
||||||
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
|
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _startingTaskForNoDataReceivedCheck;
|
||||||
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
|
private static readonly Action<ILogger, int, TimeSpan?, Exception?> _noDataReceiveTimoutReconnect;
|
||||||
|
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
|
||||||
|
|
||||||
static CryptoExchangeWebSocketClientLoggingExtension()
|
static CryptoExchangeWebSocketClientLoggingExtension()
|
||||||
{
|
{
|
||||||
@@ -151,7 +154,7 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
"[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes");
|
"[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes");
|
||||||
|
|
||||||
_receiveLoopStoppedWithException = LoggerMessage.Define<int>(
|
_receiveLoopStoppedWithException = LoggerMessage.Define<int>(
|
||||||
LogLevel.Warning,
|
LogLevel.Error,
|
||||||
new EventId(1024, "ReceiveLoopStoppedWithException"),
|
new EventId(1024, "ReceiveLoopStoppedWithException"),
|
||||||
"[Sckt {SocketId}] receive loop stopped with exception");
|
"[Sckt {SocketId}] receive loop stopped with exception");
|
||||||
|
|
||||||
@@ -169,6 +172,17 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
|
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
|
||||||
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
|
"[Sckt {SocketId}] no data received for {Timeout}, reconnecting socket");
|
||||||
|
|
||||||
|
_receivedCloseConfirmation = LoggerMessage.Define<int, string, string>(
|
||||||
|
LogLevel.Debug,
|
||||||
|
new EventId(1028, "ReceivedCloseMessage"),
|
||||||
|
"[Sckt {SocketId}] received `Close` message confirming our close request, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}");
|
||||||
|
|
||||||
|
_socketProcessingStateChanged = LoggerMessage.Define<int, string, string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(1028, "SocketProcessingStateChanged"),
|
||||||
|
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SocketConnecting(
|
public static void SocketConnecting(
|
||||||
@@ -285,6 +299,12 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
|
_receivedCloseMessage(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SocketReceivedCloseConfirmation(
|
||||||
|
this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription)
|
||||||
|
{
|
||||||
|
_receivedCloseConfirmation(logger, socketId, webSocketCloseStatus, closeStatusDescription, null);
|
||||||
|
}
|
||||||
|
|
||||||
public static void SocketReceivedPartialMessage(
|
public static void SocketReceivedPartialMessage(
|
||||||
this ILogger logger, int socketId, int countBytes)
|
this ILogger logger, int socketId, int countBytes)
|
||||||
{
|
{
|
||||||
@@ -332,5 +352,11 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
|
_noDataReceiveTimoutReconnect(logger, socketId, timeSpan, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SocketProcessingStateChanged(
|
||||||
|
this ILogger logger, int socketId, string prevState, string newState)
|
||||||
|
{
|
||||||
|
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ using System;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Logging.Extensions
|
namespace CryptoExchange.Net.Logging.Extensions
|
||||||
{
|
{
|
||||||
internal static class RateLimitGateLoggingExtensions
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
public static class RateLimitGateLoggingExtensions
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, int, string, string, string, Exception?> _rateLimitRequestFailed;
|
private static readonly Action<ILogger, int, string, string, string, Exception?> _rateLimitRequestFailed;
|
||||||
private static readonly Action<ILogger, int, string, string, Exception?> _rateLimitConnectionFailed;
|
private static readonly Action<ILogger, int, string, string, Exception?> _rateLimitConnectionFailed;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ using System.Net.Http;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Logging.Extensions
|
namespace CryptoExchange.Net.Logging.Extensions
|
||||||
{
|
{
|
||||||
internal static class RestApiClientLoggingExtensions
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
public static class RestApiClientLoggingExtensions
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
|
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiErrorReceived;
|
||||||
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
|
private static readonly Action<ILogger, int?, int?, long, string?, Exception?> _restApiResponseReceived;
|
||||||
@@ -17,6 +18,9 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitRetry;
|
||||||
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
private static readonly Action<ILogger, int, DateTime, Exception?> _restApiRateLimitPauseUntil;
|
||||||
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
private static readonly Action<ILogger, int, RequestDefinition, string?, string, string, Exception?> _restApiSendRequest;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCheckingCache;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
|
||||||
|
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
|
||||||
|
|
||||||
|
|
||||||
static RestApiClientLoggingExtensions()
|
static RestApiClientLoggingExtensions()
|
||||||
@@ -65,6 +69,21 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
new EventId(4008, "RestApiSendRequest"),
|
new EventId(4008, "RestApiSendRequest"),
|
||||||
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
"[Req {RequestId}] Sending {Definition} request with body {Body}, query parameters {Query} and headers {Headers}");
|
||||||
|
|
||||||
|
_restApiCheckingCache = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4009, "RestApiCheckingCache"),
|
||||||
|
"Checking cache for key {Key}");
|
||||||
|
|
||||||
|
_restApiCacheHit = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4010, "RestApiCacheHit"),
|
||||||
|
"Cache hit for key {Key}");
|
||||||
|
|
||||||
|
_restApiCacheNotHit = LoggerMessage.Define<string>(
|
||||||
|
LogLevel.Trace,
|
||||||
|
new EventId(4011, "RestApiCacheNotHit"),
|
||||||
|
"Cache not hit for key {Key}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error)
|
||||||
@@ -111,5 +130,20 @@ namespace CryptoExchange.Net.Logging.Extensions
|
|||||||
{
|
{
|
||||||
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
_restApiSendRequest(logger, requestId, definition, body, query, headers, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void CheckingCache(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCheckingCache(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CacheHit(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCacheHit(logger, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CacheNotHit(this ILogger logger, string key)
|
||||||
|
{
|
||||||
|
_restApiCacheNotHit(logger, key, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ using System;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Logging.Extensions
|
namespace CryptoExchange.Net.Logging.Extensions
|
||||||
{
|
{
|
||||||
internal static class SocketApiClientLoggingExtension
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
public static class SocketApiClientLoggingExtension
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, int, Exception?> _failedToAddSubscriptionRetryOnDifferentConnection;
|
private static readonly Action<ILogger, int, Exception?> _failedToAddSubscriptionRetryOnDifferentConnection;
|
||||||
private static readonly Action<ILogger, int, Exception?> _hasBeenPausedCantSubscribeAtThisMoment;
|
private static readonly Action<ILogger, int, Exception?> _hasBeenPausedCantSubscribeAtThisMoment;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Logging.Extensions
|
namespace CryptoExchange.Net.Logging.Extensions
|
||||||
{
|
{
|
||||||
internal static class SocketConnectionLoggingExtension
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
public static class SocketConnectionLoggingExtension
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, int, bool, Exception?> _activityPaused;
|
private static readonly Action<ILogger, int, bool, Exception?> _activityPaused;
|
||||||
private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged;
|
private static readonly Action<ILogger, int, Sockets.SocketConnection.SocketStatus, Sockets.SocketConnection.SocketStatus, Exception?> _socketStatusChanged;
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ using Microsoft.Extensions.Logging;
|
|||||||
|
|
||||||
namespace CryptoExchange.Net.Logging.Extensions
|
namespace CryptoExchange.Net.Logging.Extensions
|
||||||
{
|
{
|
||||||
internal static class SymbolOrderBookLoggingExtensions
|
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
|
||||||
|
|
||||||
|
public static class SymbolOrderBookLoggingExtensions
|
||||||
{
|
{
|
||||||
private static readonly Action<ILogger, string, string, OrderBookStatus, OrderBookStatus, Exception?> _orderBookStatusChanged;
|
private static readonly Action<ILogger, string, string, OrderBookStatus, OrderBookStatus, Exception?> _orderBookStatusChanged;
|
||||||
private static readonly Action<ILogger, string, string, Exception?> _orderBookStarting;
|
private static readonly Action<ILogger, string, string, Exception?> _orderBookStarting;
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The password of the proxy
|
/// The password of the proxy
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SecureString? Password { get; }
|
public string? Password { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create new settings for a proxy
|
/// Create new settings for a proxy
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="host">The proxy hostname/ip</param>
|
/// <param name="host">The proxy hostname/ip</param>
|
||||||
/// <param name="port">The proxy port</param>
|
/// <param name="port">The proxy port</param>
|
||||||
public ApiProxy(string host, int port): this(host, port, null, (SecureString?)null)
|
public ApiProxy(string host, int port): this(host, port, null, null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,18 +42,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="port">The proxy port</param>
|
/// <param name="port">The proxy port</param>
|
||||||
/// <param name="login">The proxy login</param>
|
/// <param name="login">The proxy login</param>
|
||||||
/// <param name="password">The proxy password</param>
|
/// <param name="password">The proxy password</param>
|
||||||
public ApiProxy(string host, int port, string? login, string? password) : this(host, port, login, password?.ToSecureString())
|
public ApiProxy(string host, int port, string? login, string? password)
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create new settings for a proxy
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="host">The proxy hostname/ip</param>
|
|
||||||
/// <param name="port">The proxy port</param>
|
|
||||||
/// <param name="login">The proxy login</param>
|
|
||||||
/// <param name="password">The proxy password</param>
|
|
||||||
public ApiProxy(string host, int port, string? login, SecureString? password)
|
|
||||||
{
|
{
|
||||||
Host = host;
|
Host = host;
|
||||||
Port = port;
|
Port = port;
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Wait for the AutoResetEvent to be set
|
/// Wait for the AutoResetEvent to be set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public Task<bool> WaitAsync(TimeSpan? timeout = null)
|
public Task<bool> WaitAsync(TimeSpan? timeout = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
lock (_waits)
|
lock (_waits)
|
||||||
{
|
{
|
||||||
@@ -44,22 +44,29 @@ namespace CryptoExchange.Net.Objects
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
if (ct.IsCancellationRequested)
|
||||||
if(timeout != null)
|
return _completed;
|
||||||
{
|
|
||||||
var cancellationSource = new CancellationTokenSource(timeout.Value);
|
|
||||||
var registration = cancellationSource.Token.Register(() =>
|
|
||||||
{
|
|
||||||
lock (_waits)
|
|
||||||
{
|
|
||||||
tcs.TrySetResult(false);
|
|
||||||
|
|
||||||
// Not the cleanest but it works
|
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
if (timeout.HasValue)
|
||||||
}
|
{
|
||||||
}, useSynchronizationContext: false);
|
var timeoutSource = new CancellationTokenSource(timeout.Value);
|
||||||
|
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, ct);
|
||||||
|
ct = cancellationSource.Token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var registration = ct.Register(() =>
|
||||||
|
{
|
||||||
|
lock (_waits)
|
||||||
|
{
|
||||||
|
tcs.TrySetResult(false);
|
||||||
|
|
||||||
|
// Not the cleanest but it works
|
||||||
|
_waits = new Queue<TaskCompletionSource<bool>>(_waits.Where(i => i != tcs));
|
||||||
|
}
|
||||||
|
}, useSynchronizationContext: false);
|
||||||
|
|
||||||
|
|
||||||
_waits.Enqueue(tcs);
|
_waits.Enqueue(tcs);
|
||||||
return tcs.Task;
|
return tcs.Task;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
internal class AuthTimeProvider : IAuthTimeProvider
|
||||||
|
{
|
||||||
|
public DateTime GetTime() => DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -273,6 +273,28 @@ namespace CryptoExchange.Net.Objects
|
|||||||
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
return new WebCallResult(ResponseStatusCode, ResponseHeaders, ResponseTime, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copy the WebCallResult to a new data type
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="K">The new type</typeparam>
|
||||||
|
/// <param name="data">The data of the new type</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
|
{
|
||||||
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, data, Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copy the WebCallResult to a new data type
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="K">The new type</typeparam>
|
||||||
|
/// <param name="error">The error returned</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public WebCallResult<K> AsError<K>(Error error)
|
||||||
|
{
|
||||||
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, 0, null, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Server, default, error);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
@@ -331,6 +353,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? ResponseTime { get; set; }
|
public TimeSpan? ResponseTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source of this result
|
||||||
|
/// </summary>
|
||||||
|
public ResultDataSource DataSource { get; set; } = ResultDataSource.Server;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new result
|
/// Create a new result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -344,6 +371,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="requestBody"></param>
|
/// <param name="requestBody"></param>
|
||||||
/// <param name="requestMethod"></param>
|
/// <param name="requestMethod"></param>
|
||||||
/// <param name="requestHeaders"></param>
|
/// <param name="requestHeaders"></param>
|
||||||
|
/// <param name="dataSource"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <param name="error"></param>
|
/// <param name="error"></param>
|
||||||
public WebCallResult(
|
public WebCallResult(
|
||||||
@@ -357,6 +385,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
string? requestBody,
|
string? requestBody,
|
||||||
HttpMethod? requestMethod,
|
HttpMethod? requestMethod,
|
||||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? requestHeaders,
|
||||||
|
ResultDataSource dataSource,
|
||||||
[AllowNull] T data,
|
[AllowNull] T data,
|
||||||
Error? error) : base(data, originalData, error)
|
Error? error) : base(data, originalData, error)
|
||||||
{
|
{
|
||||||
@@ -370,6 +399,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
RequestBody = requestBody;
|
RequestBody = requestBody;
|
||||||
RequestHeaders = requestHeaders;
|
RequestHeaders = requestHeaders;
|
||||||
RequestMethod = requestMethod;
|
RequestMethod = requestMethod;
|
||||||
|
DataSource = dataSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -393,7 +423,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Create a new error result
|
/// Create a new error result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="error">The error</param>
|
/// <param name="error">The error</param>
|
||||||
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, default, error) { }
|
public WebCallResult(Error? error) : this(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, default, error) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the WebCallResult to a new data type
|
/// Copy the WebCallResult to a new data type
|
||||||
@@ -403,7 +433,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> As<K>([AllowNull] K data)
|
public new WebCallResult<K> As<K>([AllowNull] K data)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, data, Error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -414,7 +444,16 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public new WebCallResult<K> AsError<K>(Error error)
|
public new WebCallResult<K> AsError<K>(Error error)
|
||||||
{
|
{
|
||||||
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, default, error);
|
return new WebCallResult<K>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, DataSource, default, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a copy of this result with data source set to cache
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
internal WebCallResult<T> Cached()
|
||||||
|
{
|
||||||
|
return new WebCallResult<T>(ResponseStatusCode, ResponseHeaders, ResponseTime, ResponseLength, OriginalData, RequestId, RequestUrl, RequestBody, RequestMethod, RequestHeaders, ResultDataSource.Cache, Data, Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@@ -13,5 +13,9 @@
|
|||||||
/// Form content type header
|
/// Form content type header
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string FormContentHeader = "application/x-www-form-urlencoded";
|
public const string FormContentHeader = "application/x-www-form-urlencoded";
|
||||||
|
/// <summary>
|
||||||
|
/// Placeholder key for when request body should be set to the value of this KVP
|
||||||
|
/// </summary>
|
||||||
|
public const string BodyPlaceHolderKey = "_BODY_";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -169,4 +169,38 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Snapshot
|
Snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public enum ReconnectPolicy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect is disabled
|
||||||
|
/// </summary>
|
||||||
|
Disabled,
|
||||||
|
/// <summary>
|
||||||
|
/// Fixed delay of `ReconnectInterval` between retries
|
||||||
|
/// </summary>
|
||||||
|
FixedDelay,
|
||||||
|
/// <summary>
|
||||||
|
/// Backof policy of 2^`reconnectAttempt`, where `reconnectAttempt` has a max value of 5
|
||||||
|
/// </summary>
|
||||||
|
ExponentialBackoff
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The data source of the result
|
||||||
|
/// </summary>
|
||||||
|
public enum ResultDataSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// From server
|
||||||
|
/// </summary>
|
||||||
|
Server,
|
||||||
|
/// <summary>
|
||||||
|
/// From cache
|
||||||
|
/// </summary>
|
||||||
|
Cache
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base for order book options
|
/// Base for order book options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class OrderBookOptions : ExchangeOptions
|
public class OrderBookOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
/// Whether or not checksum validation is enabled. Default is true, disabling will ignore checksum messages.
|
||||||
@@ -19,11 +19,7 @@
|
|||||||
{
|
{
|
||||||
return new T
|
return new T
|
||||||
{
|
{
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
|
||||||
OutputOriginalData = OutputOriginalData,
|
|
||||||
ChecksumValidationEnabled = ChecksumValidationEnabled,
|
ChecksumValidationEnabled = ChecksumValidationEnabled,
|
||||||
Proxy = Proxy,
|
|
||||||
RequestTimeout = RequestTimeout
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
public TimeSpan TimestampRecalculationInterval { get; set; } = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether caching is enabled. Caching will only be applied to GET http requests. The lifetime of cached results can be determined by the `CachingMaxAge` option
|
||||||
|
/// </summary>
|
||||||
|
public bool CachingEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The max age of a cached entry, only used when the `CachingEnabled` options is set to true. When a cached entry is older than the max age it will be discarded and a new server request will be done
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan CachingMaxAge { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a copy of this options
|
/// Create a copy of this options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -34,7 +44,9 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
Proxy = Proxy,
|
Proxy = Proxy,
|
||||||
RequestTimeout = RequestTimeout,
|
RequestTimeout = RequestTimeout,
|
||||||
RateLimiterEnabled = RateLimiterEnabled,
|
RateLimiterEnabled = RateLimiterEnabled,
|
||||||
RateLimitingBehaviour = RateLimitingBehaviour
|
RateLimitingBehaviour = RateLimitingBehaviour,
|
||||||
|
CachingEnabled = CachingEnabled,
|
||||||
|
CachingMaxAge = CachingMaxAge,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects.Options
|
namespace CryptoExchange.Net.Objects.Options
|
||||||
@@ -9,15 +10,15 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
public class SocketExchangeOptions : ExchangeOptions
|
public class SocketExchangeOptions : ExchangeOptions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether or not the socket should automatically reconnect when losing connection
|
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
|
||||||
/// </summary>
|
|
||||||
public bool AutoReconnect { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time to wait between reconnect attempts
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
/// Max number of concurrent resubscription tasks per socket after reconnecting a socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -57,7 +58,7 @@ namespace CryptoExchange.Net.Objects.Options
|
|||||||
{
|
{
|
||||||
ApiCredentials = ApiCredentials?.Copy(),
|
ApiCredentials = ApiCredentials?.Copy(),
|
||||||
OutputOriginalData = OutputOriginalData,
|
OutputOriginalData = OutputOriginalData,
|
||||||
AutoReconnect = AutoReconnect,
|
ReconnectPolicy = ReconnectPolicy,
|
||||||
DelayAfterConnect = DelayAfterConnect,
|
DelayAfterConnect = DelayAfterConnect,
|
||||||
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
MaxConcurrentResubscriptionsPerSocket = MaxConcurrentResubscriptionsPerSocket,
|
||||||
ReconnectInterval = ReconnectInterval,
|
ReconnectInterval = ReconnectInterval,
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Objects
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Order string comparer, sorts by alphabetical order
|
||||||
|
/// </summary>
|
||||||
|
public class OrderedStringComparer : IComparer<string>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Compare function
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="x"></param>
|
||||||
|
/// <param name="y"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public int Compare(string x, string y)
|
||||||
|
{
|
||||||
|
// Shortcuts: If both are null, they are the same.
|
||||||
|
if (x == null && y == null) return 0;
|
||||||
|
|
||||||
|
// If one is null and the other isn't, then the
|
||||||
|
// one that is null is "lesser".
|
||||||
|
if (x == null) return -1;
|
||||||
|
if (y == null) return 1;
|
||||||
|
|
||||||
|
return x.CompareTo(y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Converters.SystemTextJson;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Objects
|
namespace CryptoExchange.Net.Objects
|
||||||
{
|
{
|
||||||
@@ -148,6 +149,27 @@ namespace CryptoExchange.Net.Objects
|
|||||||
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
Add(key, DateTimeConverter.ConvertToSeconds(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a datetime value as string seconds timestamp
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public void AddSecondsString(string key, DateTime value)
|
||||||
|
{
|
||||||
|
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a datetime value as string seconds timestamp. Not added if value is null
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public void AddOptionalSecondsString(string key, DateTime? value)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
Add(key, DateTimeConverter.ConvertToSeconds(value).ToString());
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
/// Add an enum value as the string value as mapped using the <see cref="MapAttribute" />
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -166,7 +188,7 @@ namespace CryptoExchange.Net.Objects
|
|||||||
public void AddEnumAsInt<T>(string key, T value)
|
public void AddEnumAsInt<T>(string key, T value)
|
||||||
{
|
{
|
||||||
var stringVal = EnumConverter.GetString(value);
|
var stringVal = EnumConverter.GetString(value);
|
||||||
Add(key, EnumConverter.GetString(int.Parse(stringVal))!);
|
Add(key, int.Parse(stringVal)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -193,5 +215,18 @@ namespace CryptoExchange.Net.Objects
|
|||||||
Add(key, int.Parse(stringVal));
|
Add(key, int.Parse(stringVal));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the request body. Can be used to specify a simple value or array as the body instead of an object
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="body">Body to set</param>
|
||||||
|
/// <exception cref="InvalidOperationException"></exception>
|
||||||
|
public void SetBody(object body)
|
||||||
|
{
|
||||||
|
if (this.Any())
|
||||||
|
throw new InvalidOperationException("Can't set body when other parameters already specified");
|
||||||
|
|
||||||
|
Add(Constants.BodyPlaceHolderKey, body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,18 +48,22 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// Request weight
|
/// Request weight
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Weight { get; set; } = 1;
|
public int Weight { get; set; } = 1;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rate limit gate to use
|
/// Rate limit gate to use
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IRateLimitGate? RateLimitGate { get; set; }
|
public IRateLimitGate? RateLimitGate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rate limit for this specific endpoint
|
/// Individual endpoint rate limit guard to use
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int? EndpointLimitCount { get; set; }
|
public IRateLimitGuard? LimitGuard { get; set; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Rate limit period for this specific endpoint
|
/// Whether this request should never be cached
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan? EndpointLimitPeriod { get; set; }
|
public bool PreventCaching { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
|
|||||||
@@ -41,13 +41,13 @@ namespace CryptoExchange.Net.Objects
|
|||||||
/// <param name="method">The HttpMethod</param>
|
/// <param name="method">The HttpMethod</param>
|
||||||
/// <param name="path">Endpoint path</param>
|
/// <param name="path">Endpoint path</param>
|
||||||
/// <param name="rateLimitGate">The rate limit gate</param>
|
/// <param name="rateLimitGate">The rate limit gate</param>
|
||||||
/// <param name="endpointLimitCount">The limit count for this specific endpoint</param>
|
/// <param name="limitGuard">The rate limit guard for this specific endpoint</param>
|
||||||
/// <param name="endpointLimitPeriod">The period for the limit for this specific endpoint</param>
|
|
||||||
/// <param name="weight">Request weight</param>
|
/// <param name="weight">Request weight</param>
|
||||||
/// <param name="authenticated">Endpoint is authenticated</param>
|
/// <param name="authenticated">Endpoint is authenticated</param>
|
||||||
/// <param name="requestBodyFormat">Request body format</param>
|
/// <param name="requestBodyFormat">Request body format</param>
|
||||||
/// <param name="parameterPosition">Parameter position</param>
|
/// <param name="parameterPosition">Parameter position</param>
|
||||||
/// <param name="arraySerialization">Array serialization type</param>
|
/// <param name="arraySerialization">Array serialization type</param>
|
||||||
|
/// <param name="preventCaching">Prevent request caching</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public RequestDefinition GetOrCreate(
|
public RequestDefinition GetOrCreate(
|
||||||
HttpMethod method,
|
HttpMethod method,
|
||||||
@@ -55,11 +55,11 @@ namespace CryptoExchange.Net.Objects
|
|||||||
IRateLimitGate? rateLimitGate,
|
IRateLimitGate? rateLimitGate,
|
||||||
int weight,
|
int weight,
|
||||||
bool authenticated,
|
bool authenticated,
|
||||||
int? endpointLimitCount = null,
|
IRateLimitGuard? limitGuard = null,
|
||||||
TimeSpan? endpointLimitPeriod = null,
|
|
||||||
RequestBodyFormat? requestBodyFormat = null,
|
RequestBodyFormat? requestBodyFormat = null,
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
ArrayParametersSerialization? arraySerialization = null)
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
bool? preventCaching = null)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (!_definitions.TryGetValue(method + path, out var def))
|
if (!_definitions.TryGetValue(method + path, out var def))
|
||||||
@@ -67,13 +67,13 @@ namespace CryptoExchange.Net.Objects
|
|||||||
def = new RequestDefinition(path, method)
|
def = new RequestDefinition(path, method)
|
||||||
{
|
{
|
||||||
Authenticated = authenticated,
|
Authenticated = authenticated,
|
||||||
EndpointLimitCount = endpointLimitCount,
|
LimitGuard = limitGuard,
|
||||||
EndpointLimitPeriod = endpointLimitPeriod,
|
|
||||||
RateLimitGate = rateLimitGate,
|
RateLimitGate = rateLimitGate,
|
||||||
Weight = weight,
|
Weight = weight,
|
||||||
ArraySerialization = arraySerialization,
|
ArraySerialization = arraySerialization,
|
||||||
RequestBodyFormat = requestBodyFormat,
|
RequestBodyFormat = requestBodyFormat,
|
||||||
ParameterPosition = parameterPosition,
|
ParameterPosition = parameterPosition,
|
||||||
|
PreventCaching = preventCaching ?? false
|
||||||
};
|
};
|
||||||
_definitions.TryAdd(method + path, def);
|
_definitions.TryAdd(method + path, def);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,14 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
public DateTime Timestamp { get; set; }
|
public DateTime Timestamp { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The topic of the update, what symbol/asset etc..
|
/// The stream producing the update
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Topic { get; set; }
|
public string? StreamId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The symbol the update is for
|
||||||
|
/// </summary>
|
||||||
|
public string? Symbol { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
|
/// The original data that was received, only available when OutputOriginalData is set to true in the client options
|
||||||
@@ -33,10 +38,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public T Data { get; set; }
|
public T Data { get; set; }
|
||||||
|
|
||||||
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
|
internal DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
|
||||||
{
|
{
|
||||||
Data = data;
|
Data = data;
|
||||||
Topic = topic;
|
StreamId = streamId;
|
||||||
|
Symbol = symbol;
|
||||||
OriginalData = originalData;
|
OriginalData = originalData;
|
||||||
Timestamp = timestamp;
|
Timestamp = timestamp;
|
||||||
UpdateType = updateType;
|
UpdateType = updateType;
|
||||||
@@ -50,7 +56,7 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DataEvent<K> As<K>(K data)
|
public DataEvent<K> As<K>(K data)
|
||||||
{
|
{
|
||||||
return new DataEvent<K>(data, Topic, OriginalData, Timestamp, UpdateType);
|
return new DataEvent<K>(data, StreamId, Symbol, OriginalData, Timestamp, UpdateType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -58,11 +64,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="K">The type of the new data</typeparam>
|
/// <typeparam name="K">The type of the new data</typeparam>
|
||||||
/// <param name="data">The new data</param>
|
/// <param name="data">The new data</param>
|
||||||
/// <param name="topic">The new topic</param>
|
/// <param name="symbol">The new symbol</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DataEvent<K> As<K>(K data, string? topic)
|
public DataEvent<K> As<K>(K data, string? symbol)
|
||||||
{
|
{
|
||||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp, UpdateType);
|
return new DataEvent<K>(data, StreamId, symbol, OriginalData, Timestamp, UpdateType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,12 +76,79 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="K">The type of the new data</typeparam>
|
/// <typeparam name="K">The type of the new data</typeparam>
|
||||||
/// <param name="data">The new data</param>
|
/// <param name="data">The new data</param>
|
||||||
/// <param name="topic">The new topic</param>
|
/// <param name="streamId">The new stream id</param>
|
||||||
|
/// <param name="symbol">The new symbol</param>
|
||||||
/// <param name="updateType">The type of update</param>
|
/// <param name="updateType">The type of update</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public DataEvent<K> As<K>(K data, string? topic, SocketUpdateType updateType)
|
public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType)
|
||||||
{
|
{
|
||||||
return new DataEvent<K>(data, topic, OriginalData, Timestamp, updateType);
|
return new DataEvent<K>(data, streamId, symbol, OriginalData, Timestamp, updateType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataEvent<T> WithSymbol(string symbol)
|
||||||
|
{
|
||||||
|
Symbol = symbol;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the update type
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataEvent<T> WithUpdateType(SocketUpdateType type)
|
||||||
|
{
|
||||||
|
UpdateType = type;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify the stream id
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="streamId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public DataEvent<T> WithStreamId(string streamId)
|
||||||
|
{
|
||||||
|
StreamId = streamId;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a CallResult from this DataEvent
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public CallResult<T> ToCallResult()
|
||||||
|
{
|
||||||
|
return new CallResult<T>(Data, OriginalData, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a CallResult from this DataEvent
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public CallResult<K> ToCallResult<K>(K data)
|
||||||
|
{
|
||||||
|
return new CallResult<K>(data, OriginalData, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a CallResult from this DataEvent
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public CallResult<K> ToCallResult<K>(Error error)
|
||||||
|
{
|
||||||
|
return new CallResult<K>(default, OriginalData, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"{StreamId} - {(Symbol == null ? "" : (Symbol + " - "))}{(UpdateType == null ? "" : (UpdateType + " - "))}{Data}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,20 +26,20 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
public IDictionary<string, string> Cookies { get; set; } = new Dictionary<string, string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time to wait between reconnect attempts
|
/// The fixed time to wait between reconnect attempts, only used when `ReconnectPolicy` is set to `ReconnectPolicy.ExponentialBackoff`
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
public TimeSpan ReconnectInterval { get; set; } = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect policy
|
||||||
|
/// </summary>
|
||||||
|
public ReconnectPolicy ReconnectPolicy { get; set; } = ReconnectPolicy.FixedDelay;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Proxy for the connection
|
/// Proxy for the connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiProxy? Proxy { get; set; }
|
public ApiProxy? Proxy { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the socket should automatically reconnect when connection is lost
|
|
||||||
/// </summary>
|
|
||||||
public bool AutoReconnect { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
/// The maximum time of no data received before considering the connection lost and closting/reconnecting the socket
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,11 +68,11 @@ namespace CryptoExchange.Net.Objects.Sockets
|
|||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri">Uri</param>
|
/// <param name="uri">Uri</param>
|
||||||
/// <param name="autoReconnect">Auto reconnect</param>
|
/// <param name="policy">Reconnect policy</param>
|
||||||
public WebSocketParameters(Uri uri, bool autoReconnect)
|
public WebSocketParameters(Uri uri, ReconnectPolicy policy)
|
||||||
{
|
{
|
||||||
Uri = uri;
|
Uri = uri;
|
||||||
AutoReconnect = autoReconnect;
|
ReconnectPolicy = policy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.OrderBook
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public class OrderBookFactory<TOptions> : IOrderBookFactory<TOptions> where TOptions: OrderBookOptions
|
||||||
|
{
|
||||||
|
private readonly Func<string, Action<TOptions>?, ISymbolOrderBook> _symbolCtor;
|
||||||
|
private readonly Func<string, string, Action<TOptions>?, ISymbolOrderBook> _assetsCtor;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbolCtor"></param>
|
||||||
|
/// <param name="assetsCtor"></param>
|
||||||
|
public OrderBookFactory(Func<string, Action<TOptions>?, ISymbolOrderBook> symbolCtor, Func<string, string, Action<TOptions>?, ISymbolOrderBook> assetsCtor)
|
||||||
|
{
|
||||||
|
_symbolCtor = symbolCtor;
|
||||||
|
_assetsCtor = assetsCtor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ISymbolOrderBook Create(string symbol, Action<TOptions>? options = null) => _symbolCtor(symbol, options);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ISymbolOrderBook Create(string baseAsset, string quoteAsset, Action<TOptions>? options = null) => _assetsCtor(baseAsset, quoteAsset, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -810,7 +810,7 @@ namespace CryptoExchange.Net.OrderBook
|
|||||||
{
|
{
|
||||||
if (lastUpdateId <= LastSequenceNumber)
|
if (lastUpdateId <= LastSequenceNumber)
|
||||||
{
|
{
|
||||||
_logger.OrderBookUpdateSkipped(Api, Symbol, firstUpdateId, lastUpdateId);
|
_logger.OrderBookUpdateSkipped(Api, Symbol, lastUpdateId, LastSequenceNumber);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||||
=> definition.Authenticated == _authenticated;
|
=> definition.Authenticated == _authenticated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||||
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
|
=> string.Equals(definition.Path, _path, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||||
=> _paths.Contains(definition.Path);
|
=> _paths.Contains(definition.Path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||||
=> host == _host;
|
=> host == _host;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||||
=> type == _type;
|
=> type == _type;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ namespace CryptoExchange.Net.RateLimiting.Filters
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey)
|
public bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey)
|
||||||
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
=> definition.Path.StartsWith(_path, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,26 +14,26 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply guard per host
|
/// Apply guard per host
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Func<RequestDefinition, string, SecureString?, string> PerHost { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => host);
|
public static Func<RequestDefinition, string, string?, string> PerHost { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => host);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply guard per endpoint
|
/// Apply guard per endpoint
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Func<RequestDefinition, string, SecureString?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => def.Path + def.Method);
|
public static Func<RequestDefinition, string, string?, string> PerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply guard per API key
|
/// Apply guard per API key
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Func<RequestDefinition, string, SecureString?, string> PerApiKey { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString());
|
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key!);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply guard per API key per endpoint
|
/// Apply guard per API key per endpoint
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static Func<RequestDefinition, string, SecureString?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, SecureString?, string>((def, host, key) => key!.GetString() + def.Path + def.Method);
|
public static Func<RequestDefinition, string, string?, string> PerApiKeyPerEndpoint { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => key! + def.Path + def.Method);
|
||||||
|
|
||||||
private readonly IEnumerable<IGuardFilter> _filters;
|
private readonly IEnumerable<IGuardFilter> _filters;
|
||||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||||
private RateLimitWindowType _windowType;
|
private RateLimitWindowType _windowType;
|
||||||
private double? _decayRate;
|
private double? _decayRate;
|
||||||
private int? _connectionWeight;
|
private int? _connectionWeight;
|
||||||
private readonly Func<RequestDefinition, string, SecureString?, string> _keySelector;
|
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "RateLimitGuard";
|
public string Name => "RateLimitGuard";
|
||||||
@@ -60,7 +60,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// <param name="windowType">Type of rate limit window</param>
|
/// <param name="windowType">Type of rate limit window</param>
|
||||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||||
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IGuardFilter filter, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||||
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
|
: this(keySelector, new[] { filter }, limit, timeSpan, windowType, decayPerTimeSpan, connectionWeight)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// <param name="windowType">Type of rate limit window</param>
|
/// <param name="windowType">Type of rate limit window</param>
|
||||||
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
/// <param name="decayPerTimeSpan">The decay per timespan if windowType is DecayWindowTracker</param>
|
||||||
/// <param name="connectionWeight">The weight of a new connection</param>
|
/// <param name="connectionWeight">The weight of a new connection</param>
|
||||||
public RateLimitGuard(Func<RequestDefinition, string, SecureString?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
public RateLimitGuard(Func<RequestDefinition, string, string?, string> keySelector, IEnumerable<IGuardFilter> filters, int limit, TimeSpan timeSpan, RateLimitWindowType windowType, double? decayPerTimeSpan = null, int? connectionWeight = null)
|
||||||
{
|
{
|
||||||
_filters = filters;
|
_filters = filters;
|
||||||
_trackers = new Dictionary<string, IWindowTracker>();
|
_trackers = new Dictionary<string, IWindowTracker>();
|
||||||
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
foreach(var filter in _filters)
|
foreach(var filter in _filters)
|
||||||
{
|
{
|
||||||
@@ -114,7 +114,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
foreach (var filter in _filters)
|
foreach (var filter in _filters)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
var dif = (After + _windowBuffer) - DateTime.UtcNow;
|
var dif = (After + _windowBuffer) - DateTime.UtcNow;
|
||||||
if (dif <= TimeSpan.Zero)
|
if (dif <= TimeSpan.Zero)
|
||||||
@@ -48,7 +48,7 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
return RateLimitState.NotApplied;
|
return RateLimitState.NotApplied;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,22 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class SingleLimitGuard : IRateLimitGuard
|
public class SingleLimitGuard : IRateLimitGuard
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default endpoint limit
|
||||||
|
/// </summary>
|
||||||
|
public static Func<RequestDefinition, string, string?, string> Default { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Endpoint limit per API key
|
||||||
|
/// </summary>
|
||||||
|
public static Func<RequestDefinition, string, string?, string> PerApiKey { get; } = new Func<RequestDefinition, string, string?, string>((def, host, key) => def.Path + def.Method);
|
||||||
|
|
||||||
private readonly Dictionary<string, IWindowTracker> _trackers;
|
private readonly Dictionary<string, IWindowTracker> _trackers;
|
||||||
private readonly RateLimitWindowType _windowType;
|
private readonly RateLimitWindowType _windowType;
|
||||||
private readonly double? _decayRate;
|
private readonly double? _decayRate;
|
||||||
|
private readonly int _limit;
|
||||||
|
private readonly TimeSpan _period;
|
||||||
|
private readonly Func<RequestDefinition, string, string?, string> _keySelector;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public string Name => "EndpointLimitGuard";
|
public string Name => "EndpointLimitGuard";
|
||||||
@@ -25,20 +38,28 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SingleLimitGuard(RateLimitWindowType windowType, double? decayRate = null)
|
public SingleLimitGuard(
|
||||||
|
int limit,
|
||||||
|
TimeSpan period,
|
||||||
|
RateLimitWindowType windowType,
|
||||||
|
double? decayRate = null,
|
||||||
|
Func<RequestDefinition, string, string?, string>? keySelector = null)
|
||||||
{
|
{
|
||||||
|
_limit = limit;
|
||||||
|
_period = period;
|
||||||
_windowType = windowType;
|
_windowType = windowType;
|
||||||
_decayRate = decayRate;
|
_decayRate = decayRate;
|
||||||
|
_keySelector = keySelector ?? Default;
|
||||||
_trackers = new Dictionary<string, IWindowTracker>();
|
_trackers = new Dictionary<string, IWindowTracker>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
public LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
var key = definition.Path + definition.Method;
|
var key = _keySelector(definition, host, apiKey);
|
||||||
if (!_trackers.TryGetValue(key, out var tracker))
|
if (!_trackers.TryGetValue(key, out var tracker))
|
||||||
{
|
{
|
||||||
tracker = CreateTracker(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value);
|
tracker = CreateTracker();
|
||||||
_trackers.Add(key, tracker);
|
_trackers.Add(key, tracker);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,27 +67,27 @@ namespace CryptoExchange.Net.RateLimiting.Guards
|
|||||||
if (delay == default)
|
if (delay == default)
|
||||||
return LimitCheck.NotNeeded;
|
return LimitCheck.NotNeeded;
|
||||||
|
|
||||||
return LimitCheck.Needed(delay, definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
return LimitCheck.Needed(delay, _limit, _period, tracker.Current);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight)
|
public RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight)
|
||||||
{
|
{
|
||||||
var key = definition.Path + definition.Method;
|
var key = _keySelector(definition, host, apiKey);
|
||||||
var tracker = _trackers[key];
|
var tracker = _trackers[key];
|
||||||
tracker.ApplyWeight(requestWeight);
|
tracker.ApplyWeight(requestWeight);
|
||||||
return RateLimitState.Applied(definition.EndpointLimitCount!.Value, definition.EndpointLimitPeriod!.Value, tracker.Current);
|
return RateLimitState.Applied(_limit, _period, tracker.Current);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new WindowTracker
|
/// Create a new WindowTracker
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected IWindowTracker CreateTracker(int limit, TimeSpan timeSpan)
|
protected IWindowTracker CreateTracker()
|
||||||
{
|
{
|
||||||
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(limit, timeSpan)
|
return _windowType == RateLimitWindowType.Sliding ? new SlidingWindowTracker(_limit, _period)
|
||||||
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(limit, timeSpan) :
|
: _windowType == RateLimitWindowType.Fixed ? new FixedWindowTracker(_limit, _period) :
|
||||||
new DecayWindowTracker(limit, timeSpan, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
new DecayWindowTracker(_limit, _period, _decayRate ?? throw new InvalidOperationException("Decay rate not provided"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
|||||||
/// <param name="host">The host address</param>
|
/// <param name="host">The host address</param>
|
||||||
/// <param name="apiKey">The API key</param>
|
/// <param name="apiKey">The API key</param>
|
||||||
/// <returns>True if passed</returns>
|
/// <returns>True if passed</returns>
|
||||||
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey);
|
bool Passes(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,13 +32,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
|||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
Task SetRetryAfterGuardAsync(DateTime retryAfter);
|
Task SetRetryAfterGuardAsync(DateTime retryAfter);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the SingleLimitGuard for handling individual endpoint rate limits
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="guard"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the 'retry after' timestamp if set
|
/// Returns the 'retry after' timestamp if set
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -58,21 +51,21 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
|||||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||||
/// <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>
|
||||||
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
/// Enforces the rate limit as defined in the request definition. When a rate limit is hit will wait for the rate limit to pass if RateLimitingBehaviour is Wait, or return an error if it is set to Fail
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
/// <param name="logger">Logger</param>
|
||||||
/// <param name="itemId">Id of the item to check</param>
|
/// <param name="itemId">Id of the item to check</param>
|
||||||
|
/// <param name="guard">The guard</param>
|
||||||
/// <param name="type">The rate limit item type</param>
|
/// <param name="type">The rate limit item type</param>
|
||||||
/// <param name="definition">The request definition</param>
|
/// <param name="definition">The request definition</param>
|
||||||
/// <param name="baseAddress">The host address</param>
|
/// <param name="baseAddress">The host address</param>
|
||||||
/// <param name="apiKey">The API key</param>
|
/// <param name="apiKey">The API key</param>
|
||||||
/// <param name="requestWeight">Request weight</param>
|
|
||||||
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
/// <param name="behaviour">Behaviour when rate limit is hit</param>
|
||||||
/// <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>
|
||||||
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string baseAddress, SecureString? apiKey, int requestWeight, RateLimitingBehaviour behaviour, CancellationToken ct);
|
Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, IRateLimitGuard guard, RateLimitItemType type, RequestDefinition definition, string baseAddress, string? apiKey, RateLimitingBehaviour behaviour, CancellationToken ct);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
|||||||
/// <param name="apiKey">The API key</param>
|
/// <param name="apiKey">The API key</param>
|
||||||
/// <param name="requestWeight">The request weight</param>
|
/// <param name="requestWeight">The request weight</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
|
LimitCheck Check(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply the request to this guard with the specified weight
|
/// Apply the request to this guard with the specified weight
|
||||||
@@ -39,6 +39,6 @@ namespace CryptoExchange.Net.RateLimiting.Interfaces
|
|||||||
/// <param name="apiKey">The API key</param>
|
/// <param name="apiKey">The API key</param>
|
||||||
/// <param name="requestWeight">The request weight</param>
|
/// <param name="requestWeight">The request weight</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight);
|
RateLimitState ApplyWeight(RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public class RateLimitGate : IRateLimitGate
|
public class RateLimitGate : IRateLimitGate
|
||||||
{
|
{
|
||||||
private IRateLimitGuard _singleLimitGuard = new SingleLimitGuard(RateLimitWindowType.Sliding);
|
|
||||||
private readonly ConcurrentBag<IRateLimitGuard> _guards;
|
private readonly ConcurrentBag<IRateLimitGuard> _guards;
|
||||||
private readonly SemaphoreSlim _semaphore;
|
private readonly SemaphoreSlim _semaphore;
|
||||||
private readonly string _name;
|
private readonly string _name;
|
||||||
@@ -37,7 +36,7 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
public async Task<CallResult> ProcessAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||||
{
|
{
|
||||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
_waitingCount++;
|
_waitingCount++;
|
||||||
@@ -53,16 +52,23 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<CallResult> ProcessSingleAsync(ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
public async Task<CallResult> ProcessSingleAsync(
|
||||||
|
ILogger logger,
|
||||||
|
int itemId,
|
||||||
|
IRateLimitGuard guard,
|
||||||
|
RateLimitItemType type,
|
||||||
|
RequestDefinition definition,
|
||||||
|
string host,
|
||||||
|
string? apiKey,
|
||||||
|
RateLimitingBehaviour rateLimitingBehaviour,
|
||||||
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
if (requestWeight == 0)
|
|
||||||
requestWeight = 1;
|
|
||||||
|
|
||||||
_waitingCount++;
|
_waitingCount++;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await CheckGuardsAsync(new IRateLimitGuard[] { _singleLimitGuard }, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
return await CheckGuardsAsync(new IRateLimitGuard[] { guard }, logger, itemId, type, definition, host, apiKey, 1, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -71,7 +77,7 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, SecureString? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
private async Task<CallResult> CheckGuardsAsync(IEnumerable<IRateLimitGuard> guards, ILogger logger, int itemId, RateLimitItemType type, RequestDefinition definition, string host, string? apiKey, int requestWeight, RateLimitingBehaviour rateLimitingBehaviour, CancellationToken ct)
|
||||||
{
|
{
|
||||||
foreach (var guard in guards)
|
foreach (var guard in guards)
|
||||||
{
|
{
|
||||||
@@ -101,7 +107,7 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
logger.RateLimitDelayingRequest(itemId, definition.Path, result.Delay, guard.Name, description);
|
||||||
|
|
||||||
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
RateLimitTriggered?.Invoke(new RateLimitEvent(_name, guard.Description, definition, host, result.Current, requestWeight, result.Limit, result.Period, result.Delay, rateLimitingBehaviour));
|
||||||
await Task.Delay(result.Delay, ct).ConfigureAwait(false);
|
await Task.Delay((int)result.Delay.TotalMilliseconds + 1, ct).ConfigureAwait(false);
|
||||||
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
await _semaphore.WaitAsync(ct).ConfigureAwait(false);
|
||||||
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
return await CheckGuardsAsync(guards, logger, itemId, type, definition, host, apiKey, requestWeight, rateLimitingBehaviour, ct).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -130,13 +136,6 @@ namespace CryptoExchange.Net.RateLimiting
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IRateLimitGate SetSingleLimitGuard(SingleLimitGuard guard)
|
|
||||||
{
|
|
||||||
_singleLimitGuard = guard;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
|
public async Task SetRetryAfterGuardAsync(DateTime retryAfter)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -80,7 +80,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private TimeSpan DetermineWaitTime(int requestWeight)
|
private TimeSpan DetermineWaitTime(int requestWeight)
|
||||||
{
|
{
|
||||||
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
var weightToRemove = Math.Max(Current - (Limit - requestWeight), 0);
|
||||||
return TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
var result = TimeSpan.FromMilliseconds(Math.Ceiling(weightToRemove / DecreaseRate) * TimePeriod.TotalMilliseconds);
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private TimeSpan DetermineWaitTime()
|
private TimeSpan DetermineWaitTime()
|
||||||
{
|
{
|
||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
return (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
var result = (_nextReset!.Value - checkTime) + _fixedWindowBuffer;
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
var startCurrentWindow = checkTime.AddTicks(-(checkTime.Ticks % TimePeriod.Ticks));
|
||||||
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
var wait = startCurrentWindow.Add(TimePeriod) - checkTime;
|
||||||
return wait.Add(_fixedWindowBuffer);
|
var result = wait.Add(_fixedWindowBuffer);
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
private readonly List<LimitEntry> _entries;
|
private readonly List<LimitEntry> _entries;
|
||||||
private int _currentWeight = 0;
|
private int _currentWeight = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Additional wait time to apply to account for fluctuating request times
|
||||||
|
/// </summary>
|
||||||
|
private static readonly TimeSpan _slidingWindowBuffer = TimeSpan.FromMilliseconds(1000);
|
||||||
|
|
||||||
public SlidingWindowTracker(int limit, TimeSpan period)
|
public SlidingWindowTracker(int limit, TimeSpan period)
|
||||||
{
|
{
|
||||||
Limit = limit;
|
Limit = limit;
|
||||||
@@ -89,7 +94,10 @@ namespace CryptoExchange.Net.RateLimiting.Trackers
|
|||||||
removedWeight += entry.Weight;
|
removedWeight += entry.Weight;
|
||||||
if (removedWeight >= weightToRemove)
|
if (removedWeight >= weightToRemove)
|
||||||
{
|
{
|
||||||
return entry.Timestamp + TimePeriod - DateTime.UtcNow;
|
var result = entry.Timestamp + TimePeriod + _slidingWindowBuffer - DateTime.UtcNow;
|
||||||
|
if (result < TimeSpan.Zero)
|
||||||
|
return TimeSpan.Zero;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Requests
|
|||||||
if (client == null)
|
if (client == null)
|
||||||
{
|
{
|
||||||
var handler = new HttpClientHandler();
|
var handler = new HttpClientHandler();
|
||||||
|
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
|
||||||
if (proxy != null)
|
if (proxy != null)
|
||||||
{
|
{
|
||||||
handler.Proxy = new WebProxy
|
handler.Proxy = new WebProxy
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
private ProcessState _processState;
|
private ProcessState _processState;
|
||||||
private DateTime _lastReconnectTime;
|
private DateTime _lastReconnectTime;
|
||||||
private string _baseAddress;
|
private string _baseAddress;
|
||||||
|
private int _reconnectAttempt;
|
||||||
|
|
||||||
private const int _receiveBufferSize = 1048576;
|
private const int _receiveBufferSize = 1048576;
|
||||||
private const int _sendBufferSize = 4096;
|
private const int _sendBufferSize = 4096;
|
||||||
@@ -107,7 +108,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public event Func<Task>? OnClose;
|
public event Func<Task>? OnClose;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<WebSocketMessageType, ReadOnlyMemory<byte>>? OnStreamMessage;
|
public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task>? OnStreamMessage;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Func<int, Task>? OnRequestSent;
|
public event Func<int, Task>? OnRequestSent;
|
||||||
@@ -232,36 +233,37 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
while (!_stopRequested)
|
while (!_stopRequested)
|
||||||
{
|
{
|
||||||
_logger.SocketStartingProcessing(Id);
|
_logger.SocketStartingProcessing(Id);
|
||||||
_processState = ProcessState.Processing;
|
SetProcessState(ProcessState.Processing);
|
||||||
var sendTask = SendLoopAsync();
|
var sendTask = SendLoopAsync();
|
||||||
var receiveTask = ReceiveLoopAsync();
|
var receiveTask = ReceiveLoopAsync();
|
||||||
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
var timeoutTask = Parameters.Timeout != null && Parameters.Timeout > TimeSpan.FromSeconds(0) ? CheckTimeoutAsync() : Task.CompletedTask;
|
||||||
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
await Task.WhenAll(sendTask, receiveTask, timeoutTask).ConfigureAwait(false);
|
||||||
_logger.SocketFinishedProcessing(Id);
|
_logger.SocketFinishedProcessing(Id);
|
||||||
|
|
||||||
_processState = ProcessState.WaitingForClose;
|
SetProcessState(ProcessState.WaitingForClose);
|
||||||
while (_closeTask == null)
|
while (_closeTask == null)
|
||||||
await Task.Delay(50).ConfigureAwait(false);
|
await Task.Delay(50).ConfigureAwait(false);
|
||||||
|
|
||||||
await _closeTask.ConfigureAwait(false);
|
await _closeTask.ConfigureAwait(false);
|
||||||
_closeTask = null;
|
if (!_stopRequested)
|
||||||
|
_closeTask = null;
|
||||||
|
|
||||||
if (!Parameters.AutoReconnect)
|
if (Parameters.ReconnectPolicy == ReconnectPolicy.Disabled)
|
||||||
{
|
{
|
||||||
_processState = ProcessState.Idle;
|
SetProcessState(ProcessState.Idle);
|
||||||
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnClose?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_stopRequested)
|
if (!_stopRequested)
|
||||||
{
|
{
|
||||||
_processState = ProcessState.Reconnecting;
|
SetProcessState(ProcessState.Reconnecting);
|
||||||
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnReconnecting?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
// Delay here to prevent very repid looping when a connection to the server is accepted and immediately disconnected
|
||||||
if (sinceLastReconnect < Parameters.ReconnectInterval)
|
var initialDelay = GetReconnectDelay();
|
||||||
await Task.Delay(Parameters.ReconnectInterval - sinceLastReconnect).ConfigureAwait(false);
|
await Task.Delay(initialDelay).ConfigureAwait(false);
|
||||||
|
|
||||||
while (!_stopRequested)
|
while (!_stopRequested)
|
||||||
{
|
{
|
||||||
@@ -282,32 +284,58 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_ctsSource = new CancellationTokenSource();
|
_ctsSource = new CancellationTokenSource();
|
||||||
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
while (_sendBuffer.TryDequeue(out _)) { } // Clear send buffer
|
||||||
|
|
||||||
|
_reconnectAttempt++;
|
||||||
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
var connected = await ConnectInternalAsync().ConfigureAwait(false);
|
||||||
if (!connected)
|
if (!connected)
|
||||||
{
|
{
|
||||||
await Task.Delay(Parameters.ReconnectInterval).ConfigureAwait(false);
|
// Delay between reconnect attempts
|
||||||
|
var delay = GetReconnectDelay();
|
||||||
|
await Task.Delay(delay).ConfigureAwait(false);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_reconnectAttempt = 0;
|
||||||
_lastReconnectTime = DateTime.UtcNow;
|
_lastReconnectTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Set to processing before reconnect handling
|
||||||
|
SetProcessState(ProcessState.Processing);
|
||||||
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnReconnected?.Invoke() ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_processState = ProcessState.Idle;
|
SetProcessState(ProcessState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TimeSpan GetReconnectDelay()
|
||||||
|
{
|
||||||
|
if (_reconnectAttempt == 0)
|
||||||
|
{
|
||||||
|
// Means this is directly after disconnecting. Only delay if the last reconnect time is very recent
|
||||||
|
var sinceLastReconnect = DateTime.UtcNow - _lastReconnectTime;
|
||||||
|
if (sinceLastReconnect < TimeSpan.FromSeconds(5))
|
||||||
|
return TimeSpan.FromSeconds(5) - sinceLastReconnect;
|
||||||
|
|
||||||
|
return TimeSpan.FromMilliseconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var delay = Parameters.ReconnectPolicy == ReconnectPolicy.FixedDelay ? Parameters.ReconnectInterval : TimeSpan.FromSeconds(Math.Pow(2, Math.Min(5, _reconnectAttempt)));
|
||||||
|
if (delay > TimeSpan.Zero)
|
||||||
|
return delay;
|
||||||
|
return TimeSpan.FromMilliseconds(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void Send(int id, string data, int weight)
|
public virtual bool Send(int id, string data, int weight)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested || _processState != ProcessState.Processing)
|
||||||
return;
|
return false;
|
||||||
|
|
||||||
var bytes = Parameters.Encoding.GetBytes(data);
|
var bytes = Parameters.Encoding.GetBytes(data);
|
||||||
_logger.SocketAddingBytesToSendBuffer(Id, id, bytes);
|
_logger.SocketAddingBytesToSendBuffer(Id, id, bytes);
|
||||||
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
_sendBuffer.Enqueue(new SendItem { Id = id, Weight = weight, Bytes = bytes });
|
||||||
_sendEvent.Set();
|
_sendEvent.Set();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -366,36 +394,33 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//_closeState = CloseState.Closing;
|
try
|
||||||
_ctsSource.Cancel();
|
|
||||||
_sendEvent.Set();
|
|
||||||
|
|
||||||
if (_socket.State == WebSocketState.Open)
|
|
||||||
{
|
{
|
||||||
try
|
if (_socket.State == WebSocketState.CloseReceived)
|
||||||
{
|
|
||||||
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// Can sometimes throw an exception when socket is in aborted state due to timing
|
|
||||||
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
|
||||||
// So socket might go to aborted state, might still be open
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if(_socket.State == WebSocketState.CloseReceived)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
await _socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
else if (_socket.State == WebSocketState.Open)
|
||||||
{
|
{
|
||||||
// Can sometimes throw an exception when socket is in aborted state due to timing
|
await _socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Closing", default).ConfigureAwait(false);
|
||||||
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
var startWait = DateTime.UtcNow;
|
||||||
// So socket might go to aborted state, might still be open
|
while (_socket.State != WebSocketState.Closed && _socket.State != WebSocketState.Aborted)
|
||||||
|
{
|
||||||
|
// Wait until we receive close confirmation
|
||||||
|
await Task.Delay(10).ConfigureAwait(false);
|
||||||
|
if (DateTime.UtcNow - startWait > TimeSpan.FromSeconds(5))
|
||||||
|
break; // Wait for max 5 seconds, then just abort the connection
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Can sometimes throw an exception when socket is in aborted state due to timing
|
||||||
|
// Websocket is set to Aborted state when the cancelation token is set during SendAsync/ReceiveAsync
|
||||||
|
// So socket might go to aborted state, might still be open
|
||||||
|
}
|
||||||
|
|
||||||
|
_ctsSource.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -413,6 +438,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_disposed = true;
|
_disposed = true;
|
||||||
_socket.Dispose();
|
_socket.Dispose();
|
||||||
_ctsSource?.Dispose();
|
_ctsSource?.Dispose();
|
||||||
|
_sendEvent.Dispose();
|
||||||
_logger.SocketDisposed(Id);
|
_logger.SocketDisposed(Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,10 +453,15 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
if (_ctsSource.IsCancellationRequested)
|
try
|
||||||
|
{
|
||||||
|
if (!_sendBuffer.Any())
|
||||||
|
await _sendEvent.WaitAsync(ct: _ctsSource.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
await _sendEvent.WaitAsync().ConfigureAwait(false);
|
|
||||||
|
|
||||||
if (_ctsSource.IsCancellationRequested)
|
if (_ctsSource.IsCancellationRequested)
|
||||||
break;
|
break;
|
||||||
@@ -439,11 +470,19 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
if (Parameters.RateLimiter != null)
|
if (Parameters.RateLimiter != null)
|
||||||
{
|
{
|
||||||
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
try
|
||||||
if (!limitResult)
|
|
||||||
{
|
{
|
||||||
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
var limitResult = await Parameters.RateLimiter.ProcessAsync(_logger, data.Id, RateLimitItemType.Request, requestDefinition, _baseAddress, null, data.Weight, Parameters.RateLimitingBehaviour, _ctsSource.Token).ConfigureAwait(false);
|
||||||
continue;
|
if (!limitResult)
|
||||||
|
{
|
||||||
|
await (OnRequestRateLimited?.Invoke(data.Id) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// canceled
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +515,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
_logger.SocketSendLoopStoppedWithException(Id, e.Message, e);
|
_logger.SocketSendLoopStoppedWithException(Id, e.Message, e);
|
||||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
throw;
|
if (_closeTask?.IsCompleted != false)
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -527,10 +567,20 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
if (receiveResult.MessageType == WebSocketMessageType.Close)
|
||||||
{
|
{
|
||||||
// Connection closed unexpectedly
|
// Connection closed
|
||||||
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
|
if (_socket.State == WebSocketState.CloseReceived)
|
||||||
if (_closeTask?.IsCompleted != false)
|
{
|
||||||
_closeTask = CloseInternalAsync();
|
// Close received means it server initiated, we should send a confirmation and close the socket
|
||||||
|
_logger.SocketReceivedCloseMessage(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
|
||||||
|
if (_closeTask?.IsCompleted != false)
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Means the socket is now closed and we were the one initiating it
|
||||||
|
_logger.SocketReceivedCloseConfirmation(Id, receiveResult.CloseStatus.ToString(), receiveResult.CloseStatusDescription);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -551,7 +601,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
// Received a complete message and it's not multi part
|
// Received a complete message and it's not multi part
|
||||||
_logger.SocketReceivedSingleMessage(Id, receiveResult.Count);
|
_logger.SocketReceivedSingleMessage(Id, receiveResult.Count);
|
||||||
ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(buffer.Array, buffer.Offset, receiveResult.Count));
|
await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(buffer.Array, buffer.Offset, receiveResult.Count)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -586,7 +636,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
_logger.SocketReassembledMessage(Id, multipartStream!.Length);
|
_logger.SocketReassembledMessage(Id, multipartStream!.Length);
|
||||||
// Get the underlying buffer of the memorystream holding the written data and delimit it (GetBuffer return the full array, not only the written part)
|
// Get the underlying buffer of the memorystream holding the written data and delimit it (GetBuffer return the full array, not only the written part)
|
||||||
ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(multipartStream.GetBuffer(), 0, (int)multipartStream.Length));
|
await ProcessData(receiveResult.MessageType, new ReadOnlyMemory<byte>(multipartStream.GetBuffer(), 0, (int)multipartStream.Length)).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -602,7 +652,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
_logger.SocketReceiveLoopStoppedWithException(Id, e);
|
_logger.SocketReceiveLoopStoppedWithException(Id, e);
|
||||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
throw;
|
if (_closeTask?.IsCompleted != false)
|
||||||
|
_closeTask = CloseInternalAsync();
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -616,10 +667,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="type"></param>
|
/// <param name="type"></param>
|
||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected void ProcessData(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
protected async Task ProcessData(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
LastActionTime = DateTime.UtcNow;
|
LastActionTime = DateTime.UtcNow;
|
||||||
OnStreamMessage?.Invoke(type, data);
|
await (OnStreamMessage?.Invoke(type, data) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -660,7 +711,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
|
// any exception here will stop the timeout checking, but do so silently unless the socket get's stopped.
|
||||||
// Make sure we at least let the owner know there was an error
|
// Make sure we at least let the owner know there was an error
|
||||||
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
await (OnError?.Invoke(e) ?? Task.CompletedTask).ConfigureAwait(false);
|
||||||
throw;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,10 +735,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
var checkTime = DateTime.UtcNow;
|
var checkTime = DateTime.UtcNow;
|
||||||
if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1))
|
if (checkTime - _lastReceivedMessagesUpdate > TimeSpan.FromSeconds(1))
|
||||||
{
|
{
|
||||||
foreach (var msg in _receivedMessages.ToList()) // To list here because we're removing from the list
|
for (var i = 0; i < _receivedMessages.Count; i++)
|
||||||
{
|
{
|
||||||
|
var msg = _receivedMessages[i];
|
||||||
if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3))
|
if (checkTime - msg.Timestamp > TimeSpan.FromSeconds(3))
|
||||||
|
{
|
||||||
_receivedMessages.Remove(msg);
|
_receivedMessages.Remove(msg);
|
||||||
|
i--;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_lastReceivedMessagesUpdate = checkTime;
|
_lastReceivedMessagesUpdate = checkTime;
|
||||||
@@ -716,6 +770,15 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (proxy.Login != null)
|
if (proxy.Login != null)
|
||||||
socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
socket.Options.Proxy.Credentials = new NetworkCredential(proxy.Login, proxy.Password);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void SetProcessState(ProcessState state)
|
||||||
|
{
|
||||||
|
if (_processState == state)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logger.SocketProcessingStateChanged(Id, _processState.ToString(), state.ToString());
|
||||||
|
_processState = state;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -729,7 +792,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The request id
|
/// The request weight
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Weight { get; set; }
|
public int Weight { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Sockets
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Dedicated connection configuration
|
||||||
|
/// </summary>
|
||||||
|
public class DedicatedConnectionConfig
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Socket address
|
||||||
|
/// </summary>
|
||||||
|
public string SocketAddress { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// authenticated
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.Requests;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -18,16 +19,22 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; } = ExchangeHelpers.NextId();
|
public int Id { get; } = ExchangeHelpers.NextId();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData => true;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Has this query been completed
|
/// Has this query been completed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Completed { get; set; }
|
public bool Completed { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of required responses. Can be more than 1 when for example subscribing multiple symbols streams in a single request,
|
||||||
|
/// and each symbol receives it's own confirmation response
|
||||||
|
/// </summary>
|
||||||
|
public int RequiredResponses { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current number of responses received on this query
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentResponses { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Timestamp of when the request was send
|
/// Timestamp of when the request was send
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -46,7 +53,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wait event for the calling message processing thread
|
/// Wait event for the calling message processing thread
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ManualResetEvent? ContinueAwaiter { get; set; }
|
public AsyncResetEvent? ContinueAwaiter { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Strings to match this query to a received message
|
/// Strings to match this query to a received message
|
||||||
@@ -112,11 +119,12 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wait untill timeout or the request is competed
|
/// Wait until timeout or the request is completed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="timeout"></param>
|
/// <param name="timeout"></param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task WaitAsync(TimeSpan timeout) => await _event.WaitAsync(timeout).ConfigureAwait(false);
|
public async Task WaitAsync(TimeSpan timeout, CancellationToken ct) => await _event.WaitAsync(timeout, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
public virtual CallResult<object> Deserialize(IMessageAccessor message, Type type) => message.Deserialize(type);
|
||||||
@@ -138,23 +146,24 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public abstract CallResult Handle(SocketConnection connection, DataEvent<object> message);
|
public abstract Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Query
|
/// Query
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TResponse">Response object type</typeparam>
|
/// <typeparam name="TServerResponse">The type returned from the server</typeparam>
|
||||||
public abstract class Query<TResponse> : Query
|
/// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam>
|
||||||
|
public abstract class Query<TServerResponse, THandlerResponse> : Query
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse);
|
public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The typed call result
|
/// The typed call result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public CallResult<TResponse>? TypedResult => (CallResult<TResponse>?)Result;
|
public CallResult<THandlerResponse>? TypedResult => (CallResult<THandlerResponse>?)Result;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
@@ -167,13 +176,26 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override CallResult Handle(SocketConnection connection, DataEvent<object> message)
|
public override async Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
|
||||||
{
|
{
|
||||||
Completed = true;
|
CurrentResponses++;
|
||||||
Response = message.Data;
|
if (CurrentResponses == RequiredResponses)
|
||||||
Result = HandleMessage(connection, message.As((TResponse)message.Data));
|
{
|
||||||
_event.Set();
|
Completed = true;
|
||||||
ContinueAwaiter?.WaitOne();
|
Response = message.Data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Result?.Success != false)
|
||||||
|
// If an error result is already set don't override that
|
||||||
|
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
|
||||||
|
|
||||||
|
if (CurrentResponses == RequiredResponses)
|
||||||
|
{
|
||||||
|
_event.Set();
|
||||||
|
if (ContinueAwaiter != null)
|
||||||
|
await ContinueAwaiter.WaitAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
return Result;
|
return Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +205,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => new CallResult<TResponse>(message.Data, message.OriginalData, null);
|
public abstract CallResult<THandlerResponse> HandleMessage(SocketConnection connection, DataEvent<TServerResponse> message);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Timeout()
|
public override void Timeout()
|
||||||
@@ -192,7 +214,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Completed = true;
|
Completed = true;
|
||||||
Result = new CallResult<TResponse>(new CancellationRequestedError(null, "Query timeout", null));
|
Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null));
|
||||||
ContinueAwaiter?.Set();
|
ContinueAwaiter?.Set();
|
||||||
_event.Set();
|
_event.Set();
|
||||||
}
|
}
|
||||||
@@ -200,10 +222,35 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Fail(Error error)
|
public override void Fail(Error error)
|
||||||
{
|
{
|
||||||
Result = new CallResult<TResponse>(error);
|
Result = new CallResult<THandlerResponse>(error);
|
||||||
Completed = true;
|
Completed = true;
|
||||||
ContinueAwaiter?.Set();
|
ContinueAwaiter?.Set();
|
||||||
_event.Set();
|
_event.Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">Response object type</typeparam>
|
||||||
|
public abstract class Query<TResponse> : Query<TResponse, TResponse>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request"></param>
|
||||||
|
/// <param name="authenticated"></param>
|
||||||
|
/// <param name="weight"></param>
|
||||||
|
protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handle the query response
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="connection"></param>
|
||||||
|
/// <param name="message"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public override CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => message.ToCallResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this connection should be kept alive even when there is no subscription
|
||||||
|
/// </summary>
|
||||||
|
public bool DedicatedRequestConnection { get; internal set; }
|
||||||
|
|
||||||
private bool _pausedActivity;
|
private bool _pausedActivity;
|
||||||
private readonly object _listenersLock;
|
private readonly object _listenersLock;
|
||||||
private readonly List<IMessageProcessor> _listeners;
|
private readonly List<IMessageProcessor> _listeners;
|
||||||
@@ -408,14 +413,14 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="data"></param>
|
/// <param name="data"></param>
|
||||||
/// <param name="type"></param>
|
/// <param name="type"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual void HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
protected virtual async Task HandleStreamMessage(WebSocketMessageType type, ReadOnlyMemory<byte> data)
|
||||||
{
|
{
|
||||||
var sw = Stopwatch.StartNew();
|
var sw = Stopwatch.StartNew();
|
||||||
var receiveTime = DateTime.UtcNow;
|
var receiveTime = DateTime.UtcNow;
|
||||||
string? originalData = null;
|
string? originalData = null;
|
||||||
|
|
||||||
// 1. Decrypt/Preprocess if necessary
|
// 1. Decrypt/Preprocess if necessary
|
||||||
data = ApiClient.PreprocessStreamMessage(type, data);
|
data = ApiClient.PreprocessStreamMessage(this, type, data);
|
||||||
|
|
||||||
// 2. Read data into accessor
|
// 2. Read data into accessor
|
||||||
_accessor.Read(data);
|
_accessor.Read(data);
|
||||||
@@ -443,7 +448,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
// 4. Get the listeners interested in this message
|
// 4. Get the listeners interested in this message
|
||||||
List<IMessageProcessor> processors;
|
List<IMessageProcessor> processors;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId) && s.CanHandleData).ToList();
|
processors = _listeners.Where(s => s.ListenerIdentifiers.Contains(listenId)).ToList();
|
||||||
|
|
||||||
if (processors.Count == 0)
|
if (processors.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -451,7 +456,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
{
|
{
|
||||||
List<string> listenerIds;
|
List<string> listenerIds;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
listenerIds = _listeners.Where(l => l.CanHandleData).SelectMany(l => l.ListenerIdentifiers).ToList();
|
listenerIds = _listeners.SelectMany(l => l.ListenerIdentifiers).ToList();
|
||||||
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
_logger.ReceivedMessageNotMatchedToAnyListener(SocketId, listenId, string.Join(",", listenerIds));
|
||||||
UnhandledMessage?.Invoke(_accessor);
|
UnhandledMessage?.Invoke(_accessor);
|
||||||
}
|
}
|
||||||
@@ -478,6 +483,10 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (processor is Subscription subscriptionProcessor && !subscriptionProcessor.Confirmed)
|
||||||
|
// If this message is for this listener then it is automatically confirmed, even if the subscription is not (yet) confirmed
|
||||||
|
subscriptionProcessor.Confirmed = true;
|
||||||
|
|
||||||
// 6. Deserialize the message
|
// 6. Deserialize the message
|
||||||
object? deserialized = null;
|
object? deserialized = null;
|
||||||
desCache?.TryGetValue(messageType, out deserialized);
|
desCache?.TryGetValue(messageType, out deserialized);
|
||||||
@@ -498,7 +507,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var innerSw = Stopwatch.StartNew();
|
var innerSw = Stopwatch.StartNew();
|
||||||
processor.Handle(this, new DataEvent<object>(deserialized, null, originalData, receiveTime, null));
|
await processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null)).ConfigureAwait(false);
|
||||||
|
if (processor is Query query && query.RequiredResponses != 1)
|
||||||
|
_logger.LogDebug($"[Sckt {SocketId}] [Req {query.Id}] responses: {query.CurrentResponses}/{query.RequiredResponses}");
|
||||||
totalUserTime += (int)innerSw.ElapsedMilliseconds;
|
totalUserTime += (int)innerSw.ElapsedMilliseconds;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -564,9 +575,8 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well
|
/// Close a subscription on this connection. If all subscriptions on this connection are closed the connection gets closed as well
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="subscription">Subscription to close</param>
|
/// <param name="subscription">Subscription to close</param>
|
||||||
/// <param name="unsubEvenIfNotConfirmed">Whether to send an unsub request even if the subscription wasn't confirmed</param>
|
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task CloseAsync(Subscription subscription, bool unsubEvenIfNotConfirmed = false)
|
public async Task CloseAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
subscription.Closed = true;
|
subscription.Closed = true;
|
||||||
|
|
||||||
@@ -580,14 +590,18 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
bool anyDuplicateSubscription;
|
bool anyDuplicateSubscription;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.ListenerIdentifiers.All(l => subscription.ListenerIdentifiers.Contains(l)));
|
anyDuplicateSubscription = _listeners.OfType<Subscription>().Any(x => x != subscription && x.ListenerIdentifiers.All(l => subscription.ListenerIdentifiers.Contains(l)));
|
||||||
|
|
||||||
|
bool shouldCloseConnection;
|
||||||
|
lock (_listenersLock)
|
||||||
|
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed) && !DedicatedRequestConnection;
|
||||||
|
|
||||||
if (!anyDuplicateSubscription)
|
if (!anyDuplicateSubscription)
|
||||||
{
|
{
|
||||||
bool needUnsub;
|
bool needUnsub;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
needUnsub = _listeners.Contains(subscription);
|
needUnsub = _listeners.Contains(subscription) && !shouldCloseConnection;
|
||||||
|
|
||||||
if (needUnsub && (unsubEvenIfNotConfirmed || subscription.Confirmed) && _socket.IsOpen)
|
if (needUnsub && _socket.IsOpen)
|
||||||
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
await UnsubscribeAsync(subscription).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -601,16 +615,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool shouldCloseConnection;
|
|
||||||
lock (_listenersLock)
|
|
||||||
{
|
|
||||||
shouldCloseConnection = _listeners.OfType<Subscription>().All(r => !r.UserSubscription || r.Closed);
|
|
||||||
if (shouldCloseConnection)
|
|
||||||
Status = SocketStatus.Closing;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldCloseConnection)
|
if (shouldCloseConnection)
|
||||||
{
|
{
|
||||||
|
Status = SocketStatus.Closing;
|
||||||
_logger.ClosingNoMoreSubscriptions(SocketId);
|
_logger.ClosingNoMoreSubscriptions(SocketId);
|
||||||
await CloseAsync().ConfigureAwait(false);
|
await CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@@ -686,27 +693,30 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="query">Query to send</param>
|
/// <param name="query">Query to send</param>
|
||||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, ManualResetEvent? continueEvent = null)
|
public virtual async Task<CallResult> SendAndWaitQueryAsync(Query query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
|
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||||
return query.Result ?? new CallResult(new ServerError("Timeout"));
|
return query.Result ?? new CallResult(new ServerError("Timeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Send a query request and wait for an answer
|
/// Send a query request and wait for an answer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Query response type</typeparam>
|
/// <typeparam name="THandlerResponse">Expected result type</typeparam>
|
||||||
|
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
|
||||||
/// <param name="query">Query to send</param>
|
/// <param name="query">Query to send</param>
|
||||||
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
|
||||||
|
/// <param name="ct">Cancellation token</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual async Task<CallResult<T>> SendAndWaitQueryAsync<T>(Query<T> query, ManualResetEvent? continueEvent = null)
|
public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, AsyncResetEvent? continueEvent = null, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
|
await SendAndWaitIntAsync(query, continueEvent, ct).ConfigureAwait(false);
|
||||||
return query.TypedResult ?? new CallResult<T>(new ServerError("Timeout"));
|
return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent)
|
private async Task SendAndWaitIntAsync(Query query, AsyncResetEvent? continueEvent, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
lock(_listenersLock)
|
lock(_listenersLock)
|
||||||
_listeners.Add(query);
|
_listeners.Add(query);
|
||||||
@@ -723,7 +733,7 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (!ct.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
{
|
{
|
||||||
@@ -734,11 +744,17 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (query.Completed)
|
if (query.Completed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await query.WaitAsync(TimeSpan.FromMilliseconds(500)).ConfigureAwait(false);
|
await query.WaitAsync(TimeSpan.FromMilliseconds(500), ct).ConfigureAwait(false);
|
||||||
|
|
||||||
if (query.Completed)
|
if (query.Completed)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ct.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
query.Fail(new CancellationRequestedError());
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -784,7 +800,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_logger.SendingData(SocketId, requestId, data);
|
_logger.SendingData(SocketId, requestId, data);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_socket.Send(requestId, data, weight);
|
if (!_socket.Send(requestId, data, weight))
|
||||||
|
return new CallResult(new WebError("Failed to send message, connection not open"));
|
||||||
|
|
||||||
return new CallResult(null);
|
return new CallResult(null);
|
||||||
}
|
}
|
||||||
catch(Exception ex)
|
catch(Exception ex)
|
||||||
@@ -798,20 +816,27 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
bool anySubscriptions;
|
if (!DedicatedRequestConnection)
|
||||||
lock (_listenersLock)
|
|
||||||
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
|
||||||
if (!anySubscriptions)
|
|
||||||
{
|
{
|
||||||
// No need to resubscribe anything
|
bool anySubscriptions;
|
||||||
_logger.NothingToResubscribeCloseConnection(SocketId);
|
lock (_listenersLock)
|
||||||
_ = _socket.CloseAsync();
|
anySubscriptions = _listeners.OfType<Subscription>().Any(s => s.UserSubscription);
|
||||||
return new CallResult(null);
|
if (!anySubscriptions)
|
||||||
|
{
|
||||||
|
// No need to resubscribe anything
|
||||||
|
_logger.NothingToResubscribeCloseConnection(SocketId);
|
||||||
|
_ = _socket.CloseAsync();
|
||||||
|
return new CallResult(null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool anyAuthenticated;
|
bool anyAuthenticated;
|
||||||
lock (_listenersLock)
|
lock (_listenersLock)
|
||||||
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated);
|
{
|
||||||
|
anyAuthenticated = _listeners.OfType<Subscription>().Any(s => s.Authenticated)
|
||||||
|
|| (DedicatedRequestConnection && ApiClient.AuthenticationProvider != null);
|
||||||
|
}
|
||||||
|
|
||||||
if (anyAuthenticated)
|
if (anyAuthenticated)
|
||||||
{
|
{
|
||||||
// If we reconnected a authenticated connection we need to re-authenticate
|
// If we reconnected a authenticated connection we need to re-authenticate
|
||||||
@@ -826,40 +851,43 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
_logger.AuthenticationSucceeded(SocketId);
|
_logger.AuthenticationSucceeded(SocketId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a list of all subscriptions on the socket
|
|
||||||
List<Subscription> subList;
|
|
||||||
lock (_listenersLock)
|
|
||||||
subList = _listeners.OfType<Subscription>().ToList();
|
|
||||||
|
|
||||||
foreach(var subscription in subList)
|
|
||||||
{
|
|
||||||
subscription.ConnectionInvocations = 0;
|
|
||||||
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
|
||||||
if (!result)
|
|
||||||
{
|
|
||||||
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
|
// Foreach subscription which is subscribed by a subscription request we will need to resend that request to resubscribe
|
||||||
for (var i = 0; i < subList.Count; i += ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket)
|
int batch = 0;
|
||||||
|
int batchSize = ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket;
|
||||||
|
while (true)
|
||||||
{
|
{
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|
||||||
|
List<Subscription> subList;
|
||||||
|
lock (_listenersLock)
|
||||||
|
subList = _listeners.OfType<Subscription>().Skip(batch * batchSize).Take(batchSize).ToList();
|
||||||
|
|
||||||
|
if (subList.Count == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
var taskList = new List<Task<CallResult>>();
|
var taskList = new List<Task<CallResult>>();
|
||||||
foreach (var subscription in subList.Skip(i).Take(ApiClient.ClientOptions.MaxConcurrentResubscriptionsPerSocket))
|
foreach (var subscription in subList)
|
||||||
{
|
{
|
||||||
|
subscription.ConnectionInvocations = 0;
|
||||||
|
var result = await ApiClient.RevitalizeRequestAsync(subscription).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
_logger.FailedRequestRevitalization(SocketId, result.Error?.ToString());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
var subQuery = subscription.GetSubQuery(this);
|
var subQuery = subscription.GetSubQuery(this);
|
||||||
if (subQuery == null)
|
if (subQuery == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var waitEvent = new ManualResetEvent(false);
|
var waitEvent = new AsyncResetEvent(false);
|
||||||
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
taskList.Add(SendAndWaitQueryAsync(subQuery, waitEvent).ContinueWith((r) =>
|
||||||
{
|
{
|
||||||
subscription.HandleSubQueryResponse(subQuery.Response!);
|
subscription.HandleSubQueryResponse(subQuery.Response!);
|
||||||
waitEvent.Set();
|
waitEvent.Set();
|
||||||
|
if (r.Result.Success)
|
||||||
|
subscription.Confirmed = true;
|
||||||
return r.Result;
|
return r.Result;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -867,10 +895,9 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
await Task.WhenAll(taskList).ConfigureAwait(false);
|
await Task.WhenAll(taskList).ConfigureAwait(false);
|
||||||
if (taskList.Any(t => !t.Result.Success))
|
if (taskList.Any(t => !t.Result.Success))
|
||||||
return taskList.First(t => !t.Result.Success).Result;
|
return taskList.First(t => !t.Result.Success).Result;
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var subscription in subList)
|
batch++;
|
||||||
subscription.Confirmed = true;
|
}
|
||||||
|
|
||||||
if (!_socket.IsOpen)
|
if (!_socket.IsOpen)
|
||||||
return new CallResult(new WebError("Socket not connected"));
|
return new CallResult(new WebError("Socket not connected"));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Sockets
|
namespace CryptoExchange.Net.Sockets
|
||||||
{
|
{
|
||||||
@@ -18,11 +19,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Can handle data
|
|
||||||
/// </summary>
|
|
||||||
public bool CanHandleData => Confirmed || HandleUpdatesBeforeConfirmation;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Total amount of invocations
|
/// Total amount of invocations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -42,11 +38,6 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// Has the subscription been confirmed
|
/// Has the subscription been confirmed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool Confirmed { get; set; }
|
public bool Confirmed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether this subscription should handle update messages before confirmation
|
|
||||||
/// </summary>
|
|
||||||
public bool HandleUpdatesBeforeConfirmation { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Is the subscription closed
|
/// Is the subscription closed
|
||||||
@@ -132,11 +123,11 @@ namespace CryptoExchange.Net.Sockets
|
|||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <param name="message"></param>
|
/// <param name="message"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public CallResult Handle(SocketConnection connection, DataEvent<object> message)
|
public Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message)
|
||||||
{
|
{
|
||||||
ConnectionInvocations++;
|
ConnectionInvocations++;
|
||||||
TotalInvocations++;
|
TotalInvocations++;
|
||||||
return DoHandleMessage(connection, message);
|
return Task.FromResult(DoHandleMessage(connection, message));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.Converters.JsonNet;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Comparers
|
||||||
|
{
|
||||||
|
internal class JsonNetComparer
|
||||||
|
{
|
||||||
|
internal static void CompareData(
|
||||||
|
string method,
|
||||||
|
object resultData,
|
||||||
|
string json,
|
||||||
|
string? nestedJsonProperty,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool userSingleArrayItem = false)
|
||||||
|
{
|
||||||
|
var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyAttribute?)p.GetCustomAttributes(typeof(JsonPropertyAttribute), true).SingleOrDefault()));
|
||||||
|
var jsonObject = JToken.Parse(json);
|
||||||
|
if (nestedJsonProperty != null)
|
||||||
|
{
|
||||||
|
var nested = nestedJsonProperty.Split('.');
|
||||||
|
foreach (var nest in nested)
|
||||||
|
{
|
||||||
|
if (int.TryParse(nest, out var index))
|
||||||
|
jsonObject = jsonObject![index];
|
||||||
|
else
|
||||||
|
jsonObject = jsonObject![nest];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSingleArrayItem)
|
||||||
|
jsonObject = ((JArray)jsonObject!)[0];
|
||||||
|
|
||||||
|
if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)resultData;
|
||||||
|
var jObj = (JObject)jsonObject!;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
// TODO Some additional checking for objects
|
||||||
|
foreach (var prop in ((JObject)dictProp.Value).Properties())
|
||||||
|
CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonObject!.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)jsonObject;
|
||||||
|
if (resultData is IEnumerable list)
|
||||||
|
{
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
if (resultObj is string)
|
||||||
|
// string list
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObjs.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var item in jsonObject)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, resultData, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"Successfully validated {method}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties)
|
||||||
|
{
|
||||||
|
var resultProperties = obj.GetType().GetProperties().Select(p => (p, ((JsonPropertyAttribute?)p.GetCustomAttributes(typeof(JsonPropertyAttribute), true).SingleOrDefault())?.PropertyName));
|
||||||
|
|
||||||
|
// Property has a value
|
||||||
|
var property = resultProperties.SingleOrDefault(p => p.PropertyName == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name.Equals(prop.Name, StringComparison.InvariantCultureIgnoreCase)).p;
|
||||||
|
|
||||||
|
if (property is null)
|
||||||
|
// Property not found
|
||||||
|
throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`");
|
||||||
|
|
||||||
|
var propertyValue = property.GetValue(obj);
|
||||||
|
if (property.GetCustomAttribute<JsonPropertyAttribute>(true)?.ItemConverterType == null)
|
||||||
|
CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckPropertyValue(string method, JToken propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List<string>? ignoreProperties = null)
|
||||||
|
{
|
||||||
|
if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString()))
|
||||||
|
{
|
||||||
|
if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Property value not correct
|
||||||
|
if (propValue.ToString() != "0")
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString())) || propValue.ToString() == "0")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)propertyValue;
|
||||||
|
var jObj = (JObject)propValue;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
CheckObject(method, dictProp, dict[dictProp.Name]!, ignoreProperties);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
|
||||||
|
&& propertyValue.GetType() != typeof(string))
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
var list = (IEnumerable)propertyValue;
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (JToken jtoken in jObjs)
|
||||||
|
{
|
||||||
|
enumerator.MoveNext();
|
||||||
|
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
|
||||||
|
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
|
||||||
|
// Custom converter for the type, skip
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (jtoken.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jtoken).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jtoken.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jtoken.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jtoken).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jtoken}");
|
||||||
|
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)jtoken, value!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (propValue.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var item in propValue)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, propertyValue, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(propValue.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
if (propertyValue is IEnumerable list)
|
||||||
|
{
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
if (!enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObjs.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckValues(string method, string property, Type propertyType, JValue jsonValue, object objectValue)
|
||||||
|
{
|
||||||
|
if (jsonValue.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
if (objectValue is decimal dec)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<decimal>() != dec)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {dec}");
|
||||||
|
}
|
||||||
|
else if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (!jsonValue.Value<string>()!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromLong(jsonValue.Value<long>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Boolean)
|
||||||
|
{
|
||||||
|
if (objectValue is bool boolVal && jsonValue.Value<bool>() != boolVal)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||||
|
|
||||||
|
if (jsonValue.Value<bool>() != bool.Parse(objectValue.ToString()))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,418 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using CryptoExchange.Net.Converters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Comparers
|
||||||
|
{
|
||||||
|
internal class SystemTextJsonComparer
|
||||||
|
{
|
||||||
|
internal static void CompareData(
|
||||||
|
string method,
|
||||||
|
object resultData,
|
||||||
|
string json,
|
||||||
|
string? nestedJsonProperty,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool userSingleArrayItem = false)
|
||||||
|
{
|
||||||
|
var resultProperties = resultData.GetType().GetProperties().Select(p => (p, (JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault()));
|
||||||
|
var jsonObject = JToken.Parse(json);
|
||||||
|
if (nestedJsonProperty != null)
|
||||||
|
{
|
||||||
|
var nested = nestedJsonProperty.Split('.');
|
||||||
|
foreach (var nest in nested)
|
||||||
|
{
|
||||||
|
if (int.TryParse(nest, out var index))
|
||||||
|
jsonObject = jsonObject![index];
|
||||||
|
else
|
||||||
|
jsonObject = jsonObject![nest];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userSingleArrayItem)
|
||||||
|
jsonObject = ((JArray)jsonObject!)[0];
|
||||||
|
|
||||||
|
if (resultData.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)resultData;
|
||||||
|
var jObj = (JObject)jsonObject!;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Dictionary has no value for {dictProp.Name} while input json `{dictProp.Name}` has value {dictProp.Value}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
// TODO Some additional checking for objects
|
||||||
|
foreach (var prop in ((JObject)dictProp.Value).Properties())
|
||||||
|
CheckObject(method, prop, dict[dictProp.Name]!, ignoreProperties!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
{
|
||||||
|
if (dictProp.Value.ToString() == "")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonObject!.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)jsonObject;
|
||||||
|
if (resultData is IEnumerable list)
|
||||||
|
{
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
if (!enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
if (resultObj is string)
|
||||||
|
// string list
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resultProps = resultData.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObjs.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultData), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var item in jsonObject)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, resultData, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"Successfully validated {method}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckObject(string method, JProperty prop, object obj, List<string>? ignoreProperties)
|
||||||
|
{
|
||||||
|
var publicProperties = obj.GetType().GetProperties(
|
||||||
|
System.Reflection.BindingFlags.Public
|
||||||
|
| System.Reflection.BindingFlags.GetProperty
|
||||||
|
| System.Reflection.BindingFlags.SetProperty
|
||||||
|
| System.Reflection.BindingFlags.Instance).Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
|
||||||
|
|
||||||
|
var internalProperties = obj.GetType().GetProperties(
|
||||||
|
System.Reflection.BindingFlags.NonPublic
|
||||||
|
| System.Reflection.BindingFlags.GetProperty
|
||||||
|
| System.Reflection.BindingFlags.SetProperty
|
||||||
|
| System.Reflection.BindingFlags.Instance)
|
||||||
|
.Where(p => p.CustomAttributes.Any(x => x.AttributeType == typeof(JsonIncludeAttribute)))
|
||||||
|
.Select(p => (p, ((JsonPropertyNameAttribute?)p.GetCustomAttributes(typeof(JsonPropertyNameAttribute), true).SingleOrDefault())?.Name));
|
||||||
|
|
||||||
|
var resultProperties = publicProperties.Concat(internalProperties);
|
||||||
|
|
||||||
|
// Property has a value
|
||||||
|
var property = resultProperties.SingleOrDefault(p => p.Name == prop.Name).p;
|
||||||
|
property ??= resultProperties.SingleOrDefault(p => p.p.Name == prop.Name).p;
|
||||||
|
|
||||||
|
if (property is null)
|
||||||
|
// Property not found
|
||||||
|
throw new Exception($"{method}: Missing property `{prop.Name}` on `{obj.GetType().Name}`");
|
||||||
|
|
||||||
|
var getMethod = property.GetGetMethod();
|
||||||
|
if (getMethod is null)
|
||||||
|
// There is no getter, so probably just a set for an alternative json name
|
||||||
|
return;
|
||||||
|
|
||||||
|
var propertyValue = property.GetValue(obj);
|
||||||
|
CheckPropertyValue(method, prop.Value, propertyValue, property.PropertyType, property.Name, prop.Name, ignoreProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckPropertyValue(string method, JToken propValue, object? propertyValue, Type propertyType, string? propertyName = null, string? propName = null, List<string>? ignoreProperties = null)
|
||||||
|
{
|
||||||
|
if (propertyValue == default && propValue.Type != JTokenType.Null && !string.IsNullOrEmpty(propValue.ToString()))
|
||||||
|
{
|
||||||
|
if (propertyType == typeof(DateTime?) && (propValue.ToString() == "" || propValue.ToString() == "0" || propValue.ToString() == "-1"))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Property value not correct
|
||||||
|
if (propValue.ToString() != "0")
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (propertyValue == default && (propValue.Type == JTokenType.Null || string.IsNullOrEmpty(propValue.ToString())) || propValue.ToString() == "0")
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (propertyValue!.GetType().GetInterfaces().Contains(typeof(IDictionary)))
|
||||||
|
{
|
||||||
|
var dict = (IDictionary)propertyValue;
|
||||||
|
var jObj = (JObject)propValue;
|
||||||
|
var properties = jObj.Properties();
|
||||||
|
foreach (var dictProp in properties)
|
||||||
|
{
|
||||||
|
if (!dict.Contains(dictProp.Name))
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {propValue}");
|
||||||
|
|
||||||
|
if (dictProp.Value.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
|
||||||
|
// Property value not correct
|
||||||
|
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {propValue} for");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propertyValue.GetType().GetInterfaces().Contains(typeof(IEnumerable))
|
||||||
|
&& propertyValue.GetType() != typeof(string))
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
var list = (IEnumerable)propertyValue;
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (JToken jtoken in jObjs)
|
||||||
|
{
|
||||||
|
var moved = enumerator.MoveNext();
|
||||||
|
if (!moved)
|
||||||
|
throw new Exception("Enumeration not moved; incorrect amount of results?");
|
||||||
|
|
||||||
|
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
|
||||||
|
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
|
||||||
|
// Custom converter for the type, skip
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (jtoken.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jtoken).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jtoken.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jtoken.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), propertyType, arrayProp.Name, "Array index " + i, ignoreProperties);
|
||||||
|
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jtoken).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Property `{propertyName}` has no value while input json `{propName}` has value {jtoken}");
|
||||||
|
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)jtoken, value!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (propValue.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var item in propValue)
|
||||||
|
{
|
||||||
|
if (item is JProperty prop)
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(prop.Name) == true)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
CheckObject(method, prop, propertyValue, ignoreProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (propValue.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var jObjs = (JArray)propValue;
|
||||||
|
if (propertyValue is IEnumerable list)
|
||||||
|
{
|
||||||
|
var enumerator = list.GetEnumerator();
|
||||||
|
foreach (var jObj in jObjs)
|
||||||
|
{
|
||||||
|
if (!enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jObj.Type == JTokenType.Object)
|
||||||
|
{
|
||||||
|
foreach (var subProp in ((JObject)jObj).Properties())
|
||||||
|
{
|
||||||
|
if (ignoreProperties?.Contains(subProp.Name) == true)
|
||||||
|
continue;
|
||||||
|
CheckObject(method, subProp, enumerator.Current, ignoreProperties!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jObj.Type == JTokenType.Array)
|
||||||
|
{
|
||||||
|
var resultObj = enumerator.Current;
|
||||||
|
var resultProps = resultObj.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
var arrayConverterProperty = resultObj.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true).FirstOrDefault();
|
||||||
|
var jsonConverter = ((JsonConverterAttribute)arrayConverterProperty!).ConverterType;
|
||||||
|
if (jsonConverter != typeof(ArrayConverter))
|
||||||
|
// Not array converter?
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObj.Values())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(resultObj), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var value = enumerator.Current;
|
||||||
|
if (value == default && ((JValue)jObj).Type != JTokenType.Null)
|
||||||
|
throw new Exception($"{method}: Array has no value while input json array has value {jObj}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var resultProps = propertyValue.GetType().GetProperties().Select(p => (p, p.GetCustomAttributes(typeof(ArrayPropertyAttribute), true).Cast<ArrayPropertyAttribute>().SingleOrDefault()));
|
||||||
|
int i = 0;
|
||||||
|
foreach (var item in jObjs.Children())
|
||||||
|
{
|
||||||
|
var arrayProp = resultProps.Where(p => p.Item2 != null).SingleOrDefault(p => p.Item2!.Index == i).p;
|
||||||
|
if (arrayProp != null)
|
||||||
|
CheckPropertyValue(method, item, arrayProp.GetValue(propertyValue), arrayProp.PropertyType, arrayProp.Name, "Array index " + i, ignoreProperties!);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CheckValues(method, propertyName!, propertyType, (JValue)propValue, propertyValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckValues(string method, string property, Type propertyType, JValue jsonValue, object objectValue)
|
||||||
|
{
|
||||||
|
if (jsonValue.Type == JTokenType.String)
|
||||||
|
{
|
||||||
|
if (objectValue is decimal dec)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<decimal>() != dec)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {dec}");
|
||||||
|
}
|
||||||
|
else if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
|
||||||
|
}
|
||||||
|
else if (objectValue is bool bl)
|
||||||
|
{
|
||||||
|
var jsonStr = jsonValue.Value<string>();
|
||||||
|
if (bl && (jsonStr != "1" && jsonStr != "true" && jsonStr != "True"))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {bl}");
|
||||||
|
if (!bl && (jsonStr != "0" && jsonStr != "-1" && jsonStr != "false" && jsonStr != "False"))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {bl}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (!jsonValue.Value<string>()!.Equals(Convert.ToString(objectValue, CultureInfo.InvariantCulture), StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Integer)
|
||||||
|
{
|
||||||
|
if (objectValue is DateTime time)
|
||||||
|
{
|
||||||
|
if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!))
|
||||||
|
throw new Exception($"{method}: {property} not equal: {DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!)} vs {time}");
|
||||||
|
}
|
||||||
|
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
|
||||||
|
{
|
||||||
|
// TODO enum comparing
|
||||||
|
}
|
||||||
|
else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue))
|
||||||
|
{
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (jsonValue.Type == JTokenType.Boolean)
|
||||||
|
{
|
||||||
|
if (jsonValue.Value<bool>() != (bool)objectValue)
|
||||||
|
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<bool>()} vs {(bool)objectValue}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
internal class EnumValueTraceListener : TraceListener
|
||||||
|
{
|
||||||
|
public override void Write(string message)
|
||||||
|
{
|
||||||
|
if (message.Contains("Cannot map"))
|
||||||
|
throw new Exception("Enum value error: " + message);
|
||||||
|
|
||||||
|
if (message.Contains("Received null enum value"))
|
||||||
|
throw new Exception("Enum null error: " + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteLine(string message)
|
||||||
|
{
|
||||||
|
if (message.Contains("Cannot map"))
|
||||||
|
throw new Exception("Enum value error: " + message);
|
||||||
|
|
||||||
|
if (message.Contains("Received null enum value"))
|
||||||
|
throw new Exception("Enum null error: " + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestAuthTimeProvider : IAuthTimeProvider
|
||||||
|
{
|
||||||
|
private readonly DateTime _timestamp;
|
||||||
|
|
||||||
|
public TestAuthTimeProvider(DateTime timestamp)
|
||||||
|
{
|
||||||
|
_timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTime GetTime() => _timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Test implementation for nonce provider, returning a prespecified nonce
|
||||||
|
/// </summary>
|
||||||
|
public class TestNonceProvider : INonceProvider
|
||||||
|
{
|
||||||
|
private readonly long _nonce;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public TestNonceProvider(long nonce)
|
||||||
|
{
|
||||||
|
_nonce = nonce;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public long GetNonce() => _nonce;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRequest : IRequest
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IEnumerable<string>> _headers = new Dictionary<string, IEnumerable<string>>();
|
||||||
|
private readonly TestResponse _response;
|
||||||
|
|
||||||
|
public string Accept { set { } }
|
||||||
|
|
||||||
|
public string? Content { get; private set; }
|
||||||
|
|
||||||
|
public HttpMethod Method { get; set; }
|
||||||
|
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
|
||||||
|
public int RequestId { get; set; }
|
||||||
|
|
||||||
|
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
public TestRequest(TestResponse response)
|
||||||
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
|
{
|
||||||
|
_response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddHeader(string key, string value)
|
||||||
|
{
|
||||||
|
_headers.Add(key, new[] { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, IEnumerable<string>> GetHeaders() => _headers;
|
||||||
|
|
||||||
|
public Task<IResponse> GetResponseAsync(CancellationToken cancellationToken) => Task.FromResult<IResponse>(_response);
|
||||||
|
|
||||||
|
public void SetContent(byte[] data)
|
||||||
|
{
|
||||||
|
Content = Encoding.UTF8.GetString(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetContent(string data, string contentType)
|
||||||
|
{
|
||||||
|
Content = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestRequestFactory : IRequestFactory
|
||||||
|
{
|
||||||
|
private readonly TestRequest _request;
|
||||||
|
|
||||||
|
public TestRequestFactory(TestRequest request)
|
||||||
|
{
|
||||||
|
_request = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IRequest Create(HttpMethod method, Uri uri, int requestId)
|
||||||
|
{
|
||||||
|
_request.Method = method;
|
||||||
|
_request.Uri = uri;
|
||||||
|
_request.RequestId = requestId;
|
||||||
|
return _request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestResponse : IResponse
|
||||||
|
{
|
||||||
|
private readonly Stream _response;
|
||||||
|
|
||||||
|
public HttpStatusCode StatusCode { get; }
|
||||||
|
|
||||||
|
public bool IsSuccessStatusCode { get; }
|
||||||
|
|
||||||
|
public long? ContentLength { get; }
|
||||||
|
|
||||||
|
public IEnumerable<KeyValuePair<string, IEnumerable<string>>> ResponseHeaders { get; } = new Dictionary<string, IEnumerable<string>>();
|
||||||
|
|
||||||
|
public TestResponse(HttpStatusCode code, Stream response)
|
||||||
|
{
|
||||||
|
StatusCode = code;
|
||||||
|
IsSuccessStatusCode = code == HttpStatusCode.OK;
|
||||||
|
_response = response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Close()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<Stream> GetResponseStreamAsync() => Task.FromResult(_response);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestSocket : IWebsocket
|
||||||
|
{
|
||||||
|
public event Action<string>? OnMessageSend;
|
||||||
|
|
||||||
|
public bool CanConnect { get; set; } = true;
|
||||||
|
public bool Connected { get; set; }
|
||||||
|
|
||||||
|
public event Func<Task>? OnClose;
|
||||||
|
#pragma warning disable 0067
|
||||||
|
public event Func<Task>? OnReconnected;
|
||||||
|
public event Func<Task>? OnReconnecting;
|
||||||
|
public event Func<int, Task>? OnRequestRateLimited;
|
||||||
|
public event Func<Exception, Task>? OnError;
|
||||||
|
#pragma warning restore 0067
|
||||||
|
public event Func<int, Task>? OnRequestSent;
|
||||||
|
public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task>? OnStreamMessage;
|
||||||
|
public event Func<Task>? OnOpen;
|
||||||
|
|
||||||
|
public int Id { get; }
|
||||||
|
public bool IsClosed => !Connected;
|
||||||
|
public bool IsOpen => Connected;
|
||||||
|
public double IncomingKbps => 0;
|
||||||
|
public Uri Uri { get; set; }
|
||||||
|
public Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||||
|
|
||||||
|
public static int lastId = 0;
|
||||||
|
public static object lastIdLock = new object();
|
||||||
|
|
||||||
|
public TestSocket(string address)
|
||||||
|
{
|
||||||
|
Uri = new Uri(address);
|
||||||
|
lock (lastIdLock)
|
||||||
|
{
|
||||||
|
Id = lastId + 1;
|
||||||
|
lastId++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<CallResult> ConnectAsync()
|
||||||
|
{
|
||||||
|
Connected = CanConnect;
|
||||||
|
return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Send(int requestId, string data, int weight)
|
||||||
|
{
|
||||||
|
if (!Connected)
|
||||||
|
throw new Exception("Socket not connected");
|
||||||
|
|
||||||
|
OnRequestSent?.Invoke(requestId);
|
||||||
|
OnMessageSend?.Invoke(data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CloseAsync()
|
||||||
|
{
|
||||||
|
Connected = false;
|
||||||
|
return Task.FromResult(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeClose()
|
||||||
|
{
|
||||||
|
Connected = false;
|
||||||
|
OnClose?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeOpen()
|
||||||
|
{
|
||||||
|
OnOpen?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeMessage(string data)
|
||||||
|
{
|
||||||
|
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(data))).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InvokeMessage<T>(T data)
|
||||||
|
{
|
||||||
|
OnStreamMessage?.Invoke(WebSocketMessageType.Text, new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(data)))).Wait();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ReconnectAsync() => throw new NotImplementedException();
|
||||||
|
public void Dispose() { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing.Implementations
|
||||||
|
{
|
||||||
|
internal class TestWebsocketFactory : IWebsocketFactory
|
||||||
|
{
|
||||||
|
private readonly TestSocket _socket;
|
||||||
|
public TestWebsocketFactory(TestSocket socket)
|
||||||
|
{
|
||||||
|
_socket = socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IWebsocket CreateWebsocket(ILogger logger, WebSocketParameters parameters) => _socket;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for executing REST API integration tests
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient">Client type</typeparam>
|
||||||
|
public abstract class RestIntergrationTest<TClient>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get a client instance
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public abstract TClient GetClient(ILoggerFactory loggerFactory);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the test should be run. By default integration tests aren't executed, can be set to true to force execution.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool Run { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether API credentials are provided and thus authenticated calls can be executed. Should be set in the GetClient implementation.
|
||||||
|
/// </summary>
|
||||||
|
public bool Authenticated { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a client
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected TClient CreateClient()
|
||||||
|
{
|
||||||
|
var fact = new LoggerFactory();
|
||||||
|
fact.AddProvider(new TraceLoggerProvider());
|
||||||
|
return GetClient(fact);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if integration tests should be executed
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected bool ShouldRun()
|
||||||
|
{
|
||||||
|
var integrationTests = Environment.GetEnvironmentVariable("INTEGRATION");
|
||||||
|
if (!Run && integrationTests != "1")
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a REST endpoint call and check for any errors or warnings.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type of response</typeparam>
|
||||||
|
/// <param name="expression">The call expression</param>
|
||||||
|
/// <param name="authRequest">Whether this is an authenticated request</param>
|
||||||
|
public async Task RunAndCheckResult<T>(Expression<Func<TClient, Task<WebCallResult<T>>>> expression, bool authRequest)
|
||||||
|
{
|
||||||
|
if (!ShouldRun())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var client = CreateClient();
|
||||||
|
|
||||||
|
var expressionBody = (MethodCallExpression)expression.Body;
|
||||||
|
if (authRequest && !Authenticated)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Skipping {expressionBody.Method.Name}, not authenticated");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
WebCallResult<T> result;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result = await expression.Compile().Invoke(client).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception($"Method {expressionBody.Method.Name} threw an exception: " + ex.ToLogString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.Success)
|
||||||
|
throw new Exception($"Method {expressionBody.Method.Name} returned error: " + result.Error);
|
||||||
|
|
||||||
|
Debug.WriteLine($"{expressionBody.Method.Name} {result}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Testing.Comparers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Testing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validator for REST requests, comparing path, http method, authentication and response parsing
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TClient">The Rest client</typeparam>
|
||||||
|
public class RestRequestValidator<TClient> where TClient : BaseRestClient
|
||||||
|
{
|
||||||
|
private readonly TClient _client;
|
||||||
|
private readonly Func<WebCallResult, bool> _isAuthenticated;
|
||||||
|
private readonly string _folder;
|
||||||
|
private readonly string _baseAddress;
|
||||||
|
private readonly string? _nestedPropertyForCompare;
|
||||||
|
private readonly bool _stjCompare;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="client">Client to test</param>
|
||||||
|
/// <param name="folder">Folder for json test values</param>
|
||||||
|
/// <param name="baseAddress">The base address that is expected</param>
|
||||||
|
/// <param name="isAuthenticated">Func for checking if the request is authenticated</param>
|
||||||
|
/// <param name="nestedPropertyForCompare">Property to use for compare</param>
|
||||||
|
/// <param name="stjCompare">Use System.Text.Json for comparing</param>
|
||||||
|
public RestRequestValidator(TClient client, string folder, string baseAddress, Func<WebCallResult, bool> isAuthenticated, string? nestedPropertyForCompare = null, bool stjCompare = true)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_folder = folder;
|
||||||
|
_baseAddress = baseAddress;
|
||||||
|
_nestedPropertyForCompare = nestedPropertyForCompare;
|
||||||
|
_isAuthenticated = isAuthenticated;
|
||||||
|
_stjCompare = stjCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||||
|
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public Task ValidateAsync<TResponse>(
|
||||||
|
Func<TClient, Task<WebCallResult<TResponse>>> methodInvoke,
|
||||||
|
string name,
|
||||||
|
string? nestedJsonProperty = null,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool useSingleArrayItem = false,
|
||||||
|
bool skipResponseValidation = false)
|
||||||
|
=> ValidateAsync<TResponse, TResponse>(methodInvoke, name, nestedJsonProperty, ignoreProperties, useSingleArrayItem, skipResponseValidation);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse">Expected response type</typeparam>
|
||||||
|
/// <typeparam name="TActualResponse">The concrete response type</typeparam>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <param name="nestedJsonProperty">Use nested json property for compare</param>
|
||||||
|
/// <param name="ignoreProperties">Ignore certain properties</param>
|
||||||
|
/// <param name="useSingleArrayItem">Use the first item of an json array response</param>
|
||||||
|
/// <param name="skipResponseValidation">Whether to skip the response model validation</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task ValidateAsync<TResponse, TActualResponse>(
|
||||||
|
Func<TClient, Task<WebCallResult<TResponse>>> methodInvoke,
|
||||||
|
string name,
|
||||||
|
string? nestedJsonProperty = null,
|
||||||
|
List<string>? ignoreProperties = null,
|
||||||
|
bool useSingleArrayItem = false,
|
||||||
|
bool skipResponseValidation = false) where TActualResponse : TResponse
|
||||||
|
{
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||||
|
FileStream file;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new Exception($"Response file not found for {name}: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[file.Length];
|
||||||
|
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
|
using var reader = new StringReader(data);
|
||||||
|
var expectedMethod = reader.ReadLine();
|
||||||
|
var expectedPath = reader.ReadLine();
|
||||||
|
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||||
|
var response = reader.ReadToEnd();
|
||||||
|
|
||||||
|
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||||
|
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Check request/response properties
|
||||||
|
if (result.Error != null)
|
||||||
|
throw new Exception(name + " returned error " + result.Error);
|
||||||
|
if (_isAuthenticated(result.AsDataless()) != expectedAuth)
|
||||||
|
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result.AsDataless())}");
|
||||||
|
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
||||||
|
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||||
|
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||||
|
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||||
|
|
||||||
|
if (!skipResponseValidation)
|
||||||
|
{
|
||||||
|
// Check response data
|
||||||
|
object responseData = (TActualResponse)result.Data!;
|
||||||
|
if (_stjCompare == true)
|
||||||
|
SystemTextJsonComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem);
|
||||||
|
else
|
||||||
|
JsonNetComparer.CompareData(name, responseData, response, nestedJsonProperty ?? _nestedPropertyForCompare, ignoreProperties, useSingleArrayItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validate a request
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="methodInvoke">Method invocation</param>
|
||||||
|
/// <param name="name">Method name for looking up json test values</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
/// <exception cref="Exception"></exception>
|
||||||
|
public async Task ValidateAsync(
|
||||||
|
Func<TClient, Task<WebCallResult>> methodInvoke,
|
||||||
|
string name)
|
||||||
|
{
|
||||||
|
var listener = new EnumValueTraceListener();
|
||||||
|
Trace.Listeners.Add(listener);
|
||||||
|
|
||||||
|
var path = Directory.GetParent(Environment.CurrentDirectory)!.Parent!.Parent!.FullName;
|
||||||
|
FileStream file;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
file = File.OpenRead(Path.Combine(path, _folder, $"{name}.txt"));
|
||||||
|
}
|
||||||
|
catch (FileNotFoundException)
|
||||||
|
{
|
||||||
|
throw new Exception($"Response file not found for {name}: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = new byte[file.Length];
|
||||||
|
await file.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
|
||||||
|
file.Close();
|
||||||
|
|
||||||
|
var data = Encoding.UTF8.GetString(buffer);
|
||||||
|
using var reader = new StringReader(data);
|
||||||
|
var expectedMethod = reader.ReadLine();
|
||||||
|
var expectedPath = reader.ReadLine();
|
||||||
|
var expectedAuth = bool.Parse(reader.ReadLine()!);
|
||||||
|
var response = reader.ReadToEnd();
|
||||||
|
|
||||||
|
TestHelpers.ConfigureRestClient(_client, response, System.Net.HttpStatusCode.OK);
|
||||||
|
var result = await methodInvoke(_client).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Check request/response properties
|
||||||
|
if (result.Error != null)
|
||||||
|
throw new Exception(name + " returned error " + result.Error);
|
||||||
|
if (_isAuthenticated(result) != expectedAuth)
|
||||||
|
throw new Exception(name + $" authentication not matched. Expected: {expectedAuth}, Actual: {_isAuthenticated(result)}");
|
||||||
|
if (result.RequestMethod != new HttpMethod(expectedMethod!))
|
||||||
|
throw new Exception(name + $" http method not matched. Expected {expectedMethod}, Actual: {result.RequestMethod}");
|
||||||
|
if (expectedPath != result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0])
|
||||||
|
throw new Exception(name + $" path not matched. Expected: {expectedPath}, Actual: {result.RequestUrl!.Replace(_baseAddress, "").Split(new char[] { '?' })[0]}");
|
||||||
|
|
||||||
|
Trace.Listeners.Remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user