mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 08:53:01 +00:00
Compare commits
287 Commits
6.1.4
...
9.0.0-beta4
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d6267da93 | |||
| 8def7f32af | |||
| ac295de9f6 | |||
| d412e0895e | |||
| 1f9e2b4fcb | |||
| b13cff5a95 | |||
| 4c050744ad | |||
| 3b15c35a02 | |||
| cd78dbf575 | |||
| a258532d6a | |||
| d2a87a1069 | |||
| e07f24ea0a | |||
| 024e8dcfe2 | |||
| 4bb5aae40a | |||
| dec94678ec | |||
| 1a49fc8251 | |||
| 29b0875960 | |||
| 976ccab1da | |||
| 02bbd37bb6 | |||
| 1bbbec7f2b | |||
| 0262f04913 | |||
| fd1ec17d72 | |||
| 4bdad7fe0c | |||
| 74f73dc790 | |||
| 0527a8a76e | |||
| c693eb8c02 | |||
| 3eb28c7fed | |||
| 618c4922b9 | |||
| c81b15861d | |||
| 4a5832cccd | |||
| 4e47c4cbdf | |||
| 2af1520ecc | |||
| cf397af3ab | |||
| a1479705e2 | |||
| 175e23f110 | |||
| 9b7019ded2 | |||
| 7904aa9ba7 | |||
| 3fe6db589f | |||
| 625dccbbe4 | |||
| e650771d16 | |||
| 3dad28b19d | |||
| 2b9fda985e | |||
| ff8759409b | |||
| 0d9627c13f | |||
| 0179fd7e2a | |||
| b8d0b0cf95 | |||
| 73c42bd452 | |||
| 290be7f5e0 | |||
| 0be1bb16e3 | |||
| 8605196390 | |||
| 460dd97537 | |||
| 1ec5984fad | |||
| 8260c2661d | |||
| 591c1dd405 | |||
| 0164cdfcc4 | |||
| 23a6cfff87 | |||
| fdcdb90a5f | |||
| 0b7107401f | |||
| 06add65354 | |||
| 773d288497 | |||
| fd4e8da938 | |||
| 271743b669 | |||
| f4797caf37 | |||
| 62c9769c72 | |||
| 92d7bc1e2e | |||
| 99e4f96f63 | |||
| 94d8afe149 | |||
| 90ad59c63a | |||
| c2273edfaa | |||
| 236283f4dd | |||
| b66f12ff75 | |||
| 0403384beb | |||
| 7d7bc35869 | |||
| 48797038be | |||
| d21792d04c | |||
| 8414e9d94f | |||
| ab0243445d | |||
| f2cf70b02f | |||
| 9ff417bba8 | |||
| 6b43d08a4d | |||
| 39bf7fe9b9 | |||
| b5893c3b60 | |||
| 15657ba683 | |||
| 1aed9f0c67 | |||
| 17f1560310 | |||
| 41de0a3150 | |||
| 3e410be611 | |||
| be75449e4a | |||
| b1b05c8f6b | |||
| a0e588c3de | |||
| 9e86a08327 | |||
| ed007b5272 | |||
| bdd5526244 | |||
| ce35e30688 | |||
| b40f72b1b0 | |||
| 31a6cf285b | |||
| 1842f4fda0 | |||
| 7a58902ab6 | |||
| 3cb91296ca | |||
| 130ed40580 | |||
| 94cb2caf0b | |||
| 917d060827 | |||
| c58bc2be07 | |||
| ff3356e2b4 | |||
| 79434c7be5 | |||
| 168dabc11f | |||
| 71ee263683 | |||
| 7239b9c289 | |||
| 84d36544e4 | |||
| a71f57ae7f | |||
| 6e5bcd5e9a | |||
| 4131e563c3 | |||
| 613766dbca | |||
| 23b07d709e | |||
| bbbdac2fd3 | |||
| c614b7869c | |||
| 1f31e4a9d7 | |||
| 6cb6cd6b11 | |||
| 17ffec329f | |||
| 7a3927ef49 | |||
| c1b0437c93 | |||
| 23e947f258 | |||
| b8686d60b9 | |||
| 5d3de52da6 | |||
| fee18fd183 | |||
| 3a43d461a3 | |||
| 5f409efad3 | |||
| cc1f0796fe | |||
| b1cd9b5412 | |||
| 42003a0247 | |||
| 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 | |||
| bb7ba5ea49 | |||
| 747c986644 | |||
| d88087c8ac | |||
| 968bdc330e | |||
| 7b49562c1d | |||
| 24ba60da47 | |||
| ed5a07fbdb | |||
| 3d3a9b88e7 | |||
| b2f9d5753e | |||
| de46c7bd1d | |||
| 5b11d94f73 | |||
| 6f915a3739 | |||
| d5c4b1bd01 | |||
| 1b1961db00 | |||
| 2dbd5be924 | |||
| 24c40d2dc6 | |||
| 5ef6feb996 | |||
| 85dad6f6f0 | |||
| 3cdcf0d9be | |||
| b90a0a71e9 | |||
| e62786a70f | |||
| 87722f2d28 | |||
| a86276f18d | |||
| f432a66016 | |||
| 9e2910d2ec | |||
| 46fbc1eb85 | |||
| f397d3ab94 | |||
| 81a2da1f3f | |||
| af3303c7b8 | |||
| 8ddd9ecf22 | |||
| de72fe4fb9 | |||
| 108c8fc183 | |||
| e86713e949 | |||
| db9fba4cf2 | |||
| 926802d953 | |||
| 87f5e12b60 | |||
| 034eb83bae | |||
| 7f29275851 | |||
| 2fb3442800 | |||
| 462c857bba | |||
| 61aa589cda | |||
| 27704bf090 | |||
| 4c899861b1 | |||
| 0cff678c2d | |||
| d18514d73c | |||
| 84736cac3f | |||
| cda1cce495 | |||
| fe3a0afd6c | |||
| d533557324 | |||
| d91755dff5 | |||
| 27d49a6093 | |||
| ad1bdd9a3f | |||
| 4f2d7abc7e | |||
| ef9de5e338 | |||
| 8b513e51b9 | |||
| d43b38a23a | |||
| 0987c0f9d1 | |||
| e2dde77023 | |||
| 104ac7caad | |||
| 8788dd3deb | |||
| f64cc5e9cf | |||
| 75d1bbc6e8 | |||
| b621aa7e65 | |||
| 9783108695 | |||
| 6ba32fe280 | |||
| f75cc75bbc | |||
| 2109b65a8e | |||
| a472751638 | |||
| f08ed16f2a | |||
| 212d457a6a |
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v1
|
||||
with:
|
||||
dotnet-version: 6.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
@@ -287,5 +287,3 @@ __pycache__/
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
CryptoExchange.Net/CryptoExchange.Net.xml
|
||||
/Docs/*
|
||||
Docs/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -24,8 +25,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result1 = await waiter1;
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(result2);
|
||||
Assert.That(result1);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -39,8 +40,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result1 = await waiter1;
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(result2);
|
||||
Assert.That(result1);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -55,14 +56,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
Assert.That(result1);
|
||||
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
|
||||
evnt.Set();
|
||||
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result2);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -75,13 +76,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.True(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
Assert.That(result1);
|
||||
Assert.That(waiter2.Status != TaskStatus.RanToCompletion);
|
||||
evnt.Set();
|
||||
|
||||
var result2 = await waiter2;
|
||||
|
||||
Assert.True(result2);
|
||||
Assert.That(result2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -105,12 +106,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
for(var i = 1; i <= 10; i++)
|
||||
{
|
||||
evnt.Set();
|
||||
Assert.AreEqual(10 - i, waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||
await Task.Delay(1); // Wait for the continuation.
|
||||
Assert.That(10 - i == waiters.Count(w => w.Status != TaskStatus.RanToCompletion));
|
||||
}
|
||||
|
||||
await resultsWaiter;
|
||||
|
||||
Assert.AreEqual(10, results.Count(r => r));
|
||||
Assert.That(10 == results.Count(r => r));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -124,7 +126,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.True(result1);
|
||||
Assert.That(result1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -134,9 +136,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var waiter1 = evnt.WaitAsync(TimeSpan.FromMilliseconds(100));
|
||||
|
||||
var result1 = await waiter1;
|
||||
var result1 = await waiter1;
|
||||
|
||||
Assert.False(result1);
|
||||
ClassicAssert.False(result1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -21,7 +22,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123}");
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -34,8 +35,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||
@@ -48,7 +49,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void AppendPathTests(string baseUrl, string[] path, string expected)
|
||||
{
|
||||
var result = baseUrl.AppendPath(path);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -17,9 +18,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult(new ServerError("TestError"));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -27,9 +28,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult(null);
|
||||
|
||||
Assert.IsNull(result.Error);
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsTrue(result.Success);
|
||||
ClassicAssert.IsNull(result.Error);
|
||||
Assert.That(result);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -37,10 +38,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult<object>(new ServerError("TestError"));
|
||||
|
||||
Assert.AreEqual(result.Error.Message, "TestError");
|
||||
Assert.IsNull(result.Data);
|
||||
Assert.IsFalse(result);
|
||||
Assert.IsFalse(result.Success);
|
||||
ClassicAssert.AreSame(result.Error.Message, "TestError");
|
||||
ClassicAssert.IsNull(result.Data);
|
||||
ClassicAssert.IsFalse(result);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -48,10 +49,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var result = new CallResult<object>(new object());
|
||||
|
||||
Assert.IsNull(result.Error);
|
||||
Assert.IsNotNull(result.Data);
|
||||
Assert.IsTrue(result);
|
||||
Assert.IsTrue(result.Success);
|
||||
ClassicAssert.IsNull(result.Error);
|
||||
ClassicAssert.IsNotNull(result.Data);
|
||||
Assert.That(result);
|
||||
Assert.That(result.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -60,11 +61,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new CallResult<TestObjectResult>(new TestObjectResult());
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
Assert.IsNull(asResult.Error);
|
||||
Assert.IsNotNull(asResult.Data);
|
||||
Assert.IsTrue(asResult.Data is TestObject2);
|
||||
Assert.IsTrue(asResult);
|
||||
Assert.IsTrue(asResult.Success);
|
||||
ClassicAssert.IsNull(asResult.Error);
|
||||
ClassicAssert.IsNotNull(asResult.Data);
|
||||
Assert.That(asResult.Data is not null);
|
||||
Assert.That(asResult);
|
||||
Assert.That(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -73,11 +74,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.As<TestObject2>(default);
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -86,11 +87,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new CallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -99,11 +100,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = new WebCallResult<TestObjectResult>(new ServerError("TestError"));
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
ClassicAssert.AreSame(asResult.Error.Message, "TestError2");
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -115,23 +116,25 @@ namespace CryptoExchange.Net.UnitTests
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.AsError<TestObject2>(new ServerError("TestError2"));
|
||||
|
||||
Assert.IsNotNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.Error.Message, "TestError2");
|
||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||
Assert.IsNull(asResult.Data);
|
||||
Assert.IsFalse(asResult);
|
||||
Assert.IsFalse(asResult.Success);
|
||||
ClassicAssert.IsNotNull(asResult.Error);
|
||||
Assert.That(asResult.Error.Message == "TestError2");
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNull(asResult.Data);
|
||||
ClassicAssert.IsFalse(asResult);
|
||||
ClassicAssert.IsFalse(asResult.Success);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -143,22 +146,24 @@ namespace CryptoExchange.Net.UnitTests
|
||||
TimeSpan.FromSeconds(1),
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
"https://test.com/api",
|
||||
null,
|
||||
HttpMethod.Get,
|
||||
new List<KeyValuePair<string, IEnumerable<string>>>(),
|
||||
ResultDataSource.Server,
|
||||
new TestObjectResult(),
|
||||
null);
|
||||
var asResult = result.As<TestObject2>(result.Data.InnerData);
|
||||
|
||||
Assert.IsNull(asResult.Error);
|
||||
Assert.AreEqual(asResult.ResponseStatusCode, System.Net.HttpStatusCode.OK);
|
||||
Assert.AreEqual(asResult.ResponseTime, TimeSpan.FromSeconds(1));
|
||||
Assert.AreEqual(asResult.RequestUrl, "https://test.com/api");
|
||||
Assert.AreEqual(asResult.RequestMethod, HttpMethod.Get);
|
||||
Assert.IsNotNull(asResult.Data);
|
||||
Assert.IsTrue(asResult);
|
||||
Assert.IsTrue(asResult.Success);
|
||||
ClassicAssert.IsNull(asResult.Error);
|
||||
Assert.That(asResult.ResponseStatusCode == System.Net.HttpStatusCode.OK);
|
||||
Assert.That(asResult.ResponseTime == TimeSpan.FromSeconds(1));
|
||||
Assert.That(asResult.RequestUrl == "https://test.com/api");
|
||||
Assert.That(asResult.RequestMethod == HttpMethod.Get);
|
||||
ClassicAssert.IsNotNull(asResult.Data);
|
||||
Assert.That(asResult);
|
||||
Assert.That(asResult.Success);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0-preview-20211130-02"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0"></PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"></PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="NUnit" Version="4.2.2"></PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0"></PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
@@ -16,7 +18,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void ClampValueTests(decimal min, decimal max, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.ClampValue(min, max, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1, 1, 0.1, RoundingType.Down, 0.4, 0.4)]
|
||||
@@ -33,7 +35,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1, 1, 2, RoundingType.Closest, 0.4, 0.4)]
|
||||
@@ -48,7 +50,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void AdjustValuePrecisionTests(decimal min, decimal max, int? precision, RoundingType roundingType, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.AdjustValuePrecision(min, max, precision, roundingType, input);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(5, 0.1563158, 0.15631)]
|
||||
@@ -59,7 +61,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void RoundDownTests(int decimalPlaces, decimal input, decimal expected)
|
||||
{
|
||||
var result = ExchangeHelpers.RoundDown(input, decimalPlaces);
|
||||
Assert.AreEqual(expected, result);
|
||||
Assert.That(expected == result);
|
||||
}
|
||||
|
||||
[TestCase(0.1234560000, "0.123456")]
|
||||
@@ -67,7 +69,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void NormalizeTests(decimal input, string expected)
|
||||
{
|
||||
var result = ExchangeHelpers.Normalize(input);
|
||||
Assert.AreEqual(expected, result.ToString(CultureInfo.InvariantCulture));
|
||||
Assert.That(expected == result.ToString(CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCase("123", "BKR", 32, true, "BKRJK123")]
|
||||
[TestCase("123", "BKR", 32, false, "123")]
|
||||
[TestCase("123123123123123123123123123123", "BKR", 32, true, "123123123123123123123123123123")] // 30
|
||||
[TestCase("12312312312312312312312312312", "BKR", 32, true, "12312312312312312312312312312")] // 27
|
||||
[TestCase("123123123123123123123123123", "BKR", 32, true, "BKRJK123123123123123123123123123")] // 25
|
||||
[TestCase(null, "BKR", 32, true, null)]
|
||||
public void ApplyBrokerIdTests(string clientOrderId, string brokerId, int maxLength, bool allowValueAdjustement, string expected)
|
||||
{
|
||||
var result = LibraryHelpers.ApplyBrokerId(clientOrderId, brokerId, maxLength, allowValueAdjustement);
|
||||
|
||||
if (expected != null)
|
||||
Assert.That(result, Is.EqualTo(expected));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
-17
@@ -1,7 +1,9 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Converters.JsonNet;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -11,7 +13,7 @@ using System.Threading.Tasks;
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class ConverterTests
|
||||
public class JsonNetConverterTests
|
||||
{
|
||||
[TestCase("2021-05-12")]
|
||||
[TestCase("20210512")]
|
||||
@@ -20,12 +22,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("0.000000", true)]
|
||||
[TestCase("0", true)]
|
||||
[TestCase("", true)]
|
||||
[TestCase(" ", true)]
|
||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": \"{input}\" }}");
|
||||
Assert.AreEqual(output.Time, expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||
}
|
||||
|
||||
[TestCase(1620777600.000)]
|
||||
@@ -33,7 +37,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestDateTimeConverterDouble(double input)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.AreEqual(output.Time, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
@@ -44,7 +48,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.AreEqual(output.Time, expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
@@ -52,14 +56,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestDateTimeConverterFromSeconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromSeconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToSeconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600);
|
||||
Assert.That(output == 1620777600);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000)]
|
||||
@@ -67,49 +71,49 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMilliseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000);
|
||||
Assert.That(output == 1620777600000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000)]
|
||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMicroseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000000);
|
||||
Assert.That(output == 1620777600000000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000000)]
|
||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
||||
Assert.AreEqual(output, new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToNanoseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.AreEqual(output, 1620777600000000000);
|
||||
Assert.That(output == 1620777600000000000);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void TestDateTimeConverterNull()
|
||||
{
|
||||
var output = JsonConvert.DeserializeObject<TimeObject>($"{{ \"time\": null }}");
|
||||
Assert.AreEqual(output.Time, null);
|
||||
Assert.That(output.Time == null);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
@@ -120,7 +124,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.AreEqual(output, expected);
|
||||
Assert.That(output == expected);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
@@ -130,7 +134,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.AreEqual(output, expected);
|
||||
Assert.That(output == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
@@ -145,7 +149,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<EnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
@@ -160,7 +164,45 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<NotNullableEnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.AreEqual(output.Value, expected);
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", null)]
|
||||
public void TestBoolConverter(string value, bool? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<BoolObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", false)]
|
||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonConvert.DeserializeObject<NotNullableBoolObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,6 +222,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public TestEnum Value { get; set; }
|
||||
}
|
||||
|
||||
public class BoolObject
|
||||
{
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableBoolObject
|
||||
{
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EnumConverter))]
|
||||
public enum TestEnum
|
||||
{
|
||||
@@ -4,6 +4,7 @@ using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -49,9 +50,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
};
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.ReceiveWindow, TimeSpan.FromSeconds(10));
|
||||
Assert.AreEqual(options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.That(options.ReceiveWindow == TimeSpan.FromSeconds(10));
|
||||
Assert.That(options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.ApiCredentials.Secret == "456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -63,10 +64,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
options.Api2Options.ApiCredentials = new ApiCredentials("789", "101");
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Key.GetString(), "123");
|
||||
Assert.AreEqual(options.Api1Options.ApiCredentials.Secret.GetString(), "456");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Key.GetString(), "789");
|
||||
Assert.AreEqual(options.Api2Options.ApiCredentials.Secret.GetString(), "101");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Key == "123");
|
||||
Assert.That(options.Api1Options.ApiCredentials.Secret == "456");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Key == "789");
|
||||
Assert.That(options.Api2Options.ApiCredentials.Secret == "101");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -79,10 +80,10 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "444");
|
||||
Assert.That(authProvider1.GetKey() == "111");
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "333");
|
||||
Assert.That(authProvider2.GetSecret() == "444");
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -95,10 +96,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "111");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "222");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
Assert.That(authProvider1.GetKey() == "111");
|
||||
Assert.That(authProvider1.GetSecret() == "222");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -115,11 +120,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
var authProvider1 = (TestAuthProvider)client.Api1.AuthenticationProvider;
|
||||
var authProvider2 = (TestAuthProvider)client.Api2.AuthenticationProvider;
|
||||
Assert.AreEqual(authProvider1.GetKey(), "333");
|
||||
Assert.AreEqual(authProvider1.GetSecret(), "444");
|
||||
Assert.AreEqual(authProvider2.GetKey(), "123");
|
||||
Assert.AreEqual(authProvider2.GetSecret(), "456");
|
||||
Assert.AreEqual(client.Api2.BaseAddress, "https://localhost:123");
|
||||
Assert.That(authProvider1.GetKey() == "333");
|
||||
Assert.That(authProvider1.GetSecret() == "444");
|
||||
Assert.That(authProvider2.GetKey() == "123");
|
||||
Assert.That(authProvider2.GetSecret() == "456");
|
||||
Assert.That(client.Api2.BaseAddress == "https://localhost:123");
|
||||
|
||||
// Cleanup static values
|
||||
TestClientOptions.Default.ApiCredentials = null;
|
||||
TestClientOptions.Default.Api1Options.ApiCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +142,14 @@ namespace CryptoExchange.Net.UnitTests
|
||||
Environment = new TestEnvironment("test", "https://test.com")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestClientOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The default receive window for requests
|
||||
/// </summary>
|
||||
@@ -142,12 +159,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public RestApiOptions Api2Options { get; set; } = new RestApiOptions();
|
||||
|
||||
internal TestClientOptions Copy()
|
||||
internal TestClientOptions Set(TestClientOptions targetOptions)
|
||||
{
|
||||
var options = Copy<TestClientOptions>();
|
||||
options.Api1Options = Api1Options.Copy<RestApiOptions>();
|
||||
options.Api2Options = Api2Options.Copy<RestApiOptions>();
|
||||
return options;
|
||||
targetOptions = base.Set<TestClientOptions>(targetOptions);
|
||||
targetOptions.Api1Options = Api1Options.Set(targetOptions.Api1Options);
|
||||
targetOptions.Api2Options = Api2Options.Set(targetOptions.Api2Options);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using NUnit.Framework.Legacy;
|
||||
using CryptoExchange.Net.RateLimiting;
|
||||
using System.Net;
|
||||
using CryptoExchange.Net.RateLimiting.Guards;
|
||||
using CryptoExchange.Net.RateLimiting.Filters;
|
||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -30,8 +36,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(result.Success);
|
||||
Assert.IsTrue(TestHelpers.AreEqual(expected, result.Data));
|
||||
Assert.That(result.Success);
|
||||
Assert.That(TestHelpers.AreEqual(expected, result.Data));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -45,8 +51,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = client.Api1.Request<TestObject>().Result;
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -60,8 +66,8 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -75,11 +81,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = await client.Api1.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
Assert.IsTrue(result.Error is ServerError);
|
||||
Assert.IsTrue(result.Error.Message.Contains("Invalid request"));
|
||||
Assert.IsTrue(result.Error.Message.Contains("123"));
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
Assert.That(result.Error is ServerError);
|
||||
Assert.That(result.Error.Message.Contains("Invalid request"));
|
||||
Assert.That(result.Error.Message.Contains("123"));
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -93,11 +99,11 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var result = await client.Api2.Request<TestObject>();
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(result.Success);
|
||||
Assert.IsTrue(result.Error != null);
|
||||
Assert.IsTrue(result.Error is ServerError);
|
||||
Assert.IsTrue(result.Error.Code == 123);
|
||||
Assert.IsTrue(result.Error.Message == "Invalid request");
|
||||
ClassicAssert.IsFalse(result.Success);
|
||||
Assert.That(result.Error != null);
|
||||
Assert.That(result.Error is ServerError);
|
||||
Assert.That(result.Error.Code == 123);
|
||||
Assert.That(result.Error.Message == "Invalid request");
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -106,15 +112,15 @@ namespace CryptoExchange.Net.UnitTests
|
||||
// arrange
|
||||
// act
|
||||
var options = new TestClientOptions();
|
||||
options.Api1Options.RateLimiters = new List<IRateLimiter> { new RateLimiter() };
|
||||
options.Api1Options.RateLimitingBehaviour = RateLimitingBehaviour.Fail;
|
||||
options.Api1Options.TimestampRecalculationInterval = TimeSpan.FromMinutes(10);
|
||||
options.Api1Options.OutputOriginalData = true;
|
||||
options.RequestTimeout = TimeSpan.FromMinutes(1);
|
||||
var client = new TestBaseClient(options);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimiters.Count == 1);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).Api1Options.RateLimitingBehaviour == RateLimitingBehaviour.Fail);
|
||||
Assert.IsTrue(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.TimestampRecalculationInterval == TimeSpan.FromMinutes(10));
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).Api1Options.OutputOriginalData == true);
|
||||
Assert.That(((TestClientOptions)client.ClientOptions).RequestTimeout == TimeSpan.FromMinutes(1));
|
||||
}
|
||||
|
||||
[TestCase("GET", HttpMethodParameterPosition.InUri)] // No need to test InBody for GET since thats not valid
|
||||
@@ -145,13 +151,13 @@ namespace CryptoExchange.Net.UnitTests
|
||||
});
|
||||
|
||||
// assert
|
||||
Assert.AreEqual(request.Method, new HttpMethod(method));
|
||||
Assert.AreEqual(request.Content?.Contains("TestParam1") == true, pos == HttpMethodParameterPosition.InBody);
|
||||
Assert.AreEqual(request.Uri.ToString().Contains("TestParam1"), pos == HttpMethodParameterPosition.InUri);
|
||||
Assert.AreEqual(request.Content?.Contains("TestParam2") == true, pos == HttpMethodParameterPosition.InBody);
|
||||
Assert.AreEqual(request.Uri.ToString().Contains("TestParam2"), pos == HttpMethodParameterPosition.InUri);
|
||||
Assert.AreEqual(request.GetHeaders().First().Key, "TestHeader");
|
||||
Assert.IsTrue(request.GetHeaders().First().Value.Contains("123"));
|
||||
Assert.That(request.Method == new HttpMethod(method));
|
||||
Assert.That((request.Content?.Contains("TestParam1") == true) == (pos == HttpMethodParameterPosition.InBody));
|
||||
Assert.That((request.Uri.ToString().Contains("TestParam1")) == (pos == HttpMethodParameterPosition.InUri));
|
||||
Assert.That((request.Content?.Contains("TestParam2") == true) == (pos == HttpMethodParameterPosition.InBody));
|
||||
Assert.That((request.Uri.ToString().Contains("TestParam2")) == (pos == HttpMethodParameterPosition.InUri));
|
||||
Assert.That(request.GetHeaders().First().Key == "TestHeader");
|
||||
Assert.That(request.GetHeaders().First().Value.Contains("123"));
|
||||
}
|
||||
|
||||
|
||||
@@ -161,18 +167,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task PartialEndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||
|
||||
var triggered = false;
|
||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||
var requestDefinition = new RequestDefinition("/sapi/v1/system/status", HttpMethod.Get);
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests? result1.Data > 1 : result1.Data == 0);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests? triggered : !triggered);
|
||||
}
|
||||
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/v1/system/status", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test1", true)]
|
||||
@@ -182,29 +192,40 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/", true)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint, bool expectLimiting)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimiting ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("/sapi/", "/sapi/", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("/sapi/test", "/sapi/test123", false)]
|
||||
[TestCase("/sapi/test", "/sapi/", false)]
|
||||
public async Task PartialEndpointRateLimiterEndpoints(string endpoint1, string endpoint2, bool expectLimiting)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddPartialEndpointLimit("/sapi/", 1, TimeSpan.FromSeconds(0.1), countPerEndpoint: true);
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimiting ? result2.Data > 0 : result2.Data == 0);
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimiting ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase(1, 0.1)]
|
||||
@@ -213,18 +234,22 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase(1, 2)]
|
||||
public async Task EndpointRateLimiterBasics(int requests, double perSeconds)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", requests, TimeSpan.FromSeconds(perSeconds));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new PathStartFilter("/sapi/test"), requests, TimeSpan.FromSeconds(perSeconds), RateLimitWindowType.Fixed));
|
||||
|
||||
bool triggered = false;
|
||||
rateLimiter.RateLimitTriggered += (x) => { triggered = true; };
|
||||
var requestDefinition = new RequestDefinition("/sapi/test", HttpMethod.Get);
|
||||
|
||||
for (var i = 0; i < requests + 1; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(i == requests ? result1.Data > 1 : result1.Data == 0);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(i == requests ? triggered : !triggered);
|
||||
}
|
||||
|
||||
triggered = false;
|
||||
await Task.Delay((int)Math.Round(perSeconds * 1000) + 10);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), "/sapi/test", HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result2.Data == 0);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(!triggered);
|
||||
}
|
||||
|
||||
[TestCase("/", false)]
|
||||
@@ -232,14 +257,18 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test/123", false)]
|
||||
public async Task EndpointRateLimiterEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit("/sapi/test", 1, TimeSpan.FromSeconds(0.1));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathFilter("/sapi/test"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,47 +278,41 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/sapi/test23", false)]
|
||||
public async Task EndpointRateLimiterMultipleEndpoints(string endpoint, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddEndpointLimit(new[] { "/sapi/test", "/sapi/test2" }, 1, TimeSpan.FromSeconds(0.1));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerEndpoint, new ExactPathsFilter(new[] { "/sapi/test", "/sapi/test2" }), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition = new RequestDefinition(endpoint, HttpMethod.Get);
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
for (var i = 0; i < 2; i++)
|
||||
{
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
bool expected = i == 1 ? (expectLimited ? result1.Data > 1 : result1.Data == 0) : result1.Data == 0;
|
||||
Assert.IsTrue(expected);
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
bool expected = i == 1 ? (expectLimited ? evnt.DelayTime > TimeSpan.Zero : evnt == null) : evnt == null;
|
||||
Assert.That(expected);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, true, true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, true, true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, true, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, true, false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, true, false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, true, false)]
|
||||
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, true, false, true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", true, true, false, false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true, false, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, true, false, true)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false, true, false, false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", true, false, false, false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false, false, false, true)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool signed1, bool signed2, bool onlyForSignedRequests, bool expectLimited)
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test", true)]
|
||||
[TestCase("123", "456", "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase("123", "123", "/sapi/test", "/sapi/test2", true)]
|
||||
[TestCase("123", "123", "/sapi/test2", "/sapi/test", true)]
|
||||
[TestCase(null, "123", "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase("123", null, "/sapi/test", "/sapi/test", false)]
|
||||
[TestCase(null, null, "/sapi/test", "/sapi/test", false)]
|
||||
public async Task ApiKeyRateLimiterBasics(string key1, string key2, string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(1, TimeSpan.FromSeconds(0.1), onlyForSignedRequests, false);
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerApiKey, new AuthenticatedEndpointFilter(true), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Sliding));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get) { Authenticated = key1 != null };
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = key2 != null };
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, signed1, key1?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, signed2, key2?.ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", key1, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", key2, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", "/sapi/test", true)]
|
||||
@@ -297,29 +320,70 @@ namespace CryptoExchange.Net.UnitTests
|
||||
[TestCase("/", "/sapi/test2", true)]
|
||||
public async Task TotalRateLimiterBasics(string endpoint1, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, Array.Empty<IGuardFilter>(), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint1, HttpMethod.Get, false, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint2, HttpMethod.Get, true, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition2, "https://test.com", null, 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase("/sapi/test", true, true, true, false)]
|
||||
[TestCase("/sapi/test", false, true, true, false)]
|
||||
[TestCase("/sapi/test", false, true, false, true)]
|
||||
[TestCase("/sapi/test", true, true, false, true)]
|
||||
public async Task ApiKeyRateLimiterIgnores_TotalRateLimiter_IfSet(string endpoint, bool signed1, bool signed2, bool ignoreTotal, bool expectLimited)
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test", true)]
|
||||
[TestCase("https://test2.com", "/sapi/test", "https://test.com", "/sapi/test", false)]
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test2.com", "/sapi/test", false)]
|
||||
[TestCase("https://test.com", "/sapi/test", "https://test.com", "/sapi/test2", true)]
|
||||
public async Task HostRateLimiterBasics(string host1, string endpoint1, string host2, string endpoint2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimiter();
|
||||
rateLimiter.AddApiKeyLimit(100, TimeSpan.FromSeconds(0.1), true, ignoreTotal);
|
||||
rateLimiter.AddTotalRateLimit(1, TimeSpan.FromSeconds(0.1));
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new HostFilter("https://test.com"), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
var requestDefinition1 = new RequestDefinition(endpoint1, HttpMethod.Get);
|
||||
var requestDefinition2 = new RequestDefinition(endpoint2, HttpMethod.Get) { Authenticated = true };
|
||||
|
||||
var result1 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed1, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
var result2 = await rateLimiter.LimitRequestAsync(new TraceLogger(), endpoint, HttpMethod.Get, signed2, "123".ToSecureString(), RateLimitingBehaviour.Wait, 1, default);
|
||||
Assert.IsTrue(result1.Data == 0);
|
||||
Assert.IsTrue(expectLimited ? result2.Data > 0 : result2.Data == 0);
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Request, requestDefinition1, host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[TestCase("https://test.com", "https://test.com", true)]
|
||||
[TestCase("https://test2.com", "https://test.com", false)]
|
||||
[TestCase("https://test.com", "https://test2.com", false)]
|
||||
public async Task ConnectionRateLimiterBasics(string host1, string host2, bool expectLimited)
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(0.1), RateLimitWindowType.Fixed));
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host1, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(evnt == null);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), host2, "123", 1, RateLimitingBehaviour.Wait, null, default);
|
||||
Assert.That(expectLimited ? evnt != null : evnt == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task ConnectionRateLimiterCancel()
|
||||
{
|
||||
var rateLimiter = new RateLimitGate("Test");
|
||||
rateLimiter.AddGuard(new RateLimitGuard(RateLimitGuard.PerHost, new LimitItemTypeFilter(RateLimitItemType.Connection), 1, TimeSpan.FromSeconds(10), RateLimitWindowType.Fixed));
|
||||
|
||||
RateLimitEvent evnt = null;
|
||||
rateLimiter.RateLimitTriggered += (x) => { evnt = x; };
|
||||
var ct = new CancellationTokenSource(TimeSpan.FromSeconds(0.2));
|
||||
|
||||
var result1 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
var result2 = await rateLimiter.ProcessAsync(new TraceLogger(), 1, RateLimitItemType.Connection, new RequestDefinition("1", HttpMethod.Get), "https://test.com", "123", 1, RateLimitingBehaviour.Wait, null, ct.Token);
|
||||
Assert.That(result2.Error, Is.TypeOf<CancellationRequestedError>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Moq;
|
||||
using Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
@@ -23,10 +29,9 @@ namespace CryptoExchange.Net.UnitTests
|
||||
options.SubOptions.MaxSocketConnections = 1;
|
||||
});
|
||||
|
||||
|
||||
//assert
|
||||
Assert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
Assert.AreEqual(1, client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
@@ -42,7 +47,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
||||
|
||||
//assert
|
||||
Assert.IsTrue(connectResult.Success == canConnect);
|
||||
Assert.That(connectResult.Success == canConnect);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -53,25 +58,26 @@ namespace CryptoExchange.Net.UnitTests
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
JToken result = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
{
|
||||
result = messageEvent.JsonData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
Dictionary<string, string> result = null;
|
||||
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
|
||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
{
|
||||
result = messageEvent.Data;
|
||||
rstEvent.Set();
|
||||
});
|
||||
sub.AddSubscription(subObj);
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
||||
rstEvent.WaitOne(1000);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue((int)result["property"] == 123);
|
||||
Assert.That(result["property"] == "123");
|
||||
}
|
||||
|
||||
[TestCase(false)]
|
||||
@@ -79,52 +85,56 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(options => {
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
options.SubOptions.OutputOriginalData = enabled;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.ShouldReconnect = true;
|
||||
socket.CanConnect = true;
|
||||
socket.DisconnectTime = DateTime.UtcNow;
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
var rstEvent = new ManualResetEvent(false);
|
||||
string original = null;
|
||||
sub.AddSubscription(SocketSubscription.CreateForIdentifier(10, "TestHandler", true, false, (messageEvent) =>
|
||||
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||
{
|
||||
original = messageEvent.OriginalData;
|
||||
rstEvent.Set();
|
||||
}));
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
});
|
||||
sub.AddSubscription(subObj);
|
||||
var msgToSend = JsonConvert.SerializeObject(new { topic = "topic", action = "update", property = 123 });
|
||||
|
||||
// act
|
||||
socket.InvokeMessage("{\"property\": 123}");
|
||||
socket.InvokeMessage(msgToSend);
|
||||
rstEvent.WaitOne(1000);
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(original == (enabled ? "{\"property\": 123}" : null));
|
||||
Assert.That(original == (enabled ? msgToSend : null));
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
||||
{
|
||||
// arrange
|
||||
var client = new TestSocketClient(options => {
|
||||
var client = new TestSocketClient(options =>
|
||||
{
|
||||
options.ReconnectInterval = TimeSpan.Zero;
|
||||
});
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
||||
client.SubClient.ConnectSocketSub(sub);
|
||||
var us = SocketSubscription.CreateForIdentifier(10, "Test", true, false, (e) => { });
|
||||
var ups = new UpdateSubscription(sub, us);
|
||||
sub.AddSubscription(us);
|
||||
|
||||
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
var ups = new UpdateSubscription(sub, subscription);
|
||||
sub.AddSubscription(subscription);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAsync(ups).Wait();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(socket.Connected == false);
|
||||
Assert.That(socket.Connected == false);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
@@ -140,19 +150,20 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
||||
client.SubClient.ConnectSocketSub(sub1);
|
||||
client.SubClient.ConnectSocketSub(sub2);
|
||||
var us1 = SocketSubscription.CreateForIdentifier(10, "Test1", true, false, (e) => { });
|
||||
var us2 = SocketSubscription.CreateForIdentifier(11, "Test2", true, false, (e) => { });
|
||||
sub1.AddSubscription(us1);
|
||||
sub2.AddSubscription(us2);
|
||||
var ups1 = new UpdateSubscription(sub1, us1);
|
||||
var ups2 = new UpdateSubscription(sub2, us2);
|
||||
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||
|
||||
sub1.AddSubscription(subscription1);
|
||||
sub2.AddSubscription(subscription2);
|
||||
var ups1 = new UpdateSubscription(sub1, subscription1);
|
||||
var ups2 = new UpdateSubscription(sub2, subscription2);
|
||||
|
||||
// act
|
||||
client.UnsubscribeAllAsync().Wait();
|
||||
|
||||
// assert
|
||||
Assert.IsTrue(socket1.Connected == false);
|
||||
Assert.IsTrue(socket2.Connected == false);
|
||||
Assert.That(socket1.Connected == false);
|
||||
Assert.That(socket2.Connected == false);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
@@ -168,7 +179,53 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||
|
||||
// assert
|
||||
Assert.IsFalse(connectResult.Success);
|
||||
ClassicAssert.IsFalse(connectResult.Success);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
||||
{
|
||||
// arrange
|
||||
var channel = "trade_btcusd";
|
||||
var client = new TestSocketClient(opt =>
|
||||
{
|
||||
opt.OutputOriginalData = true;
|
||||
opt.SocketSubscriptionsCombineTarget = 1;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
||||
|
||||
// act
|
||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "error" }));
|
||||
await sub;
|
||||
|
||||
// assert
|
||||
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public async Task SuccessResponse_Should_ConfirmSubscription()
|
||||
{
|
||||
// arrange
|
||||
var channel = "trade_btcusd";
|
||||
var client = new TestSocketClient(opt =>
|
||||
{
|
||||
opt.OutputOriginalData = true;
|
||||
opt.SocketSubscriptionsCombineTarget = 1;
|
||||
});
|
||||
var socket = client.CreateSocket();
|
||||
socket.CanConnect = true;
|
||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
||||
|
||||
// act
|
||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||
socket.InvokeMessage(JsonConvert.SerializeObject(new { channel, action = "subscribe", status = "confirmed" }));
|
||||
await sub;
|
||||
|
||||
// assert
|
||||
Assert.That(client.SubClient.TestSubscription.Confirmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,22 +5,23 @@ using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.OrderBook;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework.Legacy;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class SymbolOrderBookTests
|
||||
{
|
||||
private static OrderBookOptions defaultOrderBookOptions = new OrderBookOptions();
|
||||
private static readonly OrderBookOptions _defaultOrderBookOptions = new OrderBookOptions();
|
||||
|
||||
private class TestableSymbolOrderBook : SymbolOrderBook
|
||||
{
|
||||
public TestableSymbolOrderBook() : base(null, "Test", "BTC/USD")
|
||||
public TestableSymbolOrderBook() : base(null, "Test", "Test", "BTC/USD")
|
||||
{
|
||||
Initialize(defaultOrderBookOptions);
|
||||
Initialize(_defaultOrderBookOptions);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,31 +57,31 @@ namespace CryptoExchange.Net.UnitTests
|
||||
public void GivenEmptyBidList_WhenBestBid_ThenEmptySymbolOrderBookEntry()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void GivenEmptyAskList_WhenBestAsk_ThenEmptySymbolOrderBookEntry()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestBid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestAsk.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestBid);
|
||||
Assert.That(0m == symbolOrderBook.BestBid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestAsk.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
public void GivenEmptyBidAndAskList_WhenBestOffers_ThenEmptySymbolOrderBookEntries()
|
||||
{
|
||||
var symbolOrderBook = new TestableSymbolOrderBook();
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers);
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
||||
Assert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Bid.Quantity);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Price);
|
||||
Assert.AreEqual(0m, symbolOrderBook.BestOffers.Ask.Quantity);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Bid);
|
||||
ClassicAssert.IsNotNull(symbolOrderBook.BestOffers.Ask);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Bid.Quantity);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Price);
|
||||
Assert.That(0m == symbolOrderBook.BestOffers.Ask.Quantity);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -103,12 +104,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var resultBids2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Bid);
|
||||
var resultAsks2 = orderbook.CalculateAverageFillPrice(1.5m, OrderBookEntryType.Ask);
|
||||
|
||||
Assert.True(resultBids.Success);
|
||||
Assert.True(resultAsks.Success);
|
||||
Assert.AreEqual(1.05m, resultBids.Data);
|
||||
Assert.AreEqual(1.25m, resultAsks.Data);
|
||||
Assert.AreEqual(1.06666667m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23333333m, resultAsks2.Data);
|
||||
Assert.That(resultBids.Success);
|
||||
Assert.That(resultAsks.Success);
|
||||
Assert.That(1.05m == resultBids.Data);
|
||||
Assert.That(1.25m == resultAsks.Data);
|
||||
Assert.That(1.06666667m == resultBids2.Data);
|
||||
Assert.That(1.23333333m == resultAsks2.Data);
|
||||
}
|
||||
|
||||
[TestCase]
|
||||
@@ -131,12 +132,12 @@ namespace CryptoExchange.Net.UnitTests
|
||||
var resultBids2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Bid);
|
||||
var resultAsks2 = orderbook.CalculateTradableAmount(1.5m, OrderBookEntryType.Ask);
|
||||
|
||||
Assert.True(resultBids.Success);
|
||||
Assert.True(resultAsks.Success);
|
||||
Assert.AreEqual(1.9m, resultBids.Data);
|
||||
Assert.AreEqual(1.61538462m, resultAsks.Data);
|
||||
Assert.AreEqual(1.4m, resultBids2.Data);
|
||||
Assert.AreEqual(1.23076923m, resultAsks2.Data);
|
||||
Assert.That(resultBids.Success);
|
||||
Assert.That(resultAsks.Success);
|
||||
Assert.That(1.9m == resultBids.Data);
|
||||
Assert.That(1.61538462m == resultAsks.Data);
|
||||
Assert.That(1.4m == resultBids2.Data);
|
||||
Assert.That(1.23076923m == resultAsks2.Data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using System.Text.Json;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using NUnit.Framework.Legacy;
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Testing.Comparers;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
[TestFixture()]
|
||||
public class SystemTextJsonConverterTests
|
||||
{
|
||||
[TestCase("2021-05-12")]
|
||||
[TestCase("20210512")]
|
||||
[TestCase("210512")]
|
||||
[TestCase("1620777600.000")]
|
||||
[TestCase("1620777600000")]
|
||||
[TestCase("2021-05-12T00:00:00.000Z")]
|
||||
[TestCase("2021-05-12T00:00:00.000000000Z")]
|
||||
[TestCase("0.000000", true)]
|
||||
[TestCase("0", true)]
|
||||
[TestCase("", true)]
|
||||
[TestCase(" ", true)]
|
||||
public void TestDateTimeConverterString(string input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": \"{input}\" }}");
|
||||
Assert.That(output.Time == (expectNull ? null: new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||
}
|
||||
|
||||
[TestCase(1620777600.000)]
|
||||
[TestCase(1620777600000d)]
|
||||
public void TestDateTimeConverterDouble(double input)
|
||||
{
|
||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.That(output.Time == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
[TestCase(1620777600000)]
|
||||
[TestCase(1620777600000000)]
|
||||
[TestCase(1620777600000000000)]
|
||||
[TestCase(0, true)]
|
||||
public void TestDateTimeConverterLong(long input, bool expectNull = false)
|
||||
{
|
||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": {input} }}");
|
||||
Assert.That(output.Time == (expectNull ? null : new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc)));
|
||||
}
|
||||
|
||||
[TestCase(1620777600)]
|
||||
[TestCase(1620777600.000)]
|
||||
public void TestDateTimeConverterFromSeconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromSeconds(input);
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToSeconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToSeconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == 1620777600);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000)]
|
||||
[TestCase(1620777600000.000)]
|
||||
public void TestDateTimeConverterFromMilliseconds(double input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMilliseconds(input);
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMilliseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMilliseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == 1620777600000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000)]
|
||||
public void TestDateTimeConverterFromMicroseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromMicroseconds(input);
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToMicroseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToMicroseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == 1620777600000000);
|
||||
}
|
||||
|
||||
[TestCase(1620777600000000000)]
|
||||
public void TestDateTimeConverterFromNanoseconds(long input)
|
||||
{
|
||||
var output = DateTimeConverter.ConvertFromNanoseconds(input);
|
||||
Assert.That(output == new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDateTimeConverterToNanoseconds()
|
||||
{
|
||||
var output = DateTimeConverter.ConvertToNanoseconds(new DateTime(2021, 05, 12, 0, 0, 0, DateTimeKind.Utc));
|
||||
Assert.That(output == 1620777600000000000);
|
||||
}
|
||||
|
||||
[TestCase()]
|
||||
public void TestDateTimeConverterNull()
|
||||
{
|
||||
var output = JsonSerializer.Deserialize<STJTimeObject>($"{{ \"time\": null }}");
|
||||
Assert.That(output.Time == null);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
[TestCase(TestEnum.Two, "2")]
|
||||
[TestCase(TestEnum.Three, "three")]
|
||||
[TestCase(TestEnum.Four, "Four")]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterNullableGetStringTests(TestEnum? value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.That(output == expected);
|
||||
}
|
||||
|
||||
[TestCase(TestEnum.One, "1")]
|
||||
[TestCase(TestEnum.Two, "2")]
|
||||
[TestCase(TestEnum.Three, "three")]
|
||||
[TestCase(TestEnum.Four, "Four")]
|
||||
public void TestEnumConverterGetStringTests(TestEnum value, string expected)
|
||||
{
|
||||
var output = EnumConverter.GetString(value);
|
||||
Assert.That(output == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", null)]
|
||||
[TestCase(null, null)]
|
||||
public void TestEnumConverterNullableDeserializeTests(string value, TestEnum? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<STJEnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", TestEnum.One)]
|
||||
[TestCase(null, TestEnum.One)]
|
||||
public void TestEnumConverterNotNullableDeserializeTests(string value, TestEnum? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<NotNullableSTJEnumObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", TestEnum.One)]
|
||||
[TestCase("2", TestEnum.Two)]
|
||||
[TestCase("3", TestEnum.Three)]
|
||||
[TestCase("three", TestEnum.Three)]
|
||||
[TestCase("Four", TestEnum.Four)]
|
||||
[TestCase("four", TestEnum.Four)]
|
||||
[TestCase("Four1", 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("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", null)]
|
||||
public void TestBoolConverter(string value, bool? expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<STJBoolObject>($"{{ \"Value\": {val} }}");
|
||||
Assert.That(output.Value == expected);
|
||||
}
|
||||
|
||||
[TestCase("1", true)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("yes", true)]
|
||||
[TestCase("y", true)]
|
||||
[TestCase("on", true)]
|
||||
[TestCase("-1", false)]
|
||||
[TestCase("0", false)]
|
||||
[TestCase("n", false)]
|
||||
[TestCase("no", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("off", false)]
|
||||
[TestCase("", false)]
|
||||
public void TestBoolConverterNotNullable(string value, bool expected)
|
||||
{
|
||||
var val = value == null ? "null" : $"\"{value}\"";
|
||||
var output = JsonSerializer.Deserialize<NotNullableSTJBoolObject>($"{{ \"Value\": {val} }}");
|
||||
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));
|
||||
}
|
||||
|
||||
[Test()]
|
||||
public void TestArrayConverter()
|
||||
{
|
||||
var data = new Test()
|
||||
{
|
||||
Prop1 = 2,
|
||||
Prop2 = null,
|
||||
Prop3 = "123",
|
||||
Prop3Again = "123",
|
||||
Prop4 = null,
|
||||
Prop5 = new Test2
|
||||
{
|
||||
Prop21 = 3,
|
||||
Prop22 = "456"
|
||||
},
|
||||
Prop6 = new Test3
|
||||
{
|
||||
Prop31 = 4,
|
||||
Prop32 = "789"
|
||||
},
|
||||
Prop7 = TestEnum.Two
|
||||
};
|
||||
|
||||
var serialized = JsonSerializer.Serialize(data);
|
||||
var deserialized = JsonSerializer.Deserialize<Test>(serialized);
|
||||
|
||||
Assert.That(deserialized.Prop1, Is.EqualTo(2));
|
||||
Assert.That(deserialized.Prop2, Is.Null);
|
||||
Assert.That(deserialized.Prop3, Is.EqualTo("123"));
|
||||
Assert.That(deserialized.Prop3Again, Is.EqualTo("123"));
|
||||
Assert.That(deserialized.Prop4, Is.Null);
|
||||
Assert.That(deserialized.Prop5.Prop21, Is.EqualTo(3));
|
||||
Assert.That(deserialized.Prop5.Prop22, Is.EqualTo("456"));
|
||||
Assert.That(deserialized.Prop6.Prop31, Is.EqualTo(4));
|
||||
Assert.That(deserialized.Prop6.Prop32, Is.EqualTo("789"));
|
||||
Assert.That(deserialized.Prop7, Is.EqualTo(TestEnum.Two));
|
||||
}
|
||||
}
|
||||
|
||||
public class STJDecimalObject
|
||||
{
|
||||
[JsonConverter(typeof(DecimalConverter))]
|
||||
[JsonPropertyName("test")]
|
||||
public decimal? Test { get; set; }
|
||||
}
|
||||
|
||||
public class STJTimeObject
|
||||
{
|
||||
[JsonConverter(typeof(DateTimeConverter))]
|
||||
[JsonPropertyName("time")]
|
||||
public DateTime? Time { get; set; }
|
||||
}
|
||||
|
||||
public class STJEnumObject
|
||||
{
|
||||
[JsonConverter(typeof(EnumConverter))]
|
||||
public TestEnum? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableSTJEnumObject
|
||||
{
|
||||
[JsonConverter(typeof(EnumConverter))]
|
||||
public TestEnum Value { get; set; }
|
||||
}
|
||||
|
||||
public class STJBoolObject
|
||||
{
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool? Value { get; set; }
|
||||
}
|
||||
|
||||
public class NotNullableSTJBoolObject
|
||||
{
|
||||
[JsonConverter(typeof(BoolConverter))]
|
||||
public bool Value { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter))]
|
||||
record Test
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
public int Prop1 { get; set; }
|
||||
[ArrayProperty(1)]
|
||||
public int? Prop2 { get; set; }
|
||||
[ArrayProperty(2)]
|
||||
public string Prop3 { get; set; }
|
||||
[ArrayProperty(2)]
|
||||
public string Prop3Again { get; set; }
|
||||
[ArrayProperty(3)]
|
||||
public string Prop4 { get; set; }
|
||||
[ArrayProperty(4)]
|
||||
public Test2 Prop5 { get; set; }
|
||||
[ArrayProperty(5)]
|
||||
public Test3 Prop6 { get; set; }
|
||||
[ArrayProperty(6), JsonConverter(typeof(EnumConverter))]
|
||||
public TestEnum? Prop7 { get; set; }
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(ArrayConverter))]
|
||||
record Test2
|
||||
{
|
||||
[ArrayProperty(0)]
|
||||
public int Prop21 { get; set; }
|
||||
[ArrayProperty(1)]
|
||||
public string Prop22 { get; set; }
|
||||
}
|
||||
|
||||
record Test3
|
||||
{
|
||||
[JsonPropertyName("prop31")]
|
||||
public int Prop31 { get; set; }
|
||||
[JsonPropertyName("prop32")]
|
||||
public string Prop32 { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
internal class SubResponse
|
||||
{
|
||||
|
||||
[JsonProperty("action")]
|
||||
public string Action { get; set; } = null!;
|
||||
|
||||
[JsonProperty("channel")]
|
||||
public string Channel { get; set; } = null!;
|
||||
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; } = null!;
|
||||
}
|
||||
|
||||
internal class UnsubResponse
|
||||
{
|
||||
[JsonProperty("action")]
|
||||
public string Action { get; set; } = null!;
|
||||
|
||||
[JsonProperty("status")]
|
||||
public string Status { get; set; } = null!;
|
||||
}
|
||||
|
||||
internal class TestChannelQuery : Query<SubResponse>
|
||||
{
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
||||
|
||||
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||
{
|
||||
ListenerIdentifiers = new HashSet<string> { request + "-" + channel };
|
||||
}
|
||||
|
||||
public override CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
||||
{
|
||||
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new CallResult<SubResponse>(new ServerError(message.Data.Status));
|
||||
}
|
||||
|
||||
return base.HandleMessage(connection, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
internal class TestQuery : Query<object>
|
||||
{
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
||||
|
||||
public TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
||||
{
|
||||
ListenerIdentifiers = new HashSet<string> { identifier };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
internal class TestSubscription<T> : Subscription<object, object>
|
||||
{
|
||||
private readonly Action<DataEvent<T>> _handler;
|
||||
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; } = new HashSet<string> { "update-topic" };
|
||||
|
||||
public TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
||||
{
|
||||
_handler = handler;
|
||||
}
|
||||
|
||||
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
||||
{
|
||||
var data = (T)message.Data;
|
||||
_handler.Invoke(message.As(data));
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
||||
public override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
||||
public override Query GetUnsubQuery() => new TestQuery("unsub", new object(), false, 1);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
||||
{
|
||||
internal class TestSubscriptionWithResponseCheck<T> : Subscription<SubResponse, UnsubResponse>
|
||||
{
|
||||
private readonly Action<DataEvent<T>> _handler;
|
||||
private readonly string _channel;
|
||||
|
||||
public override HashSet<string> ListenerIdentifiers { get; set; }
|
||||
|
||||
public TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
|
||||
{
|
||||
ListenerIdentifiers = new HashSet<string>() { channel };
|
||||
_handler = handler;
|
||||
_channel = channel;
|
||||
}
|
||||
|
||||
public override CallResult DoHandleMessage(SocketConnection connection, DataEvent<object> message)
|
||||
{
|
||||
var data = (T)message.Data;
|
||||
_handler.Invoke(message.As(data));
|
||||
return new CallResult(null);
|
||||
}
|
||||
|
||||
public override Type GetMessageType(IMessageAccessor message) => typeof(T);
|
||||
public override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
||||
public override Query GetUnsubQuery() => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -16,7 +20,7 @@ namespace CryptoExchange.Net.UnitTests
|
||||
|
||||
public TestBaseClient(): base(null, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
var options = new TestClientOptions();
|
||||
Initialize(options);
|
||||
SubClient = AddApiClient(new TestSubClient(options, new RestApiOptions()));
|
||||
}
|
||||
@@ -39,8 +43,20 @@ namespace CryptoExchange.Net.UnitTests
|
||||
{
|
||||
}
|
||||
|
||||
public CallResult<T> Deserialize<T>(string data) => Deserialize<T>(data, null, null);
|
||||
public CallResult<T> Deserialize<T>(string data)
|
||||
{
|
||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
||||
var accessor = CreateAccessor();
|
||||
var valid = accessor.Read(stream, true).Result;
|
||||
if (!valid)
|
||||
return new CallResult<T>(new ServerError(data));
|
||||
|
||||
var deserializeResult = accessor.Deserialize<T>();
|
||||
return deserializeResult;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
public override TimeSpan? GetTimeOffset() => null;
|
||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||
@@ -53,19 +69,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, 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 override string Sign(string toSign)
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
|
||||
public string GetKey() => _credentials.Key.GetString();
|
||||
public string GetSecret() => _credentials.Secret.GetString();
|
||||
public string GetKey() => _credentials.Key;
|
||||
public string GetSecret() => _credentials.Secret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ using CryptoExchange.Net.Authentication;
|
||||
using System.Collections.Generic;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
@@ -22,22 +25,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
public TestRestApi1Client Api1 { get; }
|
||||
public TestRestApi2Client Api2 { get; }
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestRestClient(Action<TestClientOptions> optionsDelegate = null)
|
||||
: this(null, null, Options.Create(ApplyOptionsDelegate(optionsDelegate)))
|
||||
{
|
||||
}
|
||||
|
||||
public TestRestClient(ILoggerFactory loggerFactory = null, HttpClient httpClient = null) : this((x) => { }, httpClient, loggerFactory)
|
||||
public TestRestClient(HttpClient httpClient, ILoggerFactory loggerFactory, IOptions<TestClientOptions> options) : base(loggerFactory, "Test")
|
||||
{
|
||||
}
|
||||
Initialize(options.Value);
|
||||
|
||||
public TestRestClient(Action<TestClientOptions> optionsFunc, HttpClient httpClient = null, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestClientOptions.Default.Copy();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
|
||||
Api1 = new TestRestApi1Client(options);
|
||||
Api2 = new TestRestApi2Client(options);
|
||||
Api1 = new TestRestApi1Client(options.Value);
|
||||
Api2 = new TestRestApi2Client(options.Value);
|
||||
}
|
||||
|
||||
public void SetResponse(string responseData, out IRequest requestObj)
|
||||
@@ -136,14 +134,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
||||
}
|
||||
|
||||
public async Task<CallResult<T>> RequestWithParams<T>(HttpMethod method, Dictionary<string, object> parameters, Dictionary<string, string> headers) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, additionalHeaders: headers);
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), method, default, parameters, requestWeight: 0, additionalHeaders: headers);
|
||||
}
|
||||
|
||||
public void SetParameterPosition(HttpMethod method, HttpMethodParameterPosition position)
|
||||
@@ -177,16 +178,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
|
||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||
{
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct);
|
||||
return await SendRequestAsync<T>(new Uri("http://www.test.com"), HttpMethod.Get, ct, requestWeight: 0);
|
||||
}
|
||||
|
||||
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, string data)
|
||||
protected override Error ParseErrorResponse(int httpStatusCode, IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders, IMessageAccessor accessor)
|
||||
{
|
||||
var errorData = ValidateJson(data);
|
||||
var errorData = accessor.Deserialize<TestError>();
|
||||
|
||||
return new ServerError((int)errorData.Data["errorCode"], (string)errorData.Data["errorMessage"]);
|
||||
return new ServerError(errorData.Data.ErrorCode, errorData.Data.ErrorMessage);
|
||||
}
|
||||
|
||||
public override TimeSpan? GetTimeOffset()
|
||||
@@ -208,6 +212,12 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
}
|
||||
}
|
||||
|
||||
public class TestError
|
||||
{
|
||||
public int ErrorCode { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
public class ParseErrorTestRestClient: TestRestClient
|
||||
{
|
||||
public ParseErrorTestRestClient() { }
|
||||
|
||||
@@ -1,129 +1,132 @@
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
//using System;
|
||||
//using System.IO;
|
||||
//using System.Net.WebSockets;
|
||||
//using System.Security.Authentication;
|
||||
//using System.Text;
|
||||
//using System.Threading.Tasks;
|
||||
//using CryptoExchange.Net.Interfaces;
|
||||
//using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocket: IWebsocket
|
||||
{
|
||||
public bool CanConnect { get; set; }
|
||||
public bool Connected { get; set; }
|
||||
//namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
//{
|
||||
// public class TestSocket: IWebsocket
|
||||
// {
|
||||
// public bool CanConnect { get; set; }
|
||||
// public bool Connected { get; set; }
|
||||
|
||||
public event Action OnClose;
|
||||
// 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;
|
||||
//#pragma warning restore 0067
|
||||
// public event Func<int, Task> OnRequestSent;
|
||||
// public event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
||||
// public event Func<Exception, Task> OnError;
|
||||
// public event Func<Task> OnOpen;
|
||||
// public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||
|
||||
#pragma warning disable 0067
|
||||
public event Action OnReconnected;
|
||||
public event Action OnReconnecting;
|
||||
#pragma warning restore 0067
|
||||
public event Action<int> OnRequestSent;
|
||||
public event Action<string> OnMessage;
|
||||
public event Action<Exception> OnError;
|
||||
public event Action OnOpen;
|
||||
public Func<Task<Uri>> GetReconnectionUrl { get; set; }
|
||||
// public int Id { get; }
|
||||
// public bool ShouldReconnect { get; set; }
|
||||
// public TimeSpan Timeout { get; set; }
|
||||
// public Func<string, string> DataInterpreterString { get; set; }
|
||||
// public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
// public DateTime? DisconnectTime { get; set; }
|
||||
// public string Url { get; }
|
||||
// public bool IsClosed => !Connected;
|
||||
// public bool IsOpen => Connected;
|
||||
// public bool PingConnection { get; set; }
|
||||
// public TimeSpan PingInterval { get; set; }
|
||||
// public SslProtocols SSLProtocols { get; set; }
|
||||
// public Encoding Encoding { get; set; }
|
||||
|
||||
public int Id { get; }
|
||||
public bool ShouldReconnect { get; set; }
|
||||
public TimeSpan Timeout { get; set; }
|
||||
public Func<string, string> DataInterpreterString { get; set; }
|
||||
public Func<byte[], string> DataInterpreterBytes { get; set; }
|
||||
public DateTime? DisconnectTime { get; set; }
|
||||
public string Url { get; }
|
||||
public bool IsClosed => !Connected;
|
||||
public bool IsOpen => Connected;
|
||||
public bool PingConnection { get; set; }
|
||||
public TimeSpan PingInterval { get; set; }
|
||||
public SslProtocols SSLProtocols { get; set; }
|
||||
public Encoding Encoding { get; set; }
|
||||
// public int ConnectCalls { get; private set; }
|
||||
// public bool Reconnecting { get; set; }
|
||||
// public string Origin { get; set; }
|
||||
// public int? RatelimitPerSecond { get; set; }
|
||||
|
||||
public int ConnectCalls { get; private set; }
|
||||
public bool Reconnecting { get; set; }
|
||||
public string Origin { 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 object lastIdLock = new object();
|
||||
|
||||
public static int lastId = 0;
|
||||
public static object lastIdLock = new object();
|
||||
// public TestSocket()
|
||||
// {
|
||||
// lock (lastIdLock)
|
||||
// {
|
||||
// Id = lastId + 1;
|
||||
// lastId++;
|
||||
// }
|
||||
// }
|
||||
|
||||
public TestSocket()
|
||||
{
|
||||
lock (lastIdLock)
|
||||
{
|
||||
Id = lastId + 1;
|
||||
lastId++;
|
||||
}
|
||||
}
|
||||
// public Task<CallResult> ConnectAsync()
|
||||
// {
|
||||
// Connected = CanConnect;
|
||||
// ConnectCalls++;
|
||||
// if (CanConnect)
|
||||
// InvokeOpen();
|
||||
// return Task.FromResult(CanConnect ? new CallResult(null) : new CallResult(new CantConnectError()));
|
||||
// }
|
||||
|
||||
public Task<bool> ConnectAsync()
|
||||
{
|
||||
Connected = CanConnect;
|
||||
ConnectCalls++;
|
||||
if (CanConnect)
|
||||
InvokeOpen();
|
||||
return Task.FromResult(CanConnect);
|
||||
}
|
||||
// public bool Send(int requestId, string data, int weight)
|
||||
// {
|
||||
// if(!Connected)
|
||||
// throw new Exception("Socket not connected");
|
||||
// OnRequestSent?.Invoke(requestId);
|
||||
// return true;
|
||||
// }
|
||||
|
||||
public void Send(int requestId, string data, int weight)
|
||||
{
|
||||
if(!Connected)
|
||||
throw new Exception("Socket not connected");
|
||||
OnRequestSent?.Invoke(requestId);
|
||||
}
|
||||
// public void Reset()
|
||||
// {
|
||||
// }
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
}
|
||||
// public Task CloseAsync()
|
||||
// {
|
||||
// Connected = false;
|
||||
// DisconnectTime = DateTime.UtcNow;
|
||||
// OnClose?.Invoke();
|
||||
// return Task.FromResult(0);
|
||||
// }
|
||||
|
||||
public Task CloseAsync()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
OnClose?.Invoke();
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
// public void SetProxy(string host, int port)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
// public void Dispose()
|
||||
// {
|
||||
// }
|
||||
|
||||
public void SetProxy(string host, int port)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
// public void InvokeClose()
|
||||
// {
|
||||
// Connected = false;
|
||||
// DisconnectTime = DateTime.UtcNow;
|
||||
// Reconnecting = true;
|
||||
// OnClose?.Invoke();
|
||||
// }
|
||||
|
||||
public void InvokeClose()
|
||||
{
|
||||
Connected = false;
|
||||
DisconnectTime = DateTime.UtcNow;
|
||||
Reconnecting = true;
|
||||
OnClose?.Invoke();
|
||||
}
|
||||
// public void InvokeOpen()
|
||||
// {
|
||||
// OnOpen?.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(string data)
|
||||
{
|
||||
OnMessage?.Invoke(data);
|
||||
}
|
||||
// public void SetProxy(ApiProxy proxy)
|
||||
// {
|
||||
// throw new NotImplementedException();
|
||||
// }
|
||||
|
||||
public void SetProxy(ApiProxy proxy)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void InvokeError(Exception error)
|
||||
{
|
||||
OnError?.Invoke(error);
|
||||
}
|
||||
public Task ReconnectAsync() => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
// public void InvokeError(Exception error)
|
||||
// {
|
||||
// OnError?.Invoke(error);
|
||||
// }
|
||||
// public Task ReconnectAsync() => Task.CompletedTask;
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -1,47 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CryptoExchange.Net.Testing.Implementations;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
{
|
||||
public class TestSocketClient: BaseSocketClient
|
||||
internal class TestSocketClient: BaseSocketClient
|
||||
{
|
||||
public TestSubSocketClient SubClient { get; }
|
||||
|
||||
public TestSocketClient(ILoggerFactory loggerFactory = null) : this((x) => { }, loggerFactory)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new instance of KucoinSocketClient
|
||||
/// </summary>
|
||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc) : this(optionsFunc, null)
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
|
||||
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
|
||||
{
|
||||
}
|
||||
|
||||
public TestSocketClient(Action<TestSocketOptions> optionsFunc, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
||||
{
|
||||
var options = TestSocketOptions.Default.Copy<TestSocketOptions>();
|
||||
optionsFunc(options);
|
||||
Initialize(options);
|
||||
Initialize(options.Value);
|
||||
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options, options.SubOptions));
|
||||
SubClient = AddApiClient(new TestSubSocketClient(options.Value, options.Value.SubOptions));
|
||||
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()
|
||||
{
|
||||
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/");
|
||||
}
|
||||
|
||||
@@ -64,17 +66,40 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
Environment = new TestEnvironment("Live", "https://test.test")
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public TestSocketOptions()
|
||||
{
|
||||
Default?.Set(this);
|
||||
}
|
||||
|
||||
public SocketApiOptions SubOptions { get; set; } = new SocketApiOptions();
|
||||
|
||||
internal TestSocketOptions Set(TestSocketOptions targetOptions)
|
||||
{
|
||||
targetOptions = base.Set<TestSocketOptions>(targetOptions);
|
||||
targetOptions.SubOptions = SubOptions.Set(targetOptions.SubOptions);
|
||||
return targetOptions;
|
||||
}
|
||||
}
|
||||
|
||||
public class TestSubSocketClient : SocketApiClient
|
||||
{
|
||||
private MessagePath _channelPath = MessagePath.Get().Property("channel");
|
||||
private MessagePath _actionPath = MessagePath.Get().Property("action");
|
||||
private MessagePath _topicPath = MessagePath.Get().Property("topic");
|
||||
|
||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions): base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||
public Subscription TestSubscription { get; private set; } = null;
|
||||
|
||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||
|
||||
internal IWebsocket CreateSocketInternal(string address)
|
||||
{
|
||||
return CreateSocket(address);
|
||||
@@ -83,40 +108,28 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||
=> new TestAuthProvider(credentials);
|
||||
|
||||
public CallResult<bool> ConnectSocketSub(SocketConnection sub)
|
||||
public CallResult ConnectSocketSub(SocketConnection sub)
|
||||
{
|
||||
return ConnectSocketAsync(sub).Result;
|
||||
}
|
||||
|
||||
protected internal override bool HandleQueryResponse<T>(SocketConnection s, object request, JToken data, out CallResult<T> callResult)
|
||||
public override string GetListenerIdentifier(IMessageAccessor message)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
if (!message.IsJson)
|
||||
{
|
||||
return "topic";
|
||||
}
|
||||
|
||||
var id = message.GetValue<string>(_channelPath);
|
||||
id ??= message.GetValue<string>(_topicPath);
|
||||
|
||||
return message.GetValue<string>(_actionPath) + "-" + id;
|
||||
}
|
||||
|
||||
protected internal override bool HandleSubscriptionResponse(SocketConnection s, SocketSubscription subscription, object request, JToken message,
|
||||
out CallResult<object> callResult)
|
||||
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, object request)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override bool MessageMatchesHandler(SocketConnection s, JToken message, string identifier)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected internal override Task<CallResult<bool>> AuthenticateSocketAsync(SocketConnection s)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
protected internal override Task<bool> UnsubscribeAsync(SocketConnection connection, SocketSubscription s)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
|
||||
return SubscribeAsync(TestSubscription, ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorClient", "Examples\Bl
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{5734C2A9-F12C-4754-A8B9-640C24DC4E02}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleClient", "Examples\ConsoleClient\ConsoleClient.csproj", "{23480C58-23BF-4EBF-A173-B7F51A043A99}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SharedClients", "Examples\SharedClients\SharedClients.csproj", "{988A87EF-EAEA-4313-A6CF-FA869813D5AB}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -35,6 +37,10 @@ Global
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -42,6 +48,7 @@ Global
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{AF4F5C19-162E-48F4-8B0B-BA5A2D7CE06A} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
{23480C58-23BF-4EBF-A173-B7F51A043A99} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
{988A87EF-EAEA-4313-A6CF-FA869813D5AB} = {5734C2A9-F12C-4754-A8B9-640C24DC4E02}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {0D1B9CE9-E0B7-4B8B-88BF-6EA2CC8CA3D7}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace CryptoExchange.Net.Attributes
|
||||
/// <summary>
|
||||
/// Map a enum entry to string values
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
public class MapAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#if !NETSTANDARD2_1
|
||||
#if NETSTANDARD2_0
|
||||
namespace System.Diagnostics.CodeAnalysis
|
||||
{
|
||||
using System;
|
||||
|
||||
@@ -1,79 +1,44 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
|
||||
namespace CryptoExchange.Net.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// Api credentials, used to sign requests accessing private endpoints
|
||||
/// </summary>
|
||||
public class ApiCredentials: IDisposable
|
||||
public class ApiCredentials
|
||||
{
|
||||
/// <summary>
|
||||
/// The api key to authenticate requests
|
||||
/// The api key / label to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString? Key { get; }
|
||||
public string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The api secret to authenticate requests
|
||||
/// The api secret or private key to authenticate requests
|
||||
/// </summary>
|
||||
public SecureString? Secret { get; }
|
||||
public string Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the credentials
|
||||
/// </summary>
|
||||
public ApiCredentialsType CredentialType { get; }
|
||||
public ApiCredentialsType CredentialType { get; set; }
|
||||
|
||||
/// <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>
|
||||
/// 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(string key, string 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(string key, string secret, ApiCredentialsType credentialsType)
|
||||
/// <param name="key">The api key / label used for identification</param>
|
||||
/// <param name="secret">The api secret or private key used for signing</param>
|
||||
/// <param name="credentialType">The type of credentials</param>
|
||||
public ApiCredentials(string key, string secret, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||
throw new ArgumentException("Key and secret can't be null/empty");
|
||||
|
||||
CredentialType = credentialsType;
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
CredentialType = credentialType;
|
||||
Key = key;
|
||||
Secret = secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -82,8 +47,7 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
public virtual ApiCredentials Copy()
|
||||
{
|
||||
// Use .GetString() to create a copy of the SecureString
|
||||
return new ApiCredentials(Key!.GetString(), Secret!.GetString(), CredentialType);
|
||||
return new ApiCredentials(Key, Secret, CredentialType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,47 +56,19 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="inputStream">The stream containing the json data</param>
|
||||
/// <param name="identifierKey">A key to identify the credentials for the API. For example, when set to `binanceKey` the json data should contain a value for the property `binanceKey`. Defaults to 'apiKey'.</param>
|
||||
/// <param name="identifierSecret">A key to identify the credentials for the API. For example, when set to `binanceSecret` the json data should contain a value for the property `binanceSecret`. Defaults to 'apiSecret'.</param>
|
||||
public ApiCredentials(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
public static ApiCredentials FromStream(Stream inputStream, string? identifierKey = null, string? identifierSecret = null)
|
||||
{
|
||||
using var reader = new StreamReader(inputStream, Encoding.UTF8, false, 512, true);
|
||||
|
||||
var stringData = reader.ReadToEnd();
|
||||
var jsonData = stringData.ToJToken();
|
||||
if(jsonData == null)
|
||||
var accessor = new SystemTextJsonStreamMessageAccessor();
|
||||
if (!accessor.Read(inputStream, false).Result)
|
||||
throw new ArgumentException("Input stream not valid json data");
|
||||
|
||||
var key = TryGetValue(jsonData, identifierKey ?? "apiKey");
|
||||
var secret = TryGetValue(jsonData, identifierSecret ?? "apiSecret");
|
||||
|
||||
var key = accessor.GetValue<string>(MessagePath.Get().Property(identifierKey ?? "apiKey"));
|
||||
var secret = accessor.GetValue<string>(MessagePath.Get().Property(identifierSecret ?? "apiSecret"));
|
||||
if (key == null || secret == null)
|
||||
throw new ArgumentException("apiKey or apiSecret value not found in Json credential file");
|
||||
|
||||
Key = key.ToSecureString();
|
||||
Secret = secret.ToSecureString();
|
||||
|
||||
|
||||
inputStream.Seek(0, SeekOrigin.Begin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try get the value of a key from a JToken
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
protected string? TryGetValue(JToken data, string key)
|
||||
{
|
||||
if (data[key] == null)
|
||||
return null;
|
||||
return (string) data[key]!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
Key?.Dispose();
|
||||
Secret?.Dispose();
|
||||
return new ApiCredentials(key, secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using CryptoExchange.Net.Converters;
|
||||
using CryptoExchange.Net.Clients;
|
||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
@@ -12,29 +14,36 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <summary>
|
||||
/// Base class for authentication providers
|
||||
/// </summary>
|
||||
public abstract class AuthenticationProvider : IDisposable
|
||||
public abstract class AuthenticationProvider
|
||||
{
|
||||
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
|
||||
|
||||
/// <summary>
|
||||
/// Provided credentials
|
||||
/// </summary>
|
||||
protected readonly ApiCredentials _credentials;
|
||||
protected internal readonly ApiCredentials _credentials;
|
||||
|
||||
/// <summary>
|
||||
/// Byte representation of the secret
|
||||
/// </summary>
|
||||
protected byte[] _sBytes;
|
||||
|
||||
/// <summary>
|
||||
/// Get the API key of the current credentials
|
||||
/// </summary>
|
||||
public string ApiKey => _credentials.Key!;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="credentials"></param>
|
||||
protected AuthenticationProvider(ApiCredentials credentials)
|
||||
{
|
||||
if (credentials.Secret == null)
|
||||
if (credentials.Key == null || credentials.Secret == null)
|
||||
throw new ArgumentException("ApiKey/Secret needed");
|
||||
|
||||
_credentials = credentials;
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret.GetString());
|
||||
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -43,24 +52,24 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="apiClient">The Api client sending the request</param>
|
||||
/// <param name="uri">The uri for 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="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="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="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(
|
||||
RestApiClient apiClient,
|
||||
Uri uri,
|
||||
HttpMethod method,
|
||||
Dictionary<string, object> providedParameters,
|
||||
ref IDictionary<string, object>? uriParameters,
|
||||
ref IDictionary<string, object>? bodyParameters,
|
||||
ref Dictionary<string, string>? headers,
|
||||
bool auth,
|
||||
ArrayParametersSerialization arraySerialization,
|
||||
HttpMethodParameterPosition parameterPosition,
|
||||
out SortedDictionary<string, object> uriParameters,
|
||||
out SortedDictionary<string, object> bodyParameters,
|
||||
out Dictionary<string, string> headers
|
||||
RequestBodyFormat requestBodyFormat
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
@@ -74,6 +83,17 @@ namespace CryptoExchange.Net.Authentication
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the bytes
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA256Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -87,6 +107,19 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA256.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -100,6 +133,41 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA384Bytes(string data)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA384Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA384.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -113,6 +181,41 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignSHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA512Bytes(string data)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignSHA512Bytes(byte[] data)
|
||||
{
|
||||
using var encryptor = SHA512.Create();
|
||||
return encryptor.ComputeHash(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -126,6 +229,30 @@ namespace CryptoExchange.Net.Authentication
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MD5 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <returns></returns>
|
||||
protected static byte[] SignMD5Bytes(string data)
|
||||
{
|
||||
using var encryptor = MD5.Create();
|
||||
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
@@ -133,9 +260,18 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA256(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA256(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA256 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA256(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
@@ -146,9 +282,18 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA384(string data, SignOutputType? outputType = null)
|
||||
=> SignHMACSHA384(Encoding.UTF8.GetBytes(data), outputType);
|
||||
|
||||
/// <summary>
|
||||
/// HMACSHA384 sign the data and return the hash
|
||||
/// </summary>
|
||||
/// <param name="data">Data to sign</param>
|
||||
/// <param name="outputType">String type</param>
|
||||
/// <returns></returns>
|
||||
protected string SignHMACSHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var encryptor = new HMACSHA384(_sBytes);
|
||||
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
var resultBytes = encryptor.ComputeHash(data);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
@@ -182,12 +327,51 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA256(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
using var rsa = CreateRSA();
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA384 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA384(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
using var sha384 = SHA384.Create();
|
||||
var hash = sha384.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA384, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SHA512 sign the data
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="outputType"></param>
|
||||
/// <returns></returns>
|
||||
protected string SignRSASHA512(byte[] data, SignOutputType? outputType = null)
|
||||
{
|
||||
using var rsa = CreateRSA();
|
||||
using var sha512 = SHA512.Create();
|
||||
var hash = sha512.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA512, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
private RSA CreateRSA()
|
||||
{
|
||||
var rsa = RSA.Create();
|
||||
if (_credentials.CredentialType == ApiCredentialsType.RsaPem)
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||
// Read from pem private key
|
||||
var key = _credentials.Secret!.GetString()
|
||||
var key = _credentials.Secret!
|
||||
.Replace("\n", "")
|
||||
.Replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.Replace("-----END PRIVATE KEY-----", "")
|
||||
@@ -202,37 +386,14 @@ namespace CryptoExchange.Net.Authentication
|
||||
else if (_credentials.CredentialType == ApiCredentialsType.RsaXml)
|
||||
{
|
||||
// Read from xml private key format
|
||||
rsa.FromXmlString(_credentials.Secret!.GetString());
|
||||
rsa.FromXmlString(_credentials.Secret!);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Invalid credentials type");
|
||||
}
|
||||
|
||||
using var sha256 = SHA256.Create();
|
||||
var hash = sha256.ComputeHash(data);
|
||||
var resultBytes = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
return outputType == SignOutputType.Base64? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a string
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <returns></returns>
|
||||
public virtual string Sign(string toSign)
|
||||
{
|
||||
return toSign;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sign a byte array
|
||||
/// </summary>
|
||||
/// <param name="toSign"></param>
|
||||
/// <returns></returns>
|
||||
public virtual byte[] Sign(byte[] toSign)
|
||||
{
|
||||
return toSign;
|
||||
return rsa;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -242,10 +403,14 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// <returns></returns>
|
||||
protected static string BytesToHexString(byte[] buff)
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
return Convert.ToHexString(buff);
|
||||
#else
|
||||
var result = string.Empty;
|
||||
foreach (var t in buff)
|
||||
result += t.ToString("X2");
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -263,9 +428,9 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <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>
|
||||
@@ -273,15 +438,33 @@ namespace CryptoExchange.Net.Authentication
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
protected string GetMillisecondTimestamp(RestApiClient apiClient)
|
||||
{
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
/// <summary>
|
||||
/// Get millisecond timestamp as a long including the time sync offset from the api client
|
||||
/// </summary>
|
||||
/// <param name="apiClient"></param>
|
||||
/// <returns></returns>
|
||||
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
|
||||
{
|
||||
_credentials?.Dispose();
|
||||
return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the serialized request body
|
||||
/// </summary>
|
||||
/// <param name="serializer"></param>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
protected static string GetSerializedBody(IMessageSerializer serializer, IDictionary<string, object> parameters)
|
||||
{
|
||||
if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
|
||||
return serializer.Serialize(value);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base API for all API clients
|
||||
@@ -36,37 +28,6 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Where to put the parameters for requests with different Http methods
|
||||
/// </summary>
|
||||
public Dictionary<HttpMethod, HttpMethodParameterPosition> ParameterPositions { get; set; } = new Dictionary<HttpMethod, HttpMethodParameterPosition>
|
||||
{
|
||||
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
|
||||
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
|
||||
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
|
||||
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Request body content type
|
||||
/// </summary>
|
||||
public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not we need to manually parse an error instead of relying on the http status code
|
||||
/// </summary>
|
||||
public bool manualParseError = false;
|
||||
|
||||
/// <summary>
|
||||
/// How to serialize array parameters when making requests
|
||||
/// </summary>
|
||||
public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
|
||||
|
||||
/// <summary>
|
||||
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
|
||||
/// </summary>
|
||||
public string requestBodyEmptyContent = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// The environment this client communicates to
|
||||
/// </summary>
|
||||
@@ -77,14 +38,8 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public bool OutputOriginalData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A default serializer
|
||||
/// </summary>
|
||||
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
});
|
||||
/// <inheritdoc />
|
||||
public bool Authenticated => ApiOptions.ApiCredentials != null || ClientOptions.ApiCredentials != null;
|
||||
|
||||
/// <summary>
|
||||
/// Api options
|
||||
@@ -100,7 +55,7 @@ namespace CryptoExchange.Net
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the orginal data in the call result</param>
|
||||
/// <param name="outputOriginalData">Should data from this client include the original data in the call result</param>
|
||||
/// <param name="baseAddress">Base address for this API client</param>
|
||||
/// <param name="apiCredentials">Api credentials</param>
|
||||
/// <param name="clientOptions">Client options</param>
|
||||
@@ -115,10 +70,7 @@ namespace CryptoExchange.Net
|
||||
BaseAddress = baseAddress;
|
||||
|
||||
if (apiCredentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -128,205 +80,26 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||
{
|
||||
ApiOptions.ApiCredentials = credentials;
|
||||
if (credentials != null)
|
||||
{
|
||||
AuthenticationProvider?.Dispose();
|
||||
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||
/// </summary>
|
||||
/// <param name="data">The data to parse</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<JToken> ValidateJson(string data)
|
||||
/// <inheritdoc />
|
||||
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
||||
{
|
||||
if (string.IsNullOrEmpty(data))
|
||||
{
|
||||
var info = "Empty data object received";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
ClientOptions.Proxy = options.Proxy;
|
||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<JToken>(JToken.Parse(data));
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a string into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="data">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
var tokenResult = ValidateJson(data);
|
||||
if (!tokenResult)
|
||||
{
|
||||
_logger.Log(LogLevel.Error, tokenResult.Error!.Message);
|
||||
return new CallResult<T>(tokenResult.Error);
|
||||
}
|
||||
|
||||
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a JToken into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="obj">The data to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <returns></returns>
|
||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||
{
|
||||
serializer ??= _defaultSerializer;
|
||||
|
||||
try
|
||||
{
|
||||
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||
_logger.Log(LogLevel.Error, info);
|
||||
return new CallResult<T>(new DeserializeError(info, obj));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize a stream into an object
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||
/// <param name="stream">The stream to deserialize</param>
|
||||
/// <param name="serializer">A specific serializer to use</param>
|
||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||
/// <returns></returns>
|
||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||
{
|
||||
serializer ??= _defaultSerializer;
|
||||
string? data = null;
|
||||
|
||||
try
|
||||
{
|
||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||
// in order to log/return the json data
|
||||
if (OutputOriginalData == true)
|
||||
{
|
||||
data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data);
|
||||
var result = Deserialize<T>(data, serializer, requestId);
|
||||
result.OriginalData = data;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
|
||||
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
|
||||
using var jsonReader = new JsonTextReader(reader);
|
||||
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
|
||||
return new CallResult<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (data == null)
|
||||
{
|
||||
if (stream.CanSeek)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = "[Data only available in Trace LogLevel]";
|
||||
}
|
||||
}
|
||||
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadStreamAsync(Stream stream)
|
||||
{
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
ApiOptions.ApiCredentials = options.ApiCredentials ?? ClientOptions.ApiCredentials;
|
||||
if (options.ApiCredentials != null)
|
||||
AuthenticationProvider = CreateAuthenticationProvider(options.ApiCredentials.Copy());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -335,7 +108,6 @@ namespace CryptoExchange.Net
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_disposing = true;
|
||||
AuthenticationProvider?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,43 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// The base for all clients, websocket client and rest client
|
||||
/// </summary>
|
||||
public abstract class BaseClient : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Version of the CryptoExchange.Net base library
|
||||
/// </summary>
|
||||
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the client implementation
|
||||
/// </summary>
|
||||
public Version ExchangeLibVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
lock(_versionLock)
|
||||
{
|
||||
if (_exchangeVersion == null)
|
||||
_exchangeVersion = GetType().Assembly.GetName().Version!;
|
||||
|
||||
return _exchangeVersion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The name of the API the client is for
|
||||
/// </summary>
|
||||
internal string Name { get; }
|
||||
public string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Api clients in this client
|
||||
@@ -27,7 +48,10 @@ namespace CryptoExchange.Net
|
||||
/// The log object
|
||||
/// </summary>
|
||||
protected internal ILogger _logger;
|
||||
|
||||
|
||||
private readonly object _versionLock = new object();
|
||||
private Version _exchangeVersion;
|
||||
|
||||
/// <summary>
|
||||
/// Provided client options
|
||||
/// </summary>
|
||||
@@ -37,14 +61,14 @@ namespace CryptoExchange.Net
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
protected BaseClient(ILoggerFactory? logger, string name)
|
||||
protected BaseClient(ILoggerFactory? logger, string exchange)
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
{
|
||||
_logger = logger?.CreateLogger(name) ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||
_logger = logger?.CreateLogger(exchange) ?? NullLoggerFactory.Instance.CreateLogger(exchange);
|
||||
|
||||
Name = name;
|
||||
Exchange = exchange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -52,13 +76,13 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="options"></param>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public virtual void Initialize(ExchangeOptions options)
|
||||
protected virtual void Initialize(ExchangeOptions options)
|
||||
{
|
||||
if (options == null)
|
||||
throw new ArgumentNullException(nameof(options));
|
||||
|
||||
ClientOptions = options;
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {Name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -75,7 +99,7 @@ namespace CryptoExchange.Net
|
||||
/// Register an API client
|
||||
/// </summary>
|
||||
/// <param name="apiClient">The client</param>
|
||||
protected T AddApiClient<T>(T apiClient) where T: BaseApiClient
|
||||
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
|
||||
{
|
||||
if (ClientOptions == null)
|
||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||
@@ -85,6 +109,16 @@ namespace CryptoExchange.Net
|
||||
return apiClient;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the options delegate to a new options instance
|
||||
/// </summary>
|
||||
protected static T ApplyOptionsDelegate<T>(Action<T>? del) where T: new()
|
||||
{
|
||||
var opts = new T();
|
||||
del?.Invoke(opts);
|
||||
return opts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
|
||||
@@ -2,7 +2,7 @@ using System.Linq;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest client
|
||||
|
||||
@@ -4,23 +4,24 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Logging.Extensions;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base for socket client implementations
|
||||
/// </summary>
|
||||
public abstract class BaseSocketClient: BaseClient, ISocketClient
|
||||
public abstract class BaseSocketClient : BaseClient, ISocketClient
|
||||
{
|
||||
#region fields
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// If client is disposing
|
||||
/// </summary>
|
||||
protected bool _disposing;
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||
/// <inheritdoc />
|
||||
@@ -33,8 +34,8 @@ namespace CryptoExchange.Net
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="logger">Logger</param>
|
||||
/// <param name="name">The name of the API this client is for</param>
|
||||
protected BaseSocketClient(ILoggerFactory? logger, string name) : base(logger, name)
|
||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
||||
protected BaseSocketClient(ILoggerFactory? logger, string exchange) : base(logger, exchange)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -45,11 +46,11 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||
{
|
||||
foreach(var socket in ApiClients.OfType<SocketApiClient>())
|
||||
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||
if (result)
|
||||
break;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +64,7 @@ namespace CryptoExchange.Net
|
||||
if (subscription == null)
|
||||
throw new ArgumentNullException(nameof(subscription));
|
||||
|
||||
_logger.Log(LogLevel.Information, $"Socket {subscription.SocketId} Unsubscribing subscription " + subscription.Id);
|
||||
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
||||
await subscription.CloseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -73,10 +74,10 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task UnsubscribeAllAsync()
|
||||
{
|
||||
var tasks = new List<Task>();
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
tasks.Add(client.UnsubscribeAllAsync());
|
||||
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -86,12 +87,13 @@ namespace CryptoExchange.Net
|
||||
/// <returns></returns>
|
||||
public virtual async Task ReconnectAsync()
|
||||
{
|
||||
_logger.Log(LogLevel.Information, $"Reconnecting all {CurrentConnections} connections");
|
||||
_logger.ReconnectingAllConnections(CurrentConnections);
|
||||
var tasks = new List<Task>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
tasks.Add(client.ReconnectAsync());
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -101,9 +103,27 @@ namespace CryptoExchange.Net
|
||||
public string GetSubscriptionsState()
|
||||
{
|
||||
var result = new StringBuilder();
|
||||
foreach(var client in ApiClients.OfType<SocketApiClient>())
|
||||
result.AppendLine(client.GetSubscriptionsState());
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
|
||||
{
|
||||
result.AppendLine(client.GetSubscriptionsState());
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the state of all socket api clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
|
||||
{
|
||||
var result = new List<SocketApiClient.SocketApiClientState>();
|
||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||
{
|
||||
result.Add(client.GetState());
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <summary>
|
||||
/// Base crypto client
|
||||
/// </summary>
|
||||
public class CryptoBaseClient : IDisposable
|
||||
{
|
||||
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
||||
|
||||
/// <summary>
|
||||
/// Service provider
|
||||
/// </summary>
|
||||
protected readonly IServiceProvider? _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CryptoBaseClient() { }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider"></param>
|
||||
public CryptoBaseClient(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_serviceCache = new Dictionary<Type, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try get a client by type for the service collection
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
public T TryGet<T>(Func<T> createFunc)
|
||||
{
|
||||
var type = typeof(T);
|
||||
if (_serviceCache.TryGetValue(type, out var value))
|
||||
return (T)value;
|
||||
|
||||
if (_serviceProvider == null)
|
||||
{
|
||||
// Create with default options
|
||||
var createResult = createFunc();
|
||||
_serviceCache.Add(typeof(T), createResult!);
|
||||
return createResult;
|
||||
}
|
||||
|
||||
var result = _serviceProvider.GetService<T>()
|
||||
?? throw new InvalidOperationException($"No service was found for {typeof(T).Name}, make sure the exchange is registered in dependency injection with the `services.Add[Exchange]()` method");
|
||||
_serviceCache.Add(type, result!);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_serviceCache.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CryptoRestClient()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider"></param>
|
||||
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of the registered ISpotClient implementations
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<ISpotClient> GetSpotClients()
|
||||
{
|
||||
if (_serviceProvider == null)
|
||||
return new List<ISpotClient>();
|
||||
|
||||
return _serviceProvider.GetServices<ISpotClient>().ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get an ISpotClient implementation by exchange name
|
||||
/// </summary>
|
||||
/// <param name="exchangeName"></param>
|
||||
/// <returns></returns>
|
||||
public ISpotClient? SpotClient(string exchangeName) => _serviceProvider?.GetServices<ISpotClient>()?.SingleOrDefault(s => s.ExchangeName.Equals(exchangeName, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Clients
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
|
||||
{
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public CryptoSocketClient()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider"></param>
|
||||
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Order type
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of an order
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
namespace CryptoExchange.Net.CommonObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Ticker data
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Mark property as an index in the array
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ArrayPropertyAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The index in the array
|
||||
/// </summary>
|
||||
public int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="index"></param>
|
||||
public ArrayPropertyAttribute(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-33
@@ -8,7 +8,7 @@ using CryptoExchange.Net.Attributes;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
||||
@@ -36,7 +36,7 @@ namespace CryptoExchange.Net.Converters
|
||||
|
||||
var result = Activator.CreateInstance(objectType);
|
||||
var arr = JArray.Load(reader);
|
||||
return ParseObject(arr, result, objectType);
|
||||
return ParseObject(arr, result!, objectType);
|
||||
}
|
||||
|
||||
private static object ParseObject(JArray arr, object result, Type objectType)
|
||||
@@ -58,25 +58,25 @@ namespace CryptoExchange.Net.Converters
|
||||
var count = 0;
|
||||
if (innerArray.Count == 0)
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 });
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 0 })!;
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
else if (innerArray[0].Type == JTokenType.Array)
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count });
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { innerArray.Count })!;
|
||||
foreach (var obj in innerArray)
|
||||
{
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj, objType!);
|
||||
arrayResult[count] = ParseObject((JArray)obj, innerObj!, objType!);
|
||||
count++;
|
||||
}
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 });
|
||||
var arrayResult = (IList)Activator.CreateInstance(property.PropertyType, new [] { 1 })!;
|
||||
var innerObj = Activator.CreateInstance(objType!);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj, objType!);
|
||||
arrayResult[0] = ParseObject(innerArray, innerObj!, objType!);
|
||||
property.SetValue(result, arrayResult);
|
||||
}
|
||||
continue;
|
||||
@@ -88,7 +88,7 @@ namespace CryptoExchange.Net.Converters
|
||||
object? value;
|
||||
if (converterAttribute != null)
|
||||
{
|
||||
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)}});
|
||||
value = arr[attribute.Index].ToObject(property.PropertyType, new JsonSerializer {Converters = {(JsonConverter) Activator.CreateInstance(converterAttribute.ConverterType)!}});
|
||||
}
|
||||
else if (conversionAttribute != null)
|
||||
{
|
||||
@@ -120,7 +120,7 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
else if ((property.PropertyType == typeof(decimal)
|
||||
|| property.PropertyType == typeof(decimal?))
|
||||
&& (value != null && value.ToString().IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
&& (value != null && value.ToString()!.IndexOf("e", StringComparison.OrdinalIgnoreCase) >= 0))
|
||||
{
|
||||
var v = value.ToString();
|
||||
if (decimal.TryParse(v, NumberStyles.Float, CultureInfo.InvariantCulture, out var dec))
|
||||
@@ -164,7 +164,7 @@ namespace CryptoExchange.Net.Converters
|
||||
last = arrayProp.Index;
|
||||
var converterAttribute = GetCustomAttribute<JsonConverterAttribute>(prop);
|
||||
if (converterAttribute != null)
|
||||
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)));
|
||||
writer.WriteRawValue(JsonConvert.SerializeObject(prop.GetValue(value), (JsonConverter)Activator.CreateInstance(converterAttribute.ConverterType)!));
|
||||
else if (!IsSimple(prop.PropertyType))
|
||||
serializer.Serialize(writer, prop.GetValue(value));
|
||||
else
|
||||
@@ -187,30 +187,9 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
|
||||
private static T? GetCustomAttribute<T>(MemberInfo memberInfo) where T : Attribute =>
|
||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T)));
|
||||
(T?)_attributeByMemberInfoAndTypeCache.GetOrAdd((memberInfo, typeof(T)), tuple => memberInfo.GetCustomAttribute(typeof(T))!);
|
||||
|
||||
private static T? GetCustomAttribute<T>(Type type) where T : Attribute =>
|
||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mark property as an index in the array
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class ArrayPropertyAttribute: Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// The index in the array
|
||||
/// </summary>
|
||||
public int Index { get; }
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
/// <param name="index"></param>
|
||||
public ArrayPropertyAttribute(int index)
|
||||
{
|
||||
Index = index;
|
||||
}
|
||||
(T?)_attributeByTypeAndTypeCache.GetOrAdd((type, typeof(T)), tuple => type.GetCustomAttribute(typeof(T))!);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for enum converters
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-2
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Boolean converter with support for "0"/"1" (strings)
|
||||
@@ -34,17 +34,29 @@ namespace CryptoExchange.Net.Converters
|
||||
/// </returns>
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
switch (reader.Value?.ToString().ToLower().Trim())
|
||||
var value = reader.Value?.ToString()!.ToLower().Trim();
|
||||
if (value == null || value == "")
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
return null;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "true":
|
||||
case "yes":
|
||||
case "y":
|
||||
case "1":
|
||||
case "on":
|
||||
return true;
|
||||
case "false":
|
||||
case "no":
|
||||
case "n":
|
||||
case "0":
|
||||
case "off":
|
||||
case "-1":
|
||||
return false;
|
||||
}
|
||||
|
||||
+109
-67
@@ -4,7 +4,7 @@ using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Datetime converter. Supports converting from string/long/double to DateTime and back. Numbers are assumed to be the time since 1970-01-01.
|
||||
@@ -26,21 +26,20 @@ namespace CryptoExchange.Net.Converters
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
if (reader.Value == null)
|
||||
{
|
||||
if (objectType == typeof(DateTime))
|
||||
return default(DateTime);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if(reader.TokenType is JsonToken.Integer)
|
||||
{
|
||||
var longValue = (long)reader.Value;
|
||||
if (longValue == 0 || longValue == -1)
|
||||
return objectType == typeof(DateTime) ? default(DateTime): null;
|
||||
if (longValue < 19999999999)
|
||||
return ConvertFromSeconds(longValue);
|
||||
if (longValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(longValue);
|
||||
if (longValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(longValue);
|
||||
|
||||
return ConvertFromNanoseconds(longValue);
|
||||
|
||||
return ParseFromLong(longValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonToken.Float)
|
||||
{
|
||||
@@ -56,67 +55,14 @@ namespace CryptoExchange.Net.Converters
|
||||
else if(reader.TokenType is JsonToken.String)
|
||||
{
|
||||
var stringValue = (string)reader.Value;
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
return null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stringValue) || stringValue == "0" || stringValue == "-1")
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue == "-1"
|
||||
|| (double.TryParse(stringValue, out var doubleVal) && doubleVal == 0))
|
||||
{
|
||||
return objectType == typeof(DateTime) ? default(DateTime) : null;
|
||||
|
||||
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);
|
||||
return ParseFromString(stringValue);
|
||||
}
|
||||
else if(reader.TokenType == JsonToken.Date)
|
||||
{
|
||||
@@ -129,6 +75,102 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a long value to datetime
|
||||
/// </summary>
|
||||
/// <param name="longValue"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ParseFromLong(long longValue)
|
||||
{
|
||||
if (longValue < 19999999999)
|
||||
return ConvertFromSeconds(longValue);
|
||||
if (longValue < 19999999999999)
|
||||
return ConvertFromMilliseconds(longValue);
|
||||
if (longValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds(longValue);
|
||||
|
||||
return ConvertFromNanoseconds(longValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parse a string value to datetime
|
||||
/// </summary>
|
||||
/// <param name="stringValue"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ParseFromString(string stringValue)
|
||||
{
|
||||
if (stringValue.Length == 12 && stringValue.StartsWith("202"))
|
||||
{
|
||||
// Parse 202303261200 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day)
|
||||
|| !int.TryParse(stringValue.Substring(8, 2), out var hour)
|
||||
|| !int.TryParse(stringValue.Substring(10, 2), out var minute))
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 8)
|
||||
{
|
||||
// Parse 20211103 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 4), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(6, 2), out var day))
|
||||
{
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 6)
|
||||
{
|
||||
// Parse 211103 format
|
||||
if (!int.TryParse(stringValue.Substring(0, 2), out var year)
|
||||
|| !int.TryParse(stringValue.Substring(2, 2), out var month)
|
||||
|| !int.TryParse(stringValue.Substring(4, 2), out var day))
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||
return default;
|
||||
}
|
||||
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
|
||||
{
|
||||
// Parse 1637745563.000 format
|
||||
if (doubleValue < 19999999999)
|
||||
return ConvertFromSeconds(doubleValue);
|
||||
if (doubleValue < 19999999999999)
|
||||
return ConvertFromMilliseconds((long)doubleValue);
|
||||
if (doubleValue < 19999999999999999)
|
||||
return ConvertFromMicroseconds((long)doubleValue);
|
||||
|
||||
return ConvertFromNanoseconds((long)doubleValue);
|
||||
}
|
||||
|
||||
if (stringValue.Length == 10)
|
||||
{
|
||||
// Parse 2021-11-03 format
|
||||
var values = stringValue.Split('-');
|
||||
if (!int.TryParse(values[0], out var year)
|
||||
|| !int.TryParse(values[1], out var month)
|
||||
|| !int.TryParse(values[2], out var day))
|
||||
{
|
||||
Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
|
||||
return default;
|
||||
}
|
||||
|
||||
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
return DateTime.Parse(stringValue, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
@@ -0,0 +1,27 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for serializing decimal values as string
|
||||
/// </summary>
|
||||
public class DecimalStringWriterConverter : JsonConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanRead => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType) => objectType == typeof(decimal) || objectType == typeof(decimal?);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => writer.WriteValue(((decimal?)value)?.ToString(CultureInfo.InvariantCulture) ?? null);
|
||||
}
|
||||
}
|
||||
+16
-6
@@ -7,7 +7,7 @@ using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
namespace CryptoExchange.Net.Converters
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||
@@ -15,6 +15,7 @@ namespace CryptoExchange.Net.Converters
|
||||
public class EnumConverter : JsonConverter
|
||||
{
|
||||
private bool _warnOnMissingEntry = true;
|
||||
private bool _writeAsInt;
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
@@ -22,9 +23,11 @@ namespace CryptoExchange.Net.Converters
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="writeAsInt"></param>
|
||||
/// <param name="warnOnMissingEntry"></param>
|
||||
public EnumConverter(bool warnOnMissingEntry)
|
||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
||||
{
|
||||
_writeAsInt = writeAsInt;
|
||||
_warnOnMissingEntry = warnOnMissingEntry;
|
||||
}
|
||||
|
||||
@@ -33,7 +36,7 @@ namespace CryptoExchange.Net.Converters
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.IsEnum;
|
||||
return objectType.IsEnum || Nullable.GetUnderlyingType(objectType)?.IsEnum == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -44,7 +47,7 @@ namespace CryptoExchange.Net.Converters
|
||||
mapping = AddMapping(enumType);
|
||||
|
||||
var stringValue = reader.Value?.ToString();
|
||||
if (stringValue == null)
|
||||
if (stringValue == null || stringValue == "")
|
||||
{
|
||||
// Received null value
|
||||
var emptyResult = GetDefaultValue(objectType, enumType);
|
||||
@@ -158,8 +161,15 @@ namespace CryptoExchange.Net.Converters
|
||||
}
|
||||
else
|
||||
{
|
||||
var stringValue = GetString(value.GetType(), value);
|
||||
writer.WriteValue(stringValue);
|
||||
if (!_writeAsInt)
|
||||
{
|
||||
var stringValue = GetString(value.GetType(), value);
|
||||
writer.WriteValue(stringValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteValue((int)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Json.Net message accessor
|
||||
/// </summary>
|
||||
public abstract class JsonNetMessageAccessor : IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// The json token loaded
|
||||
/// </summary>
|
||||
protected JToken? _token;
|
||||
private static readonly JsonSerializer _serializer = JsonSerializer.Create(SerializerOptions.WithConverters);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsJson { get; protected set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool OriginalDataAvailable { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? Underlying => _token;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||
{
|
||||
if (!IsJson)
|
||||
return new CallResult<object>(GetOriginalString());
|
||||
|
||||
var source = _token;
|
||||
if (path != null)
|
||||
source = GetPathNode(path.Value);
|
||||
|
||||
try
|
||||
{
|
||||
var result = source!.ToObject(type, _serializer)!;
|
||||
return new CallResult<object>(result);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||
{
|
||||
var source = _token;
|
||||
if (path != null)
|
||||
source = GetPathNode(path.Value);
|
||||
|
||||
try
|
||||
{
|
||||
var result = source!.ToObject<T>(_serializer)!;
|
||||
return new CallResult<T>(result);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var exceptionInfo = ex.ToLogString();
|
||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_token == null)
|
||||
return null;
|
||||
|
||||
if (_token.Type == JTokenType.Object)
|
||||
return NodeType.Object;
|
||||
|
||||
if (_token.Type == JTokenType.Array)
|
||||
return NodeType.Array;
|
||||
|
||||
return NodeType.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var node = GetPathNode(path);
|
||||
if (node == null)
|
||||
return null;
|
||||
|
||||
if (node.Type == JTokenType.Object)
|
||||
return NodeType.Object;
|
||||
|
||||
if (node.Type == JTokenType.Array)
|
||||
return NodeType.Array;
|
||||
|
||||
return NodeType.Value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Type == JTokenType.Object || value.Type == JTokenType.Array)
|
||||
return default;
|
||||
|
||||
return value!.Value<T>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<T?>? GetValues<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Type == JTokenType.Object)
|
||||
return default;
|
||||
|
||||
return value!.Values<T>().ToList();
|
||||
}
|
||||
|
||||
private JToken? GetPathNode(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var currentToken = _token;
|
||||
foreach (var node in path)
|
||||
{
|
||||
if (node.Type == 0)
|
||||
{
|
||||
// Int value
|
||||
var val = node.Index!.Value;
|
||||
if (currentToken!.Type != JTokenType.Array || ((JArray)currentToken).Count <= val)
|
||||
return null;
|
||||
|
||||
currentToken = currentToken[val];
|
||||
}
|
||||
else if (node.Type == 1)
|
||||
{
|
||||
// String value
|
||||
if (currentToken!.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
currentToken = currentToken[node.Property!];
|
||||
}
|
||||
else
|
||||
{
|
||||
// Property name
|
||||
if (currentToken!.Type != JTokenType.Object)
|
||||
return null;
|
||||
|
||||
currentToken = (currentToken.First as JProperty)?.Name;
|
||||
}
|
||||
|
||||
if (currentToken == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
return currentToken;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string GetOriginalString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json.Net stream message accessor
|
||||
/// </summary>
|
||||
public class JsonNetStreamMessageAccessor : JsonNetMessageAccessor, IStreamMessageAccessor
|
||||
{
|
||||
private Stream? _stream;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||
{
|
||||
if (bufferStream && stream is not MemoryStream)
|
||||
{
|
||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
||||
_stream = new MemoryStream();
|
||||
stream.CopyTo(_stream);
|
||||
_stream.Position = 0;
|
||||
}
|
||||
else if (bufferStream)
|
||||
{
|
||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
||||
_stream = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
||||
}
|
||||
|
||||
var readStream = _stream ?? stream;
|
||||
var length = readStream.CanSeek ? readStream.Length : 4096;
|
||||
using var reader = new StreamReader(readStream, Encoding.UTF8, false, (int)Math.Max(2, length), true);
|
||||
using var jsonTextReader = new JsonTextReader(reader);
|
||||
|
||||
try
|
||||
{
|
||||
_token = await JToken.LoadAsync(jsonTextReader).ConfigureAwait(false);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
}
|
||||
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString()
|
||||
{
|
||||
if (_stream is null)
|
||||
throw new NullReferenceException("Stream not initialized");
|
||||
|
||||
_stream.Position = 0;
|
||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
||||
return textReader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
_token = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Json.Net byte message accessor
|
||||
/// </summary>
|
||||
public class JsonNetByteMessageAccessor : JsonNetMessageAccessor, IByteMessageAccessor
|
||||
{
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
_bytes = data;
|
||||
|
||||
// Try getting the underlying byte[] instead of the ToArray to prevent creating a copy
|
||||
using var stream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
||||
: new MemoryStream(data.ToArray());
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, Math.Max(2, data.Length), true);
|
||||
using var jsonTextReader = new JsonTextReader(reader);
|
||||
|
||||
try
|
||||
{
|
||||
_token = JToken.Load(jsonTextReader);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString() =>
|
||||
// Netstandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
||||
#if NETSTANDARD2_0
|
||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
||||
#else
|
||||
Encoding.UTF8.GetString(_bytes.Span);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_bytes = null;
|
||||
_token = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class JsonNetMessageSerializer : IMessageSerializer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Serialize(object message) => JsonConvert.SerializeObject(message, Formatting.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Globalization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.JsonNet
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer options
|
||||
/// </summary>
|
||||
public static class SerializerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter and BoolConverter
|
||||
/// </summary>
|
||||
public static JsonSerializerSettings WithConverters => new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
Converters =
|
||||
{
|
||||
new EnumConverter(),
|
||||
new DateTimeConverter(),
|
||||
new BoolConverter()
|
||||
}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Default json serializer settings
|
||||
/// </summary>
|
||||
public static JsonSerializerSettings Default => new JsonSerializerSettings
|
||||
{
|
||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||
Culture = CultureInfo.InvariantCulture
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Node accessor
|
||||
/// </summary>
|
||||
public readonly struct NodeAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Index
|
||||
/// </summary>
|
||||
public int? Index { get; }
|
||||
/// <summary>
|
||||
/// Property name
|
||||
/// </summary>
|
||||
public string? Property { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Type (0 = int, 1 = string, 2 = prop name)
|
||||
/// </summary>
|
||||
public int Type { get; }
|
||||
|
||||
private NodeAccessor(int? index, string? property, int type)
|
||||
{
|
||||
Index = index;
|
||||
Property = property;
|
||||
Type = type;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create an int node accessor
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
|
||||
|
||||
/// <summary>
|
||||
/// Create a string node accessor
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
|
||||
|
||||
/// <summary>
|
||||
/// Create a property name node accessor
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message access definition
|
||||
/// </summary>
|
||||
public readonly struct MessagePath : IEnumerable<NodeAccessor>
|
||||
{
|
||||
private readonly List<NodeAccessor> _path;
|
||||
|
||||
internal void Add(NodeAccessor node)
|
||||
{
|
||||
_path.Add(node);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public MessagePath()
|
||||
{
|
||||
_path = new List<NodeAccessor>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new message path
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Get()
|
||||
{
|
||||
return new MessagePath();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IEnumerable implementation
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerator<NodeAccessor> GetEnumerator()
|
||||
{
|
||||
for (var i = 0; i < _path.Count; i++)
|
||||
yield return _path[i];
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message path extension methods
|
||||
/// </summary>
|
||||
public static class MessagePathExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Add a string node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="propName"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Property(this MessagePath path, string propName)
|
||||
{
|
||||
path.Add(NodeAccessor.String(propName));
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a property name node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath PropertyName(this MessagePath path)
|
||||
{
|
||||
path.Add(NodeAccessor.PropertyName());
|
||||
return path;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a int node accessor
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <returns></returns>
|
||||
public static MessagePath Index(this MessagePath path, int index)
|
||||
{
|
||||
path.Add(NodeAccessor.Int(index));
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace CryptoExchange.Net.Converters.MessageParsing
|
||||
{
|
||||
/// <summary>
|
||||
/// Message node type
|
||||
/// </summary>
|
||||
public enum NodeType
|
||||
{
|
||||
/// <summary>
|
||||
/// Array node
|
||||
/// </summary>
|
||||
Array,
|
||||
/// <summary>
|
||||
/// Object node
|
||||
/// </summary>
|
||||
Object,
|
||||
/// <summary>
|
||||
/// Value node
|
||||
/// </summary>
|
||||
Value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for arrays to objects. Can deserialize data like [0.1, 0.2, "test"] to an object. Mapping is done by marking the class with [JsonConverter(typeof(ArrayConverter))] and the properties
|
||||
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
||||
/// </summary>
|
||||
public class ArrayConverter : JsonConverterFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(ArrayConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
private class ArrayPropertyInfo
|
||||
{
|
||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
||||
public Type? JsonConverterType { get; set; }
|
||||
public bool DefaultDeserialization { get; set; }
|
||||
public Type TargetType { get; set; } = null!;
|
||||
}
|
||||
|
||||
private class ArrayConverterInner<T> : JsonConverter<T>
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, List<ArrayPropertyInfo>> _typeAttributesCache = new ConcurrentDictionary<Type, List<ArrayPropertyInfo>>();
|
||||
private static readonly ConcurrentDictionary<Type, JsonSerializerOptions> _converterOptionsCache = new ConcurrentDictionary<Type, JsonSerializerOptions>();
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
return;
|
||||
}
|
||||
|
||||
writer.WriteStartArray();
|
||||
|
||||
var valueType = value.GetType();
|
||||
if (!_typeAttributesCache.TryGetValue(valueType, out var typeAttributes))
|
||||
typeAttributes = CacheTypeAttributes(valueType);
|
||||
|
||||
var ordered = typeAttributes.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
||||
var last = -1;
|
||||
foreach (var prop in ordered)
|
||||
{
|
||||
if (prop.ArrayProperty.Index == last)
|
||||
continue;
|
||||
|
||||
while (prop.ArrayProperty.Index != last + 1)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
last += 1;
|
||||
}
|
||||
|
||||
last = prop.ArrayProperty.Index;
|
||||
|
||||
var objValue = prop.PropertyInfo.GetValue(value);
|
||||
if (objValue == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
JsonSerializerOptions? typeOptions = null;
|
||||
if (prop.JsonConverterType != null)
|
||||
{
|
||||
var converter = (JsonConverter)Activator.CreateInstance(prop.JsonConverterType)!;
|
||||
typeOptions = new JsonSerializerOptions();
|
||||
typeOptions.Converters.Clear();
|
||||
typeOptions.Converters.Add(converter);
|
||||
}
|
||||
|
||||
if (prop.JsonConverterType == null && IsSimple(prop.PropertyInfo.PropertyType))
|
||||
{
|
||||
if (prop.TargetType == typeof(string))
|
||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||
else if(prop.TargetType.IsEnum)
|
||||
writer.WriteStringValue(EnumConverter.GetString(objValue));
|
||||
else if (prop.TargetType == typeof(bool))
|
||||
writer.WriteBooleanValue((bool)objValue);
|
||||
else
|
||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteEndArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
return default;
|
||||
|
||||
var result = Activator.CreateInstance(typeToConvert)!;
|
||||
return (T)ParseObject(ref reader, result, typeToConvert, options);
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type type)
|
||||
{
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||
{
|
||||
// nullable type, check if the nested type is simple.
|
||||
return IsSimple(type.GetGenericArguments()[0]);
|
||||
}
|
||||
return type.IsPrimitive
|
||||
|| type.IsEnum
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
|
||||
private static List<ArrayPropertyInfo> CacheTypeAttributes(Type type)
|
||||
{
|
||||
var attributes = new List<ArrayPropertyInfo>();
|
||||
var properties = type.GetProperties();
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
||||
if (att == null)
|
||||
continue;
|
||||
|
||||
attributes.Add(new ArrayPropertyInfo
|
||||
{
|
||||
ArrayProperty = att,
|
||||
PropertyInfo = property,
|
||||
DefaultDeserialization = property.GetCustomAttribute<JsonConversionAttribute>() != null,
|
||||
JsonConverterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? property.PropertyType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType,
|
||||
TargetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType
|
||||
});
|
||||
}
|
||||
|
||||
_typeAttributesCache.TryAdd(type, attributes);
|
||||
return attributes;
|
||||
}
|
||||
|
||||
private static object ParseObject(ref Utf8JsonReader reader, object result, Type objectType, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartArray)
|
||||
throw new Exception("Not an array");
|
||||
|
||||
if (!_typeAttributesCache.TryGetValue(objectType, out var attributes))
|
||||
attributes = CacheTypeAttributes(objectType);
|
||||
|
||||
int index = 0;
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndArray)
|
||||
break;
|
||||
|
||||
var indexAttributes = attributes.Where(a => a.ArrayProperty.Index == index);
|
||||
if (!indexAttributes.Any())
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var attribute in indexAttributes)
|
||||
{
|
||||
var targetType = attribute.TargetType;
|
||||
object? value = null;
|
||||
if (attribute.JsonConverterType != null)
|
||||
{
|
||||
if (!_converterOptionsCache.TryGetValue(attribute.JsonConverterType, out var newOptions))
|
||||
{
|
||||
var converter = (JsonConverter)Activator.CreateInstance(attribute.JsonConverterType)!;
|
||||
newOptions = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = SerializerOptions.WithConverters.NumberHandling,
|
||||
PropertyNameCaseInsensitive = SerializerOptions.WithConverters.PropertyNameCaseInsensitive,
|
||||
Converters = { converter },
|
||||
};
|
||||
_converterOptionsCache.TryAdd(attribute.JsonConverterType, newOptions);
|
||||
}
|
||||
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, newOptions);
|
||||
}
|
||||
else if (attribute.DefaultDeserialization)
|
||||
{
|
||||
// Use default deserialization
|
||||
value = JsonDocument.ParseValue(ref reader).Deserialize(attribute.PropertyInfo.PropertyType, SerializerOptions.WithConverters);
|
||||
}
|
||||
else
|
||||
{
|
||||
value = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.Null => null,
|
||||
JsonTokenType.False => false,
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetDecimal(),
|
||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
||||
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
||||
};
|
||||
}
|
||||
|
||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||
attribute.PropertyInfo.SetValue(result, value);
|
||||
else
|
||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Bool converter
|
||||
/// </summary>
|
||||
public class BoolConverter : JsonConverterFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(BoolConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
private class BoolConverterInner<T> : JsonConverter<T>
|
||||
{
|
||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
|
||||
|
||||
public bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.True)
|
||||
return true;
|
||||
|
||||
if (reader.TokenType == JsonTokenType.False)
|
||||
return false;
|
||||
|
||||
var value = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
||||
_ => null
|
||||
};
|
||||
|
||||
value = value?.ToLowerInvariant().Trim();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
if (typeToConvert == typeof(bool))
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
|
||||
return default;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case "true":
|
||||
case "yes":
|
||||
case "y":
|
||||
case "1":
|
||||
case "on":
|
||||
return true;
|
||||
case "false":
|
||||
case "no":
|
||||
case "n":
|
||||
case "0":
|
||||
case "off":
|
||||
case "-1":
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new SerializationException($"Can't convert bool value {value}");
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value is bool boolVal)
|
||||
writer.WriteBooleanValue(boolVal);
|
||||
else
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for comma seperated enum values
|
||||
/// </summary>
|
||||
public class CommaSplitEnumConverter<T> : JsonConverter<IEnumerable<T>> where T : Enum
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
return (reader.GetString()?.Split(',').Select(x => EnumConverter.ParseString<T>(x)).ToArray() ?? new T[0])!;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, IEnumerable<T> value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Date time converter
|
||||
/// </summary>
|
||||
public class DateTimeConverter : JsonConverterFactory
|
||||
{
|
||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
||||
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
Type converterType = typeof(DateTimeConverterInner<>).MakeGenericType(typeToConvert);
|
||||
return (JsonConverter)Activator.CreateInstance(converterType)!;
|
||||
}
|
||||
|
||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
||||
{
|
||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
|
||||
|
||||
private DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.Null)
|
||||
{
|
||||
if (typeToConvert == typeof(DateTime))
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is JsonTokenType.Number)
|
||||
{
|
||||
var longValue = reader.GetDouble();
|
||||
if (longValue == 0 || longValue < 0)
|
||||
return default;
|
||||
|
||||
return ParseFromDouble(longValue);
|
||||
}
|
||||
else if (reader.TokenType is JsonTokenType.String)
|
||||
{
|
||||
var stringValue = reader.GetString();
|
||||
if (string.IsNullOrWhiteSpace(stringValue)
|
||||
|| stringValue == "-1"
|
||||
|| stringValue == "0001-01-01T00:00:00Z"
|
||||
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return ParseFromString(stringValue!);
|
||||
}
|
||||
else
|
||||
{
|
||||
return reader.GetDateTime();
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
var dtValue = (DateTime)(object)value;
|
||||
if (dtValue == default)
|
||||
writer.WriteStringValue(default(DateTime));
|
||||
else
|
||||
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// Convert a seconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="seconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||
/// <summary>
|
||||
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="milliseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||
/// <summary>
|
||||
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="microseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||
/// <summary>
|
||||
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||
/// </summary>
|
||||
/// <param name="nanoseconds"></param>
|
||||
/// <returns></returns>
|
||||
public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
|
||||
/// <summary>
|
||||
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
|
||||
/// </summary>
|
||||
/// <param name="time"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("time")]
|
||||
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Decimal converter
|
||||
/// </summary>
|
||||
public class DecimalConverter : JsonConverter<decimal?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override decimal? 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) || string.Equals("null", value, StringComparison.OrdinalIgnoreCase))
|
||||
return null;
|
||||
|
||||
if (string.Equals("Infinity", value, StringComparison.Ordinal))
|
||||
// Infinity returned by the server, default to max value
|
||||
return decimal.MaxValue;
|
||||
|
||||
try
|
||||
{
|
||||
return decimal.Parse(value, 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)
|
||||
{
|
||||
if (value == null)
|
||||
writer.WriteNullValue();
|
||||
else
|
||||
writer.WriteNumberValue(value.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for serializing decimal values as string
|
||||
/// </summary>
|
||||
public class DecimalStringWriterConverter : JsonConverter<decimal>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter mapping to an object but also handles when an empty array is send
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
public class EmptyArrayObjectConverter<T> : JsonConverter<T>
|
||||
{
|
||||
private static JsonSerializerOptions _defaultConverter = SerializerOptions.WithConverters;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override T? Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options)
|
||||
{
|
||||
switch (reader.TokenType)
|
||||
{
|
||||
case JsonTokenType.StartArray:
|
||||
_ = JsonSerializer.Deserialize<object[]>(ref reader, options);
|
||||
return default;
|
||||
case JsonTokenType.StartObject:
|
||||
return JsonSerializer.Deserialize<T>(ref reader, _defaultConverter);
|
||||
};
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
=> JsonSerializer.Serialize(writer, (object?)value, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
using CryptoExchange.Net.Attributes;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||
/// </summary>
|
||||
public class EnumConverter : JsonConverterFactory
|
||||
{
|
||||
private bool _warnOnMissingEntry = true;
|
||||
private bool _writeAsInt;
|
||||
private static readonly ConcurrentDictionary<Type, List<KeyValuePair<object, string>>> _mapping = new();
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
public EnumConverter() { }
|
||||
|
||||
/// <summary>
|
||||
/// </summary>
|
||||
/// <param name="writeAsInt"></param>
|
||||
/// <param name="warnOnMissingEntry"></param>
|
||||
public EnumConverter(bool writeAsInt, bool warnOnMissingEntry)
|
||||
{
|
||||
_writeAsInt = writeAsInt;
|
||||
_warnOnMissingEntry = warnOnMissingEntry;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanConvert(Type typeToConvert)
|
||||
{
|
||||
return typeToConvert.IsEnum || Nullable.GetUnderlyingType(typeToConvert)?.IsEnum == true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
|
||||
typeof(EnumConverterInner<>).MakeGenericType(
|
||||
new Type[] { typeToConvert }),
|
||||
BindingFlags.Instance | BindingFlags.Public,
|
||||
binder: null,
|
||||
args: new object[] { _writeAsInt, _warnOnMissingEntry },
|
||||
culture: null)!;
|
||||
|
||||
return converter;
|
||||
}
|
||||
|
||||
private static List<KeyValuePair<object, string>> AddMapping(Type objectType)
|
||||
{
|
||||
var mapping = new List<KeyValuePair<object, string>>();
|
||||
var enumMembers = objectType.GetMembers();
|
||||
foreach (var member in enumMembers)
|
||||
{
|
||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||
foreach (MapAttribute attribute in maps)
|
||||
{
|
||||
foreach (var value in attribute.Values)
|
||||
mapping.Add(new KeyValuePair<object, string>(Enum.Parse(objectType, member.Name), value));
|
||||
}
|
||||
}
|
||||
_mapping.TryAdd(objectType, mapping);
|
||||
return mapping;
|
||||
}
|
||||
|
||||
private class EnumConverterInner<T> : JsonConverter<T>
|
||||
{
|
||||
private bool _warnOnMissingEntry = true;
|
||||
private bool _writeAsInt;
|
||||
|
||||
public EnumConverterInner(bool writeAsInt, bool warnOnMissingEntry)
|
||||
{
|
||||
_warnOnMissingEntry = warnOnMissingEntry;
|
||||
_writeAsInt = writeAsInt;
|
||||
}
|
||||
|
||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var enumType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
|
||||
if (!_mapping.TryGetValue(enumType, out var mapping))
|
||||
mapping = AddMapping(enumType);
|
||||
|
||||
var stringValue = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.GetInt16().ToString(),
|
||||
JsonTokenType.True => reader.GetBoolean().ToString(),
|
||||
JsonTokenType.False => reader.GetBoolean().ToString(),
|
||||
JsonTokenType.Null => null,
|
||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(stringValue))
|
||||
{
|
||||
// Received null value
|
||||
var emptyResult = GetDefaultValue(typeToConvert, enumType);
|
||||
if (emptyResult != null)
|
||||
// If the property we're parsing to isn't nullable there isn't a correct way to return this as null will either throw an exception (.net framework) or the default enum value (dotnet core).
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
||||
|
||||
return (T?)emptyResult;
|
||||
}
|
||||
|
||||
if (!GetValue(enumType, mapping, stringValue!, out var result))
|
||||
{
|
||||
var defaultValue = GetDefaultValue(typeToConvert, enumType);
|
||||
if (string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
if (defaultValue != null)
|
||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {enumType.Name}. If you think {enumType.Name} should be nullable please open an issue on the Github repo");
|
||||
}
|
||||
else
|
||||
{
|
||||
// We received an enum value but weren't able to parse it.
|
||||
if (_warnOnMissingEntry)
|
||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
||||
}
|
||||
|
||||
return (T?)defaultValue;
|
||||
}
|
||||
|
||||
return (T?)result;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!_writeAsInt)
|
||||
{
|
||||
var stringValue = GetString(value.GetType(), value);
|
||||
writer.WriteStringValue(stringValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteNumberValue((int)Convert.ChangeType(value, typeof(int)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static object? GetDefaultValue(Type objectType, Type enumType)
|
||||
{
|
||||
if (Nullable.GetUnderlyingType(objectType) != null)
|
||||
return null;
|
||||
|
||||
return Activator.CreateInstance(enumType); // return default value
|
||||
}
|
||||
|
||||
private static bool GetValue(Type objectType, List<KeyValuePair<object, string>> enumMapping, string value, out object? result)
|
||||
{
|
||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
||||
var mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
||||
if (mapping.Equals(default(KeyValuePair<object, string>)))
|
||||
mapping = enumMapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (!mapping.Equals(default(KeyValuePair<object, string>)))
|
||||
{
|
||||
result = mapping.Key;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
||||
{
|
||||
var intValue = int.Parse(value);
|
||||
result = Enum.ToObject(objectType, intValue);
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// If no explicit mapping is found try to parse string
|
||||
result = Enum.Parse(objectType, value, true);
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
public static string? GetString<T>(T enumValue) => GetString(typeof(T), enumValue);
|
||||
|
||||
/// <summary>
|
||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||
/// </summary>
|
||||
/// <param name="objectType"></param>
|
||||
/// <param name="enumValue"></param>
|
||||
/// <returns></returns>
|
||||
[return: NotNullIfNotNull("enumValue")]
|
||||
public static string? GetString(Type objectType, object? enumValue)
|
||||
{
|
||||
objectType = Nullable.GetUnderlyingType(objectType) ?? objectType;
|
||||
|
||||
if (!_mapping.TryGetValue(objectType, out var mapping))
|
||||
mapping = AddMapping(objectType);
|
||||
|
||||
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,31 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Attribute for allowing specifying a JsonConverter with constructor parameters
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property)]
|
||||
public class JsonConverterCtorAttribute : JsonConverterAttribute
|
||||
{
|
||||
private readonly object[] _parameters;
|
||||
private readonly Type _type;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public JsonConverterCtorAttribute(Type type, params object[] parameters)
|
||||
{
|
||||
_type = type;
|
||||
_parameters = parameters;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override JsonConverter CreateConverter(Type typeToConvert)
|
||||
{
|
||||
return (JsonConverter)Activator.CreateInstance(_type, _parameters)!;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,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,42 @@
|
||||
using System;
|
||||
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();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return reader.GetString();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Replace a value on a string property
|
||||
/// </summary>
|
||||
public class ReplaceConverter : JsonConverter<string>
|
||||
{
|
||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public ReplaceConverter(params string[] replaceSets)
|
||||
{
|
||||
_replacementSets = replaceSets.Select(x =>
|
||||
{
|
||||
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
|
||||
if (split.Length != 2)
|
||||
throw new ArgumentException("Invalid replacement config");
|
||||
return (split[0], split[1]);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
var value = reader.GetString();
|
||||
foreach (var set in _replacementSets)
|
||||
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer options
|
||||
/// </summary>
|
||||
public static class SerializerOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Json serializer settings which includes the EnumConverter, DateTimeConverter, BoolConverter and DecimalConverter
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions WithConverters { get; } = new JsonSerializerOptions
|
||||
{
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
PropertyNameCaseInsensitive = false,
|
||||
Converters =
|
||||
{
|
||||
new DateTimeConverter(),
|
||||
new EnumConverter(),
|
||||
new BoolConverter(),
|
||||
new DecimalConverter(),
|
||||
new IntConverter(),
|
||||
new LongConverter()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <summary>
|
||||
/// System.Text.Json message accessor
|
||||
/// </summary>
|
||||
public abstract class SystemTextJsonMessageAccessor : IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// The JsonDocument loaded
|
||||
/// </summary>
|
||||
protected JsonDocument? _document;
|
||||
|
||||
private static readonly JsonSerializerOptions _serializerOptions = SerializerOptions.WithConverters;
|
||||
private readonly JsonSerializerOptions? _customSerializerOptions;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsJson { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract bool OriginalDataAvailable { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? Underlying => throw new NotImplementedException();
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonMessageAccessor()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
|
||||
{
|
||||
_customSerializerOptions = options;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
||||
{
|
||||
if (!IsJson)
|
||||
return new CallResult<object>(GetOriginalString());
|
||||
|
||||
if (_document == null)
|
||||
throw new InvalidOperationException("No json document loaded");
|
||||
|
||||
try
|
||||
{
|
||||
var result = _document.Deserialize(type, _customSerializerOptions ?? _serializerOptions);
|
||||
return new CallResult<object>(result!);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var info = $"Deserialize unknown Exception: {ex.Message}";
|
||||
return new CallResult<object>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
||||
{
|
||||
if (_document == null)
|
||||
throw new InvalidOperationException("No json document loaded");
|
||||
|
||||
try
|
||||
{
|
||||
var result = _document.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
||||
return new CallResult<T>(result!);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
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]"));
|
||||
}
|
||||
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 />
|
||||
public NodeType? GetNodeType()
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_document == null)
|
||||
throw new InvalidOperationException("No json document loaded");
|
||||
|
||||
return _document.RootElement.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => NodeType.Object,
|
||||
JsonValueKind.Array => NodeType.Array,
|
||||
_ => NodeType.Value
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public NodeType? GetNodeType(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var node = GetPathNode(path);
|
||||
if (!node.HasValue)
|
||||
return null;
|
||||
|
||||
return node.Value.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => NodeType.Object,
|
||||
JsonValueKind.Array => NodeType.Array,
|
||||
_ => NodeType.Value
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? GetValue<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Value.ValueKind == JsonValueKind.Object || value.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
try
|
||||
{
|
||||
return value.Value.Deserialize<T>(_customSerializerOptions ?? _serializerOptions);
|
||||
}
|
||||
catch { }
|
||||
|
||||
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>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<T?>? GetValues<T>(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
var value = GetPathNode(path);
|
||||
if (value == null)
|
||||
return default;
|
||||
|
||||
if (value.Value.ValueKind != JsonValueKind.Array)
|
||||
return default;
|
||||
|
||||
return value.Value.Deserialize<List<T>>()!;
|
||||
}
|
||||
|
||||
private JsonElement? GetPathNode(MessagePath path)
|
||||
{
|
||||
if (!IsJson)
|
||||
throw new InvalidOperationException("Can't access json data on non-json message");
|
||||
|
||||
if (_document == null)
|
||||
throw new InvalidOperationException("No json document loaded");
|
||||
|
||||
JsonElement? currentToken = _document.RootElement;
|
||||
foreach (var node in path)
|
||||
{
|
||||
if (node.Type == 0)
|
||||
{
|
||||
// Int value
|
||||
var val = node.Index!.Value;
|
||||
if (currentToken!.Value.ValueKind != JsonValueKind.Array || currentToken.Value.GetArrayLength() <= val)
|
||||
return null;
|
||||
|
||||
currentToken = currentToken.Value[val];
|
||||
}
|
||||
else if (node.Type == 1)
|
||||
{
|
||||
// String value
|
||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
if (!currentToken.Value.TryGetProperty(node.Property!, out var token))
|
||||
return null;
|
||||
currentToken = token;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Property name
|
||||
if (currentToken!.Value.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
if (currentToken == null)
|
||||
return null;
|
||||
}
|
||||
|
||||
return currentToken;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract string GetOriginalString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract void Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json stream message accessor
|
||||
/// </summary>
|
||||
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
|
||||
{
|
||||
private Stream? _stream;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonStreamMessageAccessor(): base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonStreamMessageAccessor(JsonSerializerOptions options): base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<CallResult> Read(Stream stream, bool bufferStream)
|
||||
{
|
||||
if (bufferStream && stream is not MemoryStream)
|
||||
{
|
||||
// We need to be buffer the stream, and it's not currently a seekable stream, so copy it to a new memory stream
|
||||
_stream = new MemoryStream();
|
||||
stream.CopyTo(_stream);
|
||||
_stream.Position = 0;
|
||||
}
|
||||
else if (bufferStream)
|
||||
{
|
||||
// We need to buffer the stream, and the current stream is seekable, store as is
|
||||
_stream = stream;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We don't need to buffer the stream, so don't bother keeping the reference
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_document = await JsonDocument.ParseAsync(_stream ?? stream).ConfigureAwait(false);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString()
|
||||
{
|
||||
if (_stream is null)
|
||||
throw new NullReferenceException("Stream not initialized");
|
||||
|
||||
_stream.Position = 0;
|
||||
using var textReader = new StreamReader(_stream, Encoding.UTF8, false, 1024, true);
|
||||
return textReader.ReadToEnd();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_stream?.Dispose();
|
||||
_stream = null;
|
||||
_document?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// System.Text.Json byte message accessor
|
||||
/// </summary>
|
||||
public class SystemTextJsonByteMessageAccessor : SystemTextJsonMessageAccessor, IByteMessageAccessor
|
||||
{
|
||||
private ReadOnlyMemory<byte> _bytes;
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonByteMessageAccessor() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ctor
|
||||
/// </summary>
|
||||
public SystemTextJsonByteMessageAccessor(JsonSerializerOptions options) : base(options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public CallResult Read(ReadOnlyMemory<byte> data)
|
||||
{
|
||||
_bytes = data;
|
||||
|
||||
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);
|
||||
IsJson = true;
|
||||
return new CallResult(null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Not a json message
|
||||
IsJson = false;
|
||||
return new CallResult(new ServerError("JsonError: " + ex.Message));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string GetOriginalString() =>
|
||||
// NetStandard 2.0 doesn't support GetString from a ReadonlySpan<byte>, so use ToArray there instead
|
||||
#if NETSTANDARD2_0
|
||||
Encoding.UTF8.GetString(_bytes.ToArray());
|
||||
#else
|
||||
Encoding.UTF8.GetString(_bytes.Span);
|
||||
#endif
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OriginalDataAvailable => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Clear()
|
||||
{
|
||||
_bytes = null;
|
||||
_document?.Dispose();
|
||||
_document = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
using CryptoExchange.Net.Interfaces;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class SystemTextJsonMessageSerializer : IMessageSerializer
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Serialize(object message) => JsonSerializer.Serialize(message, SerializerOptions.WithConverters);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,32 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net9.0</TargetFrameworks>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PackageId>CryptoExchange.Net</PackageId>
|
||||
<Authors>JKorf</Authors>
|
||||
<Description>A base package for implementing cryptocurrency API's</Description>
|
||||
<PackageVersion>6.1.4</PackageVersion>
|
||||
<AssemblyVersion>6.1.4</AssemblyVersion>
|
||||
<FileVersion>6.1.4</FileVersion>
|
||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||
<PackageVersion>8.8.0</PackageVersion>
|
||||
<AssemblyVersion>8.8.0</AssemblyVersion>
|
||||
<FileVersion>8.8.0</FileVersion>
|
||||
<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>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/JKorf/CryptoExchange.Net.git</RepositoryUrl>
|
||||
<PackageProjectUrl>https://github.com/JKorf/CryptoExchange.Net</PackageProjectUrl>
|
||||
<NeutralLanguage>en</NeutralLanguage>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageReleaseNotes>6.1.4 - Added BoolConverter, Added parameter for logging warning message on missing enum entry to EnumConverter</PackageReleaseNotes>
|
||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>10.0</LangVersion>
|
||||
<LangVersion>12.0</LangVersion>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
@@ -28,7 +35,7 @@
|
||||
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Deterministic Build" Condition="'$(Configuration)' == 'Release'">
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0">
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
@@ -37,19 +44,21 @@
|
||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0">
|
||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="6.0.0">
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="3.1.32" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="3.1.32" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.32" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,5 +1,11 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -8,14 +14,28 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
public static class ExchangeHelpers
|
||||
{
|
||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||
|
||||
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
||||
{
|
||||
{ 1, "F" },
|
||||
{ 2, "G" },
|
||||
{ 3, "H" },
|
||||
{ 4, "J" },
|
||||
{ 5, "K" },
|
||||
{ 6, "M" },
|
||||
{ 7, "N" },
|
||||
{ 8, "Q" },
|
||||
{ 9, "U" },
|
||||
{ 10, "V" },
|
||||
{ 11, "X" },
|
||||
{ 12, "Z" },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The last used id, use NextId() to get the next id and up this
|
||||
/// </summary>
|
||||
private static int _lastId;
|
||||
/// <summary>
|
||||
/// Lock for id generating
|
||||
/// </summary>
|
||||
private static object _idLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Clamp a value between a min and max
|
||||
@@ -55,6 +75,11 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
value -= offset;
|
||||
}
|
||||
else if(roundingType == RoundingType.Up)
|
||||
{
|
||||
if (offset != 0)
|
||||
value += (step.Value - offset);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (offset < step / 2)
|
||||
@@ -107,17 +132,23 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds a value down to
|
||||
/// Rounds a value down
|
||||
/// </summary>
|
||||
/// <param name="i"></param>
|
||||
/// <param name="decimalPlaces"></param>
|
||||
/// <returns></returns>
|
||||
public static decimal RoundDown(decimal i, double decimalPlaces)
|
||||
{
|
||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
||||
return Math.Floor(i * power) / power;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds a value up
|
||||
/// </summary>
|
||||
public static decimal RoundUp(decimal i, double decimalPlaces)
|
||||
{
|
||||
var power = Convert.ToDecimal(Math.Pow(10, decimalPlaces));
|
||||
return Math.Ceiling(i * power) / power;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strips any trailing zero's of a decimal value, useful when converting the value to string.
|
||||
/// </summary>
|
||||
@@ -129,16 +160,119 @@ namespace CryptoExchange.Net
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 statically stored so it is guaranteed to be unique
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int NextId()
|
||||
public static int NextId() => Interlocked.Increment(ref _lastId);
|
||||
|
||||
/// <summary>
|
||||
/// Return the last unique id that was generated
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public static int LastId() => _lastId;
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="length">Length of the random string</param>
|
||||
/// <returns></returns>
|
||||
public static string RandomString(int length)
|
||||
{
|
||||
lock (_idLock)
|
||||
var randomChars = new char[length];
|
||||
|
||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[RandomNumberGenerator.GetInt32(0, _allowedRandomChars.Length)];
|
||||
#else
|
||||
var random = new Random();
|
||||
for (int i = 0; i < length; i++)
|
||||
randomChars[i] = _allowedRandomChars[random.Next(0, _allowedRandomChars.Length)];
|
||||
#endif
|
||||
|
||||
return new string(randomChars);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a random string of specified length
|
||||
/// </summary>
|
||||
/// <param name="source">The initial string</param>
|
||||
/// <param name="totalLength">Total length of the resulting string</param>
|
||||
/// <returns></returns>
|
||||
public static string AppendRandomString(string source, int totalLength)
|
||||
{
|
||||
if (totalLength < source.Length)
|
||||
throw new ArgumentException("Total length smaller than source string length", nameof(totalLength));
|
||||
|
||||
if (totalLength == source.Length)
|
||||
return source;
|
||||
|
||||
return source + RandomString(totalLength - source.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the month representation for futures symbol based on the delivery month
|
||||
/// </summary>
|
||||
/// <param name="time">Delivery time</param>
|
||||
/// <returns></returns>
|
||||
public static string GetDeliveryMonthSymbol(DateTime time) => _monthSymbols[time.Month];
|
||||
|
||||
/// <summary>
|
||||
/// Execute multiple requests to retrieve multiple pages of the result set
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the client</typeparam>
|
||||
/// <typeparam name="U">Type of the request</typeparam>
|
||||
/// <param name="paginatedFunc">The func to execute with each request</param>
|
||||
/// <param name="request">The request parameters</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns></returns>
|
||||
public static async IAsyncEnumerable<ExchangeWebResult<IEnumerable<T>>> ExecutePages<T, U>(Func<U, INextPageToken?, CancellationToken, Task<ExchangeWebResult<IEnumerable<T>>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||
{
|
||||
var result = new List<T>();
|
||||
ExchangeWebResult<IEnumerable<T>> batch;
|
||||
INextPageToken? nextPageToken = null;
|
||||
while (true)
|
||||
{
|
||||
_lastId += 1;
|
||||
return _lastId;
|
||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||
yield return batch;
|
||||
if (!batch || ct.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
result.AddRange(batch.Data);
|
||||
nextPageToken = batch.NextPageToken;
|
||||
if (nextPageToken == null)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol as retrieved from the exchange</param>
|
||||
/// <param name="quantity">Quantity to trade</param>
|
||||
/// <param name="price">Price to trade at</param>
|
||||
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
|
||||
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
|
||||
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
|
||||
{
|
||||
adjustedPrice = price;
|
||||
adjustedQuantity = quantity;
|
||||
var minNotionalAdjust = false;
|
||||
|
||||
if (price != null)
|
||||
{
|
||||
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
|
||||
adjustedPrice = symbol.PriceSignificantFigures.HasValue ? RoundToSignificantDigits(adjustedPrice.Value, symbol.PriceSignificantFigures.Value, RoundingType.Closest) : adjustedPrice;
|
||||
adjustedPrice = symbol.PriceDecimals.HasValue ? RoundDown(price.Value, symbol.PriceDecimals.Value) : adjustedPrice;
|
||||
if (adjustedPrice != 0 && adjustedPrice * quantity < symbol.MinNotionalValue)
|
||||
{
|
||||
adjustedQuantity = symbol.MinNotionalValue.Value / adjustedPrice.Value;
|
||||
minNotionalAdjust = true;
|
||||
}
|
||||
}
|
||||
|
||||
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
|
||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
@@ -29,18 +29,6 @@ namespace CryptoExchange.Net
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a parameter
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="converter"></param>
|
||||
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value, JsonConverter converter)
|
||||
{
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a parameter
|
||||
/// </summary>
|
||||
@@ -52,18 +40,6 @@ namespace CryptoExchange.Net
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a parameter
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="converter"></param>
|
||||
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value, JsonConverter converter)
|
||||
{
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
@@ -76,19 +52,6 @@ namespace CryptoExchange.Net
|
||||
parameters.Add(key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an optional parameter. Not added if value is null
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <param name="key"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <param name="converter"></param>
|
||||
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value, JsonConverter converter)
|
||||
{
|
||||
if (value != null)
|
||||
parameters.Add(key, JsonConvert.SerializeObject(value, converter));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a query string of the specified parameters
|
||||
/// </summary>
|
||||
@@ -96,7 +59,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="urlEncodeValues">Whether or not the values should be url encoded</param>
|
||||
/// <param name="serializationType">How to serialize array parameters</param>
|
||||
/// <returns></returns>
|
||||
public static string CreateParamString(this Dictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||
{
|
||||
var uriString = string.Empty;
|
||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
||||
@@ -104,17 +67,22 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
if (serializationType == ArrayParametersSerialization.Array)
|
||||
{
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={v}"))}&";
|
||||
uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
|
||||
}
|
||||
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||
{
|
||||
var array = (Array)arrayEntry.Value;
|
||||
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}"));
|
||||
uriString += "&";
|
||||
}
|
||||
else
|
||||
{
|
||||
var array = (Array)arrayEntry.Value;
|
||||
uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(a.ToString())}"));
|
||||
uriString += "&";
|
||||
uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
|
||||
}
|
||||
}
|
||||
|
||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(s.Value.ToString()) : s.Value)}"))}";
|
||||
uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}";
|
||||
uriString = uriString.TrimEnd('&');
|
||||
return uriString;
|
||||
}
|
||||
@@ -124,141 +92,27 @@ namespace CryptoExchange.Net
|
||||
/// </summary>
|
||||
/// <param name="parameters"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToFormData(this SortedDictionary<string, object> parameters)
|
||||
public static string ToFormData(this IDictionary<string, object> parameters)
|
||||
{
|
||||
var formData = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var kvp in parameters)
|
||||
{
|
||||
if (kvp.Value is null)
|
||||
continue;
|
||||
|
||||
if (kvp.Value.GetType().IsArray)
|
||||
{
|
||||
var array = (Array)kvp.Value;
|
||||
foreach (var value in array)
|
||||
formData.Add(kvp.Key, value.ToString());
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
||||
}
|
||||
else
|
||||
{
|
||||
formData.Add(kvp.Key, kvp.Value.ToString());
|
||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||
}
|
||||
}
|
||||
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>
|
||||
/// String to JToken
|
||||
/// </summary>
|
||||
/// <param name="stringData"></param>
|
||||
/// <param name="logger"></param>
|
||||
/// <returns></returns>
|
||||
public static JToken? ToJToken(this string stringData, ILogger? logger = null)
|
||||
{
|
||||
if (string.IsNullOrEmpty(stringData))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return JToken.Parse(stringData);
|
||||
}
|
||||
catch (JsonReaderException jre)
|
||||
{
|
||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}. Data: {stringData}";
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
catch (JsonSerializationException jse)
|
||||
{
|
||||
var info = $"Deserialize JsonSerializationException: {jse.Message}. Data: {stringData}";
|
||||
logger?.Log(LogLevel.Error, info);
|
||||
if (logger == null) Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | {info}");
|
||||
return null;
|
||||
}
|
||||
return formData.ToString()!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -336,6 +190,16 @@ namespace CryptoExchange.Net
|
||||
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format a string to RFC3339/ISO8601 string
|
||||
/// </summary>
|
||||
/// <param name="dateTime"></param>
|
||||
/// <returns></returns>
|
||||
public static string ToRfc3339String(this DateTime dateTime)
|
||||
{
|
||||
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Format an exception and inner exception to a readable string
|
||||
/// </summary>
|
||||
@@ -380,26 +244,6 @@ namespace CryptoExchange.Net
|
||||
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>
|
||||
/// Create a new uri with the provided parameters as query
|
||||
/// </summary>
|
||||
@@ -407,7 +251,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="baseUri"></param>
|
||||
/// <param name="arraySerialization"></param>
|
||||
/// <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();
|
||||
uriBuilder.Scheme = baseUri.Scheme;
|
||||
@@ -417,16 +261,33 @@ namespace CryptoExchange.Net
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
||||
foreach (var parameter in parameters)
|
||||
{
|
||||
if(parameter.Value.GetType().IsArray)
|
||||
if (parameter.Value.GetType().IsArray)
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
{
|
||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
@@ -450,14 +311,31 @@ namespace CryptoExchange.Net
|
||||
{
|
||||
if (parameter.Value.GetType().IsArray)
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
httpValueCollection.Add(arraySerialization == ArrayParametersSerialization.Array ? parameter.Key + "[]" : parameter.Key, item.ToString());
|
||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in (object[])parameter.Value)
|
||||
{
|
||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
uriBuilder.Query = httpValueCollection.ToString();
|
||||
return uriBuilder.Uri;
|
||||
}
|
||||
@@ -469,7 +347,7 @@ namespace CryptoExchange.Net
|
||||
/// <param name="name"></param>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
public static Uri AddQueryParmeter(this Uri uri, string name, string value)
|
||||
public static Uri AddQueryParameter(this Uri uri, string name, string value)
|
||||
{
|
||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
||||
|
||||
@@ -481,6 +359,151 @@ namespace CryptoExchange.Net
|
||||
|
||||
return ub.Uri;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decompress using GzipStream
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
/// <returns></returns>
|
||||
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
||||
{
|
||||
using var decompressedStream = new MemoryStream();
|
||||
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
|
||||
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
|
||||
: new MemoryStream(data.ToArray());
|
||||
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
||||
deflateStream.CopyTo(decompressedStream);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the trading mode is linear
|
||||
/// </summary>
|
||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the trading mode is inverse
|
||||
/// </summary>
|
||||
public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the trading mode is perpetual
|
||||
/// </summary>
|
||||
public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the trading mode is delivery
|
||||
/// </summary>
|
||||
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
||||
|
||||
/// <summary>
|
||||
/// Register rest client interfaces
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterSharedRestInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
||||
{
|
||||
if (typeof(IAssetsRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IAssetsRestClient)client(x)!);
|
||||
if (typeof(IBalanceRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IBalanceRestClient)client(x)!);
|
||||
if (typeof(IDepositRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IDepositRestClient)client(x)!);
|
||||
if (typeof(IKlineRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IKlineRestClient)client(x)!);
|
||||
if (typeof(IListenKeyRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IListenKeyRestClient)client(x)!);
|
||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
||||
if (typeof(IRecentTradeRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IRecentTradeRestClient)client(x)!);
|
||||
if (typeof(ITradeHistoryRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ITradeHistoryRestClient)client(x)!);
|
||||
if (typeof(IWithdrawalRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IWithdrawalRestClient)client(x)!);
|
||||
if (typeof(IWithdrawRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IWithdrawRestClient)client(x)!);
|
||||
if (typeof(IFeeRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFeeRestClient)client(x)!);
|
||||
|
||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
||||
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
||||
|
||||
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFundingRateRestClient)client(x)!);
|
||||
if (typeof(IFuturesOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFuturesOrderRestClient)client(x)!);
|
||||
if (typeof(IFuturesSymbolRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFuturesSymbolRestClient)client(x)!);
|
||||
if (typeof(IFuturesTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFuturesTickerRestClient)client(x)!);
|
||||
if (typeof(IIndexPriceKlineRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IIndexPriceKlineRestClient)client(x)!);
|
||||
if (typeof(ILeverageRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ILeverageRestClient)client(x)!);
|
||||
if (typeof(IMarkPriceKlineRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IMarkPriceKlineRestClient)client(x)!);
|
||||
if (typeof(IOpenInterestRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IOpenInterestRestClient)client(x)!);
|
||||
if (typeof(IPositionHistoryRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IPositionHistoryRestClient)client(x)!);
|
||||
if (typeof(IPositionModeRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IPositionModeRestClient)client(x)!);
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Register socket client interfaces
|
||||
/// </summary>
|
||||
public static IServiceCollection RegisterSharedSocketInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
||||
{
|
||||
if (typeof(IBalanceSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IBalanceSocketClient)client(x)!);
|
||||
if (typeof(IBookTickerSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
||||
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
||||
if (typeof(IOrderBookRestClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IOrderBookRestClient)client(x)!);
|
||||
if (typeof(ITickerSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ITickerSocketClient)client(x)!);
|
||||
if (typeof(ITickersSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ITickersSocketClient)client(x)!);
|
||||
if (typeof(ITradeSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ITradeSocketClient)client(x)!);
|
||||
if (typeof(IUserTradeSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IUserTradeSocketClient)client(x)!);
|
||||
|
||||
if (typeof(ISpotOrderSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
|
||||
|
||||
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
|
||||
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
|
||||
services.AddTransient(x => (IPositionSocketClient)client(x)!);
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
@@ -8,131 +8,87 @@ using System.Threading.Tasks;
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Common rest client endpoints
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
public interface IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the exchange
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
string ExchangeName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Should be triggered on order placing
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
event Action<OrderId> OnOrderPlaced;
|
||||
/// <summary>
|
||||
/// Should be triggered on order cancelling
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
event Action<OrderId> OnOrderCanceled;
|
||||
|
||||
/// <summary>
|
||||
/// Get the symbol name based on a base and quote asset
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="baseAsset">The base asset</param>
|
||||
/// <param name="quoteAsset">The quote asset</param>
|
||||
/// <returns></returns>
|
||||
string GetSymbolName(string baseAsset, string quoteAsset);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of symbols for the exchange
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Symbol>>> GetSymbolsAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a ticker for the exchange
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get klines for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<Ticker>> GetTickerAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of tickers for the exchange
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Ticker>>> GetTickersAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of candles for a given symbol on the exchange
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to retrieve the candles for</param>
|
||||
/// <param name="timespan">The timespan to retrieve the candles for. The supported value are dependent on the exchange</param>
|
||||
/// <param name="startTime">[Optional] Start time to retrieve klines for</param>
|
||||
/// <param name="endTime">[Optional] End time to retrieve klines for</param>
|
||||
/// <param name="limit">[Optional] Max number of results</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Kline>>> GetKlinesAsync(string symbol, TimeSpan timespan, DateTime? startTime = null, DateTime? endTime = null, int? limit = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get the order book for a symbol
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get the book for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<CommonObjects.OrderBook>> GetOrderBookAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// The recent trades for a symbol
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol to get the trades for</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Trade>>> GetRecentTradesAsync(string symbol, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get balances
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="accountId">[Optional] The account id to retrieve balances for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Balance>>> GetBalancesAsync(string? accountId = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get an order by id
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<Order>> GetOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get trades for an order by id
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<UserTrade>>> GetOrderTradesAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of open orders
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get open orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Order>>> GetOpenOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get a list of closed orders
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="symbol">[Optional] The symbol to get closed orders for, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Order>>> GetClosedOrdersAsync(string? symbol = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Cancel an order by id
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="orderId">The id</param>
|
||||
/// <param name="symbol">[Optional] The symbol the order is on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<OrderId>> CancelOrderAsync(string orderId, string? symbol = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,23 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Common futures endpoints
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
public interface IFuturesClient : IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="leverage">[Optional] Leverage for this order. This is needed for some exchanges. For exchanges where this is not needed this parameter is ignored (and should be set before hand)</param>
|
||||
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, int? leverage = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Get position
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns></returns>
|
||||
Task<WebCallResult<IEnumerable<Position>>> GetPositionsAsync(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using CryptoExchange.Net.CommonObjects;
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -7,22 +6,13 @@ using System.Threading.Tasks;
|
||||
namespace CryptoExchange.Net.Interfaces.CommonClients
|
||||
{
|
||||
/// <summary>
|
||||
/// Common spot endpoints
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="" /> for more info.
|
||||
/// </summary>
|
||||
public interface ISpotClient: IBaseRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Place an order
|
||||
/// DEPRECATED; use <see cref="SharedApis.ISharedClient" /> instead for common/shared functionality. See <see href="https://jkorf.github.io/CryptoExchange.Net/docs/index.html#shared" /> for more info.
|
||||
/// </summary>
|
||||
/// <param name="symbol">The symbol the order is for</param>
|
||||
/// <param name="side">The side of the order</param>
|
||||
/// <param name="type">The type of the order</param>
|
||||
/// <param name="quantity">The quantity of the order</param>
|
||||
/// <param name="price">The price of the order, only for limit orders</param>
|
||||
/// <param name="accountId">[Optional] The account id to place the order on, required for some exchanges, ignored otherwise</param>
|
||||
/// <param name="clientOrderId">[Optional] Client specified id for this order</param>
|
||||
/// <param name="ct">[Optional] Cancellation token for cancelling the request</param>
|
||||
/// <returns>The id of the resulting order</returns>
|
||||
Task<WebCallResult<OrderId>> PlaceOrderAsync(string symbol, CommonOrderSide side, CommonOrderType type, decimal quantity, decimal? price = null, string? accountId = null, string? clientOrderId = null, CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -12,11 +16,33 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
string BaseAddress { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not API credentials have been configured for this client. Does not check the credentials are actually valid.
|
||||
/// </summary>
|
||||
bool Authenticated { 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>
|
||||
/// <param name="tradingMode">The trading mode</param>
|
||||
/// <param name="deliverDate">The deliver date for a delivery futures symbol</param>
|
||||
/// <returns></returns>
|
||||
string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||
|
||||
/// <summary>
|
||||
/// Set the API credentials for this API client
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="credentials"></param>
|
||||
void SetApiCredentials<T>(T credentials) where T : ApiCredentials;
|
||||
|
||||
/// <summary>
|
||||
/// Set new options. Note that when using a proxy this should be provided in the options even when already set before or it will be reset.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Api credentials type</typeparam>
|
||||
/// <param name="options">Options to set</param>
|
||||
void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using CryptoExchange.Net.Interfaces.CommonClients;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for accessing REST API's for different exchanges
|
||||
/// </summary>
|
||||
public interface ICryptoRestClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Get a list of all registered common ISpotClient types
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
IEnumerable<ISpotClient> GetSpotClients();
|
||||
|
||||
/// <summary>
|
||||
/// Get an ISpotClient implementation by exchange name
|
||||
/// </summary>
|
||||
/// <param name="exchangeName"></param>
|
||||
/// <returns></returns>
|
||||
ISpotClient? SpotClient(string exchangeName);
|
||||
|
||||
/// <summary>
|
||||
/// Try get
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
T TryGet<T>(Func<T> createFunc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Client for accessing Websocket API's for different exchanges
|
||||
/// </summary>
|
||||
public interface ICryptoSocketClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Try get a client by type for the service collection
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
T TryGet<T>(Func<T> createFunc);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using CryptoExchange.Net.Converters.MessageParsing;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Message accessor
|
||||
/// </summary>
|
||||
public interface IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Is this a json message
|
||||
/// </summary>
|
||||
bool IsJson { get; }
|
||||
/// <summary>
|
||||
/// Is the original data available for retrieval
|
||||
/// </summary>
|
||||
bool OriginalDataAvailable { get; }
|
||||
/// <summary>
|
||||
/// The underlying data object
|
||||
/// </summary>
|
||||
object? Underlying { get; }
|
||||
/// <summary>
|
||||
/// Clear internal data structure
|
||||
/// </summary>
|
||||
void Clear();
|
||||
/// <summary>
|
||||
/// Get the type of node
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
NodeType? GetNodeType();
|
||||
/// <summary>
|
||||
/// Get the type of node
|
||||
/// </summary>
|
||||
/// <param name="path">Access path</param>
|
||||
/// <returns></returns>
|
||||
NodeType? GetNodeType(MessagePath path);
|
||||
/// <summary>
|
||||
/// Get the value of a path
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
T? GetValue<T>(MessagePath path);
|
||||
/// <summary>
|
||||
/// Get the values of an array
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
List<T?>? GetValues<T>(MessagePath path);
|
||||
/// <summary>
|
||||
/// Deserialize the message into this type
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
CallResult<object> Deserialize(Type type, MessagePath? path = null);
|
||||
/// <summary>
|
||||
/// Deserialize the message into this type
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <returns></returns>
|
||||
CallResult<T> Deserialize<T>(MessagePath? path = null);
|
||||
|
||||
/// <summary>
|
||||
/// Get the original string value
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
string GetOriginalString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stream message accessor
|
||||
/// </summary>
|
||||
public interface IStreamMessageAccessor : IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Load a stream message
|
||||
/// </summary>
|
||||
/// <param name="stream"></param>
|
||||
/// <param name="bufferStream"></param>
|
||||
Task<CallResult> Read(Stream stream, bool bufferStream);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Byte message accessor
|
||||
/// </summary>
|
||||
public interface IByteMessageAccessor : IMessageAccessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Load a data message
|
||||
/// </summary>
|
||||
/// <param name="data"></param>
|
||||
CallResult Read(ReadOnlyMemory<byte> data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Message processor
|
||||
/// </summary>
|
||||
public interface IMessageProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Id of the processor
|
||||
/// </summary>
|
||||
public int Id { get; }
|
||||
/// <summary>
|
||||
/// The identifiers for this processor
|
||||
/// </summary>
|
||||
public HashSet<string> ListenerIdentifiers { get; }
|
||||
/// <summary>
|
||||
/// Handle a message
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> Handle(SocketConnection connection, DataEvent<object> message);
|
||||
/// <summary>
|
||||
/// Get the type the message should be deserialized to
|
||||
/// </summary>
|
||||
/// <param name="messageAccessor"></param>
|
||||
/// <returns></returns>
|
||||
Type? GetMessageType(IMessageAccessor messageAccessor);
|
||||
/// <summary>
|
||||
/// Deserialize a message into object of type
|
||||
/// </summary>
|
||||
/// <param name="accessor"></param>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
CallResult<object> Deserialize(IMessageAccessor accessor, Type type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Serializer interface
|
||||
/// </summary>
|
||||
public interface IMessageSerializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize an object to a string
|
||||
/// </summary>
|
||||
/// <param name="message"></param>
|
||||
/// <returns></returns>
|
||||
string Serialize(object message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.SharedApis;
|
||||
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);
|
||||
/// <summary>
|
||||
/// Create a new order book by base and quote asset names
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol</param>
|
||||
/// <param name="options">Options for the order book</param>
|
||||
/// <returns></returns>
|
||||
public ISymbolOrderBook Create(SharedSymbol symbol, Action<TOptions>? options = null);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http;
|
||||
using System.Security;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -24,6 +23,6 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="requestWeight">The weight of the request</param>
|
||||
/// <param name="ct">Cancellation token to cancel waiting</param>
|
||||
/// <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,5 +1,4 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
|
||||
@@ -25,6 +24,13 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="requestTimeout">Request timeout to use</param>
|
||||
/// <param name="httpClient">Optional shared http client instance</param>
|
||||
/// <param name="proxy">Optional proxy to use when no http client is provided</param>
|
||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient=null);
|
||||
void Configure(ApiProxy? proxy, TimeSpan requestTimeout, HttpClient? httpClient = null);
|
||||
|
||||
/// <summary>
|
||||
/// Update settings
|
||||
/// </summary>
|
||||
/// <param name="proxy">Proxy to use</param>
|
||||
/// <param name="requestTimeout">Request timeout to use</param>
|
||||
void UpdateSettings(ApiProxy? proxy, TimeSpan requestTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// Base rest API client
|
||||
@@ -17,17 +14,5 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Total amount of requests made with this API client
|
||||
/// </summary>
|
||||
int TotalRequestsMade { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get time offset for an API client. Return null if time syncing shouldnt/cant be done
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
TimeSpan? GetTimeOffset();
|
||||
|
||||
/// <summary>
|
||||
/// Get time sync info for an API client. Return null if time syncing shouldnt/cant be done
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
TimeSyncInfo? GetTimeSyncInfo();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
@@ -19,5 +17,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// The total amount of requests made with this client
|
||||
/// </summary>
|
||||
int TotalRequestsMade { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
@@ -20,44 +19,31 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
int CurrentSubscriptions { get; }
|
||||
/// <summary>
|
||||
/// Incoming data kpbs
|
||||
/// Incoming data Kbps
|
||||
/// </summary>
|
||||
double IncomingKbps { get; }
|
||||
/// <summary>
|
||||
/// The factory for creating sockets. Used for unit testing
|
||||
/// </summary>
|
||||
IWebsocketFactory SocketFactory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the url to reconnect to after losing a connection
|
||||
/// Current client options
|
||||
/// </summary>
|
||||
/// <param name="connection"></param>
|
||||
/// <returns></returns>
|
||||
Task<Uri?> GetReconnectUriAsync(SocketConnection connection);
|
||||
|
||||
SocketExchangeOptions ClientOptions { get; }
|
||||
/// <summary>
|
||||
/// Current API options
|
||||
/// </summary>
|
||||
SocketApiOptions ApiOptions { get; }
|
||||
/// <summary>
|
||||
/// Log the current state of connections and subscriptions
|
||||
/// </summary>
|
||||
string GetSubscriptionsState();
|
||||
string GetSubscriptionsState(bool includeSubDetails = true);
|
||||
/// <summary>
|
||||
/// Reconnect all connections
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task ReconnectAsync();
|
||||
/// <summary>
|
||||
/// Update the original request to send when the connection is restored after disconnecting. Can be used to update an authentication token for example.
|
||||
/// </summary>
|
||||
/// <param name="request">The original request</param>
|
||||
/// <returns></returns>
|
||||
Task<CallResult<object>> RevitalizeRequestAsync(object request);
|
||||
/// <summary>
|
||||
/// Periodically sends data over a socket connection
|
||||
/// </summary>
|
||||
/// <param name="identifier">Identifier for the periodic send</param>
|
||||
/// <param name="interval">How often</param>
|
||||
/// <param name="objGetter">Method returning the object to send</param>
|
||||
void SendPeriodic(string identifier, TimeSpan interval, Func<SocketConnection, object> objGetter);
|
||||
/// <summary>
|
||||
/// Unsubscribe all subscriptions
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
@@ -74,5 +60,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||
/// <returns></returns>
|
||||
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||
|
||||
/// <summary>
|
||||
/// Prepare connections which can subsequently be used for sending websocket requests. Note that this is not required. If not prepared it will be initialized at the first websocket request.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<CallResult> PrepareConnectionsAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CryptoExchange.Net.Authentication;
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Objects.Options;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
{
|
||||
@@ -12,6 +10,11 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
public interface ISocketClient: IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The exchange name
|
||||
/// </summary>
|
||||
string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The options provided for this client
|
||||
/// </summary>
|
||||
|
||||
@@ -12,9 +12,14 @@ namespace CryptoExchange.Net.Interfaces
|
||||
public interface ISymbolOrderBook
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier
|
||||
/// The exchange the book is for
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
string Exchange { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The Api the book is for
|
||||
/// </summary>
|
||||
string Api { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The status of the order book. Order book is up to date when the status is `Synced`
|
||||
@@ -100,7 +105,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
Task StopAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Get the average price that a market order would fill at at the current order book state. This is no guarentee that an order of that quantity would actually be filled
|
||||
/// Get the average price that a market order would fill at at the current order book state. This is no guarantee that an order of that quantity would actually be filled
|
||||
/// at that price since between this calculation and the order placement the book might have changed.
|
||||
/// </summary>
|
||||
/// <param name="quantity">The quantity in base asset to fill</param>
|
||||
@@ -110,7 +115,7 @@ namespace CryptoExchange.Net.Interfaces
|
||||
|
||||
/// <summary>
|
||||
/// Get the amount of base asset which can be traded with the quote quantity when placing a market order at at the current order book state.
|
||||
/// This is no guarentee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
|
||||
/// This is no guarantee that an order of that quantity would actually be fill the quantity returned by this since between this calculation and the order placement the book might have changed.
|
||||
/// </summary>
|
||||
/// <param name="quoteQuantity">The quantity in quote asset looking to trade</param>
|
||||
/// <param name="type">The type</param>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using CryptoExchange.Net.Objects;
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using System;
|
||||
using System.Security.Authentication;
|
||||
using System.Text;
|
||||
using System.Net.WebSockets;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
@@ -15,33 +13,41 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// <summary>
|
||||
/// Websocket closed event
|
||||
/// </summary>
|
||||
event Action OnClose;
|
||||
event Func<Task> OnClose;
|
||||
/// <summary>
|
||||
/// Websocket message received event
|
||||
/// </summary>
|
||||
event Action<string> OnMessage;
|
||||
event Func<WebSocketMessageType, ReadOnlyMemory<byte>, Task> OnStreamMessage;
|
||||
/// <summary>
|
||||
/// Websocket sent event, RequestId as parameter
|
||||
/// </summary>
|
||||
event Action<int> OnRequestSent;
|
||||
event Func<int, Task> OnRequestSent;
|
||||
/// <summary>
|
||||
/// Websocket query was ratelimited and couldn't be send
|
||||
/// </summary>
|
||||
event Func<int, Task>? OnRequestRateLimited;
|
||||
/// <summary>
|
||||
/// Connection was ratelimited and couldn't be established
|
||||
/// </summary>
|
||||
event Func<Task>? OnConnectRateLimited;
|
||||
/// <summary>
|
||||
/// Websocket error event
|
||||
/// </summary>
|
||||
event Action<Exception> OnError;
|
||||
event Func<Exception, Task> OnError;
|
||||
/// <summary>
|
||||
/// Websocket opened event
|
||||
/// </summary>
|
||||
event Action OnOpen;
|
||||
event Func<Task> OnOpen;
|
||||
/// <summary>
|
||||
/// Websocket has lost connection to the server and is attempting to reconnect
|
||||
/// </summary>
|
||||
event Action OnReconnecting;
|
||||
event Func<Task> OnReconnecting;
|
||||
/// <summary>
|
||||
/// Websocket has reconnected to the server
|
||||
/// </summary>
|
||||
event Action OnReconnected;
|
||||
event Func<Task> OnReconnected;
|
||||
/// <summary>
|
||||
/// Get reconntion url
|
||||
/// Get reconnection url
|
||||
/// </summary>
|
||||
Func<Task<Uri?>>? GetReconnectionUrl { get; set; }
|
||||
|
||||
@@ -69,14 +75,14 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// Connect the socket
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task<bool> ConnectAsync();
|
||||
Task<CallResult> ConnectAsync();
|
||||
/// <summary>
|
||||
/// Send data
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="data"></param>
|
||||
/// <param name="weight"></param>
|
||||
void Send(int id, string data, int weight);
|
||||
bool Send(int id, string data, int weight);
|
||||
/// <summary>
|
||||
/// Reconnect the socket
|
||||
/// </summary>
|
||||
@@ -87,5 +93,10 @@ namespace CryptoExchange.Net.Interfaces
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
Task CloseAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Update proxy setting
|
||||
/// </summary>
|
||||
void UpdateProxy(ApiProxy? proxy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using CryptoExchange.Net.Sockets;
|
||||
using CryptoExchange.Net.Objects.Sockets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace CryptoExchange.Net.Interfaces
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace CryptoExchange.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Helpers for client libraries
|
||||
/// </summary>
|
||||
public static class LibraryHelpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Client order id separator
|
||||
/// </summary>
|
||||
public const string ClientOrderIdSeparator = "JK";
|
||||
|
||||
/// <summary>
|
||||
/// Apply broker id to a client order id
|
||||
/// </summary>
|
||||
/// <param name="clientOrderId"></param>
|
||||
/// <param name="brokerId"></param>
|
||||
/// <param name="maxLength"></param>
|
||||
/// <param name="allowValueAdjustment"></param>
|
||||
/// <returns></returns>
|
||||
public static string ApplyBrokerId(string? clientOrderId, string brokerId, int maxLength, bool allowValueAdjustment)
|
||||
{
|
||||
var reservedLength = brokerId.Length + ClientOrderIdSeparator.Length;
|
||||
|
||||
if ((clientOrderId?.Length + reservedLength) > maxLength)
|
||||
return clientOrderId!;
|
||||
|
||||
if (!string.IsNullOrEmpty(clientOrderId))
|
||||
{
|
||||
if (allowValueAdjustment)
|
||||
clientOrderId = brokerId + ClientOrderIdSeparator + clientOrderId;
|
||||
|
||||
return clientOrderId!;
|
||||
}
|
||||
else
|
||||
{
|
||||
clientOrderId = ExchangeHelpers.AppendRandomString(brokerId + ClientOrderIdSeparator, maxLength);
|
||||
}
|
||||
|
||||
return clientOrderId;
|
||||
}
|
||||
}
|
||||
}
|
||||
+374
@@ -0,0 +1,374 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
#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, string, Exception?> _connectionFailed;
|
||||
private static readonly Action<ILogger, int, Uri, Exception?> _connected;
|
||||
private static readonly Action<ILogger, int, Exception?> _startingProcessing;
|
||||
private static readonly Action<ILogger, int, Exception?> _finishedProcessing;
|
||||
private static readonly Action<ILogger, int, Exception?> _attemptReconnect;
|
||||
private static readonly Action<ILogger, int, Uri, Exception?> _setReconnectUri;
|
||||
private static readonly Action<ILogger, int, int, int, Exception?> _addingBytesToSendBuffer;
|
||||
private static readonly Action<ILogger, int, Exception?> _reconnectRequested;
|
||||
private static readonly Action<ILogger, int, Exception?> _closeAsyncWaitingForExistingCloseTask;
|
||||
private static readonly Action<ILogger, int, Exception?> _closeAsyncSocketNotOpen;
|
||||
private static readonly Action<ILogger, int, Exception?> _closing;
|
||||
private static readonly Action<ILogger, int, Exception?> _closed;
|
||||
private static readonly Action<ILogger, int, Exception?> _disposing;
|
||||
private static readonly Action<ILogger, int, Exception?> _disposed;
|
||||
private static readonly Action<ILogger, int, int, int, Exception?> _sentBytes;
|
||||
private static readonly Action<ILogger, int, string, Exception?> _sendLoopStoppedWithException;
|
||||
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?> _receivedCloseConfirmation;
|
||||
private static readonly Action<ILogger, int, int, Exception?> _receivedPartialMessage;
|
||||
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?> _discardIncompleteMessage;
|
||||
private static readonly Action<ILogger, int, Exception?> _receiveLoopStoppedWithException;
|
||||
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?> _noDataReceiveTimeoutReconnect;
|
||||
private static readonly Action<ILogger, int, string, string, Exception?> _socketProcessingStateChanged;
|
||||
private static readonly Action<ILogger, int, Exception?> _socketPingTimeout;
|
||||
|
||||
static CryptoExchangeWebSocketClientLoggingExtension()
|
||||
{
|
||||
_connecting = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1000, "Connecting"),
|
||||
"[Sckt {SocketId}] connecting");
|
||||
|
||||
_connectionFailed = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Error,
|
||||
new EventId(1001, "ConnectionFailed"),
|
||||
"[Sckt {SocketId}] connection failed: {ErrorMessage}");
|
||||
|
||||
_connected = LoggerMessage.Define<int, Uri?>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1002, "Connected"),
|
||||
"[Sckt {SocketId}] connected to {Uri}");
|
||||
|
||||
_startingProcessing = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1003, "StartingProcessing"),
|
||||
"[Sckt {SocketId}] starting processing tasks");
|
||||
|
||||
_finishedProcessing = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1004, "FinishedProcessing"),
|
||||
"[Sckt {SocketId}] processing tasks finished");
|
||||
|
||||
_attemptReconnect = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1005, "AttemptReconnect"),
|
||||
"[Sckt {SocketId}] attempting to reconnect");
|
||||
|
||||
_setReconnectUri = LoggerMessage.Define<int, Uri>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1006, "SetReconnectUri"),
|
||||
"[Sckt {SocketId}] reconnect URI set to {ReconnectUri}");
|
||||
|
||||
_addingBytesToSendBuffer = LoggerMessage.Define<int, int, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1007, "AddingBytesToSendBuffer"),
|
||||
"[Sckt {SocketId}] [Req {RequestId}] adding {NumBytes} bytes to send buffer");
|
||||
|
||||
_reconnectRequested = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1008, "ReconnectRequested"),
|
||||
"[Sckt {SocketId}] reconnect requested");
|
||||
|
||||
_closeAsyncWaitingForExistingCloseTask = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1009, "CloseAsyncWaitForExistingCloseTask"),
|
||||
"[Sckt {SocketId}] CloseAsync() waiting for existing close task");
|
||||
|
||||
_closeAsyncSocketNotOpen = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1010, "CloseAsyncSocketNotOpen"),
|
||||
"[Sckt {SocketId}] CloseAsync() socket not open");
|
||||
|
||||
_closing = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1011, "Closing"),
|
||||
"[Sckt {SocketId}] closing");
|
||||
|
||||
_closed = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1012, "Closed"),
|
||||
"[Sckt {SocketId}] closed");
|
||||
|
||||
_disposing = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1013, "Disposing"),
|
||||
"[Sckt {SocketId}] disposing");
|
||||
|
||||
_disposed = LoggerMessage.Define<int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1014, "Disposed"),
|
||||
"[Sckt {SocketId}] disposed");
|
||||
|
||||
_sentBytes = LoggerMessage.Define<int, int, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1016, "SentBytes"),
|
||||
"[Sckt {SocketId}] [Req {RequestId}] sent {NumBytes} bytes");
|
||||
|
||||
_sendLoopStoppedWithException = LoggerMessage.Define<int, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(1017, "SendLoopStoppedWithException"),
|
||||
"[Sckt {SocketId}] send loop stopped with exception: {ErrorMessage}");
|
||||
|
||||
_sendLoopFinished = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1018, "SendLoopFinished"),
|
||||
"[Sckt {SocketId}] send loop finished");
|
||||
|
||||
_receivedCloseMessage = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1019, "ReceivedCloseMessage"),
|
||||
"[Sckt {SocketId}] received `Close` message, CloseStatus: {CloseStatus}, CloseStatusDescription: {CloseStatusDescription}");
|
||||
|
||||
_receivedPartialMessage = LoggerMessage.Define<int, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1020, "ReceivedPartialMessage"),
|
||||
"[Sckt {SocketId}] received {NumBytes} bytes in partial message");
|
||||
|
||||
_receivedSingleMessage = LoggerMessage.Define<int, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1021, "ReceivedSingleMessage"),
|
||||
"[Sckt {SocketId}] received {NumBytes} bytes in single message");
|
||||
|
||||
_reassembledMessage = LoggerMessage.Define<int, long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1022, "ReassembledMessage"),
|
||||
"[Sckt {SocketId}] reassembled message of {NumBytes} bytes");
|
||||
|
||||
_discardIncompleteMessage = LoggerMessage.Define<int, long>(
|
||||
LogLevel.Trace,
|
||||
new EventId(1023, "DiscardIncompleteMessage"),
|
||||
"[Sckt {SocketId}] discarding incomplete message of {NumBytes} bytes");
|
||||
|
||||
_receiveLoopStoppedWithException = LoggerMessage.Define<int>(
|
||||
LogLevel.Error,
|
||||
new EventId(1024, "ReceiveLoopStoppedWithException"),
|
||||
"[Sckt {SocketId}] receive loop stopped with exception");
|
||||
|
||||
_receiveLoopFinished = LoggerMessage.Define<int>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1025, "ReceiveLoopFinished"),
|
||||
"[Sckt {SocketId}] receive loop finished");
|
||||
|
||||
_startingTaskForNoDataReceivedCheck = LoggerMessage.Define<int, TimeSpan?>(
|
||||
LogLevel.Debug,
|
||||
new EventId(1026, "StartingTaskForNoDataReceivedCheck"),
|
||||
"[Sckt {SocketId}] starting task checking for no data received for {Timeout}");
|
||||
|
||||
_noDataReceiveTimeoutReconnect = LoggerMessage.Define<int, TimeSpan?>(
|
||||
LogLevel.Warning,
|
||||
new EventId(1027, "NoDataReceiveTimeoutReconnect"),
|
||||
"[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(1029, "SocketProcessingStateChanged"),
|
||||
"[Sckt {Id}] processing state change: {PreviousState} -> {NewState}");
|
||||
|
||||
_socketPingTimeout = LoggerMessage.Define<int>(
|
||||
LogLevel.Warning,
|
||||
new EventId(1030, "SocketPingTimeout"),
|
||||
"[Sckt {Id}] ping frame timeout; reconnecting socket");
|
||||
|
||||
}
|
||||
|
||||
public static void SocketConnecting(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_connecting(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketConnectionFailed(
|
||||
this ILogger logger, int socketId, string message, Exception e)
|
||||
{
|
||||
_connectionFailed(logger, socketId, message, e);
|
||||
}
|
||||
|
||||
public static void SocketConnected(
|
||||
this ILogger logger, int socketId, Uri uri)
|
||||
{
|
||||
_connected(logger, socketId, uri, null);
|
||||
}
|
||||
|
||||
public static void SocketStartingProcessing(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_startingProcessing(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketFinishedProcessing(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_finishedProcessing(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketAttemptReconnect(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_attemptReconnect(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketSetReconnectUri(
|
||||
this ILogger logger, int socketId, Uri uri)
|
||||
{
|
||||
_setReconnectUri(logger, socketId, uri, null);
|
||||
}
|
||||
|
||||
public static void SocketAddingBytesToSendBuffer(
|
||||
this ILogger logger, int socketId, int requestId, byte[] bytes)
|
||||
{
|
||||
_addingBytesToSendBuffer(logger, socketId, requestId, bytes.Length, null);
|
||||
}
|
||||
|
||||
public static void SocketReconnectRequested(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_reconnectRequested(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketCloseAsyncWaitingForExistingCloseTask(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_closeAsyncWaitingForExistingCloseTask(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketCloseAsyncSocketNotOpen(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_closeAsyncSocketNotOpen(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketClosing(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_closing(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketClosed(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_closed(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketDisposing(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_disposing(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketDisposed(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_disposed(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketSentBytes(
|
||||
this ILogger logger, int socketId, int requestId, int numBytes)
|
||||
{
|
||||
_sentBytes(logger, socketId, requestId, numBytes, null);
|
||||
}
|
||||
|
||||
public static void SocketSendLoopStoppedWithException(
|
||||
this ILogger logger, int socketId, string message, Exception e)
|
||||
{
|
||||
_sendLoopStoppedWithException(logger, socketId, message, e);
|
||||
}
|
||||
|
||||
public static void SocketSendLoopFinished(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_sendLoopFinished(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketReceivedCloseMessage(
|
||||
this ILogger logger, int socketId, string webSocketCloseStatus, string closeStatusDescription)
|
||||
{
|
||||
_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(
|
||||
this ILogger logger, int socketId, int countBytes)
|
||||
{
|
||||
_receivedPartialMessage(logger, socketId, countBytes, null);
|
||||
}
|
||||
|
||||
public static void SocketReceivedSingleMessage(
|
||||
this ILogger logger, int socketId, int countBytes)
|
||||
{
|
||||
_receivedSingleMessage(logger, socketId, countBytes, null);
|
||||
}
|
||||
|
||||
public static void SocketReassembledMessage(
|
||||
this ILogger logger, int socketId, long countBytes)
|
||||
{
|
||||
_reassembledMessage(logger, socketId, countBytes, null);
|
||||
}
|
||||
|
||||
public static void SocketDiscardIncompleteMessage(
|
||||
this ILogger logger, int socketId, long countBytes)
|
||||
{
|
||||
_discardIncompleteMessage(logger, socketId, countBytes, null);
|
||||
}
|
||||
|
||||
public static void SocketReceiveLoopStoppedWithException(
|
||||
this ILogger logger, int socketId, Exception e)
|
||||
{
|
||||
_receiveLoopStoppedWithException(logger, socketId, e);
|
||||
}
|
||||
|
||||
public static void SocketReceiveLoopFinished(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_receiveLoopFinished(logger, socketId, null);
|
||||
}
|
||||
|
||||
public static void SocketStartingTaskForNoDataReceivedCheck(
|
||||
this ILogger logger, int socketId, TimeSpan? timeSpan)
|
||||
{
|
||||
_startingTaskForNoDataReceivedCheck(logger, socketId, timeSpan, null);
|
||||
}
|
||||
|
||||
public static void SocketNoDataReceiveTimoutReconnect(
|
||||
this ILogger logger, int socketId, TimeSpan? timeSpan)
|
||||
{
|
||||
_noDataReceiveTimeoutReconnect(logger, socketId, timeSpan, null);
|
||||
}
|
||||
|
||||
public static void SocketProcessingStateChanged(
|
||||
this ILogger logger, int socketId, string prevState, string newState)
|
||||
{
|
||||
_socketProcessingStateChanged(logger, socketId, prevState, newState, null);
|
||||
}
|
||||
|
||||
public static void SocketPingTimeout(
|
||||
this ILogger logger, int socketId)
|
||||
{
|
||||
_socketPingTimeout(logger, socketId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
|
||||
namespace CryptoExchange.Net.Logging.Extensions
|
||||
{
|
||||
#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, Exception?> _rateLimitConnectionFailed;
|
||||
private static readonly Action<ILogger, int, string, TimeSpan, string, string, Exception?> _rateLimitDelayingRequest;
|
||||
private static readonly Action<ILogger, int, TimeSpan, string, string, Exception?> _rateLimitDelayingConnection;
|
||||
private static readonly Action<ILogger, int, string, string, string, int, Exception?> _rateLimitAppliedRequest;
|
||||
private static readonly Action<ILogger, int, string, string, int, Exception?> _rateLimitAppliedConnection;
|
||||
|
||||
static RateLimitGateLoggingExtensions()
|
||||
{
|
||||
_rateLimitRequestFailed = LoggerMessage.Define<int, string, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6000, "RateLimitRequestFailed"),
|
||||
"[Req {Id}] Call to {Path} failed because of ratelimit guard {Guard}; {Limit}");
|
||||
|
||||
_rateLimitConnectionFailed = LoggerMessage.Define<int, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6001, "RateLimitConnectionFailed"),
|
||||
"[Sckt {Id}] Connection failed because of ratelimit guard {Guard}; {Limit}");
|
||||
|
||||
_rateLimitDelayingRequest = LoggerMessage.Define<int, string, TimeSpan, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6002, "RateLimitDelayingRequest"),
|
||||
"[Req {Id}] Delaying call to {Path} by {Delay} because of ratelimit guard {Guard}; {Limit}");
|
||||
|
||||
_rateLimitDelayingConnection = LoggerMessage.Define<int, TimeSpan, string, string>(
|
||||
LogLevel.Warning,
|
||||
new EventId(6003, "RateLimitDelayingConnection"),
|
||||
"[Sckt {Id}] Delaying connection by {Delay} because of ratelimit guard {Guard}; {Limit}");
|
||||
|
||||
_rateLimitAppliedConnection = LoggerMessage.Define<int, string, string, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6004, "RateLimitDelayingConnection"),
|
||||
"[Sckt {Id}] Connection passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
|
||||
|
||||
_rateLimitAppliedRequest = LoggerMessage.Define<int, string, string, string, int>(
|
||||
LogLevel.Trace,
|
||||
new EventId(6005, "RateLimitAppliedRequest"),
|
||||
"[Req {Id}] Call to {Path} passed ratelimit guard {Guard}; {Limit}, New count: {Current}");
|
||||
}
|
||||
|
||||
public static void RateLimitRequestFailed(this ILogger logger, int requestId, string path, string guard, string limit)
|
||||
{
|
||||
_rateLimitRequestFailed(logger, requestId, path, guard, limit, null);
|
||||
}
|
||||
|
||||
public static void RateLimitConnectionFailed(this ILogger logger, int connectionId, string guard, string limit)
|
||||
{
|
||||
_rateLimitConnectionFailed(logger, connectionId, guard, limit, null);
|
||||
}
|
||||
|
||||
public static void RateLimitDelayingRequest(this ILogger logger, int requestId, string path, TimeSpan delay, string guard, string limit)
|
||||
{
|
||||
_rateLimitDelayingRequest(logger, requestId, path, delay, guard, limit, null);
|
||||
}
|
||||
|
||||
public static void RateLimitDelayingConnection(this ILogger logger, int connectionId, TimeSpan delay, string guard, string limit)
|
||||
{
|
||||
_rateLimitDelayingConnection(logger, connectionId, delay, guard, limit, null);
|
||||
}
|
||||
|
||||
public static void RateLimitAppliedConnection(this ILogger logger, int connectionId, string guard, string limit, int current)
|
||||
{
|
||||
_rateLimitAppliedConnection(logger, connectionId, guard, limit, current, null);
|
||||
}
|
||||
|
||||
public static void RateLimitAppliedRequest(this ILogger logger, int requestIdId, string path, string guard, string limit, int current)
|
||||
{
|
||||
_rateLimitAppliedRequest(logger, requestIdId, path, guard, limit, current, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user