mirror of
https://github.com/JKorf/CryptoExchange.Net.git
synced 2026-08-12 17:03:10 +00:00
Compare commits
159 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 204bda8622 | |||
| 78e3523a4f | |||
| 89a73747b0 | |||
| 02b70398b3 | |||
| 73fcb47b17 | |||
| d41ca3459e | |||
| bea2b2bd7b | |||
| b29cdc41f3 | |||
| 0ce2e778f4 | |||
| 36c2411d46 | |||
| 6d3e72745a | |||
| 51c74baa26 | |||
| 419e01d009 | |||
| 297eee0e1f | |||
| 6a9231b1e3 | |||
| e40f2a15b6 | |||
| b94085a27a | |||
| 5e083811df | |||
| 7dcf5cd6ea | |||
| 1471a4733f | |||
| f39d9f7cfb | |||
| 9fab8faa45 | |||
| 226f175343 | |||
| 813bd9f5a1 | |||
| c8d2b4f09d | |||
| 6560b82a3e | |||
| e151af8f37 | |||
| bdf7a07c6f | |||
| a8ffe90bf2 | |||
| 3372b9eb44 | |||
| df25221960 | |||
| 7c67a014f5 | |||
| 65291b3195 | |||
| ece6a9d27d | |||
| 1ed29b0474 | |||
| be2eb01353 | |||
| 39cd66596d | |||
| 63f51811a9 | |||
| abda065237 | |||
| 759c8b9a58 | |||
| 8e18482781 | |||
| 87dd2d9d40 | |||
| f2b0cb2f0d | |||
| de0a954a91 | |||
| 4a79ce22ec | |||
| 40d480e1fc | |||
| 74e5cf6fc9 | |||
| 2fd3912795 | |||
| ec44307a0c | |||
| ba55705385 | |||
| 2c63a83117 | |||
| 71b1e5e906 | |||
| eaeba6f27e | |||
| 913bdaa855 | |||
| 5aa5790d0a | |||
| a8321e083e | |||
| ce3fa5f186 | |||
| fff70a9c65 | |||
| cff33bb5ac | |||
| 21c8133292 | |||
| 76772e91ba | |||
| 218e0260ce | |||
| 96b3904266 | |||
| bc8faf9822 | |||
| 90c1b89ceb | |||
| 21206ffb25 | |||
| 5942423bfb | |||
| dc4abc42a7 | |||
| c71a81e686 | |||
| 550c0eabf1 | |||
| 28a2a0c7fd | |||
| 7dd1cd5bbd | |||
| a7ff4416bd | |||
| 669d1f7c9e | |||
| 34ee2d3690 | |||
| 005fb7875d | |||
| fa9300ce97 | |||
| fc2d3fc2d2 | |||
| 187ca6a4ef | |||
| 3b2a85d210 | |||
| c512bee825 | |||
| 0943b052b9 | |||
| a896fffdb3 | |||
| 177daf903b | |||
| aa1ebdc4ed | |||
| 38058c4a70 | |||
| a7eb483479 | |||
| c76931a3b4 | |||
| b90b7e9e0c | |||
| beda53d36d | |||
| 0668f669c1 | |||
| 64250e13db | |||
| 451d38d5e7 | |||
| e11e437bbb | |||
| b8a1ad798d | |||
| 4a851c44f2 | |||
| d079796020 | |||
| f125bc88b0 | |||
| f3d535f286 | |||
| 6e8c6feec2 | |||
| 84b0444caf | |||
| 4be986ebe7 | |||
| 21872f818a | |||
| 4e7c45d758 | |||
| 0e55e5f065 | |||
| b1c5cf318a | |||
| 9d94a24862 | |||
| 6cf9684f55 | |||
| 8043a48c49 | |||
| 7d657dd533 | |||
| 1bfdec1484 | |||
| 8d5b6a53f3 | |||
| ad97102e7c | |||
| d8dc121386 | |||
| 10c3868c00 | |||
| 411ba00a82 | |||
| c1b2b62dbc | |||
| 3960cab7a7 | |||
| 34e9447e55 | |||
| 9d3295acc7 | |||
| 995cd3d84c | |||
| 919cdf0075 | |||
| d181c9cfc1 | |||
| 5943142c44 | |||
| dbc430e838 | |||
| 7413d03d31 | |||
| dd60067684 | |||
| 04e4ddf525 | |||
| 99bf6d7c75 | |||
| 99a203933c | |||
| b43d2a2040 | |||
| ba9c406def | |||
| f5f4d50cc9 | |||
| f87506b490 | |||
| f6f9a53ce5 | |||
| 61130ef54e | |||
| e8bcbd59be | |||
| d433ff7475 | |||
| 71957037d0 | |||
| bcdcdbbd4e | |||
| 1ece13f5bc | |||
| da70ba6ec7 | |||
| a832f0e4d4 | |||
| 6ab4d005a0 | |||
| 18dc935038 | |||
| bb3a534f75 | |||
| 649ba370c6 | |||
| 51732c5ce6 | |||
| 0ba7b46680 | |||
| 94dfbb7b9e | |||
| b8b7512b35 | |||
| aba6b773ce | |||
| d88fb0d356 | |||
| b8c6d55156 | |||
| d9a5481db2 | |||
| 6a8bb42c0e | |||
| 2445f001ab | |||
| c84fa9ac32 | |||
| d44a11c44e |
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v1
|
uses: actions/setup-dotnet@v1
|
||||||
with:
|
with:
|
||||||
dotnet-version: 9.0.x
|
dotnet-version: 10.0.x
|
||||||
- name: Restore dependencies
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
<PackageId>CryptoExchange.Net.Protobuf</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>Protobuf support for CryptoExchange.Net</Description>
|
<Description>Protobuf support for CryptoExchange.Net</Description>
|
||||||
<PackageVersion>9.6.0</PackageVersion>
|
<PackageVersion>10.0.1</PackageVersion>
|
||||||
<AssemblyVersion>9.6.0</AssemblyVersion>
|
<AssemblyVersion>10.0.1</AssemblyVersion>
|
||||||
<FileVersion>9.6.0</FileVersion>
|
<FileVersion>10.0.1</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>CryptoExchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
<DocumentationFile>CryptoExchange.Net.Protobuf.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CryptoExchange.Net" Version="9.6.0" />
|
<PackageReference Include="CryptoExchange.Net" Version="10.0.2" />
|
||||||
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
<PackageReference Include="protobuf-net" Version="3.2.56" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -5,6 +5,36 @@
|
|||||||
Protobuf support for CryptoExchange.Net.
|
Protobuf support for CryptoExchange.Net.
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
* Version 10.0.1 - 16 Dec 2025
|
||||||
|
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 10.0.0 - 16 Dec 2025
|
||||||
|
* Updated CryptoExchange.Net version to 10.0.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.13.0 - 10 Nov 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.13.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.12.0 - 03 Nov 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.12.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.11.1 - 30 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.11.0 - 30 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.11.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.10.0 - 15 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.10.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.9.0 - 06 Oct 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.9.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.8.0 - 30 Sep 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.8.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
|
* Version 9.7.0 - 01 Sep 2025
|
||||||
|
* Updated CryptoExchange.Net version to 9.7.0, see https://github.com/JKorf/CryptoExchange.Net/releases/
|
||||||
|
|
||||||
* Version 9.6.0 - 25 Aug 2025
|
* Version 9.6.0 - 25 Aug 2025
|
||||||
* Updated CryptoExchange.Net version to 9.6.0
|
* Updated CryptoExchange.Net version to 9.6.0
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ using NUnit.Framework.Legacy;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
@@ -140,5 +140,17 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
ClassicAssert.False(result1);
|
ClassicAssert.False(result1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task CancellingWait_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
var evnt = new AsyncResetEvent(false, true);
|
||||||
|
|
||||||
|
var waiter1 = evnt.WaitAsync(ct: new CancellationTokenSource(50).Token);
|
||||||
|
|
||||||
|
var result1 = await waiter1;
|
||||||
|
|
||||||
|
ClassicAssert.False(result1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using NUnit.Framework;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -25,20 +19,6 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(result.Success);
|
Assert.That(result.Success);
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase]
|
|
||||||
public void DeserializingInvalidJson_Should_GiveErrorResult()
|
|
||||||
{
|
|
||||||
// arrange
|
|
||||||
var client = new TestBaseClient();
|
|
||||||
|
|
||||||
// act
|
|
||||||
var result = client.SubClient.Deserialize<object>("{\"testProperty\": 123");
|
|
||||||
|
|
||||||
// assert
|
|
||||||
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")]
|
[TestCase("https://api.test.com/api", new[] { "path1", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
[TestCase("https://api.test.com/api", new[] { "path1", "/path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
|
[TestCase("https://api.test.com/api", new[] { "path1/", "path2" }, "https://api.test.com/api/path1/path2")]
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ using NUnit.Framework;
|
|||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
@@ -113,7 +111,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
new KeyValuePair<string, string[]>[0],
|
HttpVersion.Version11,
|
||||||
|
new HttpResponseMessage().Headers,
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
@@ -121,7 +120,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new HttpRequestMessage().Headers,
|
||||||
ResultDataSource.Server,
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
@@ -143,7 +142,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
{
|
{
|
||||||
var result = new WebCallResult<TestObjectResult>(
|
var result = new WebCallResult<TestObjectResult>(
|
||||||
System.Net.HttpStatusCode.OK,
|
System.Net.HttpStatusCode.OK,
|
||||||
new KeyValuePair<string, string[]>[0],
|
HttpVersion.Version11,
|
||||||
|
new HttpResponseMessage().Headers,
|
||||||
TimeSpan.FromSeconds(1),
|
TimeSpan.FromSeconds(1),
|
||||||
null,
|
null,
|
||||||
"{}",
|
"{}",
|
||||||
@@ -151,7 +151,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
"https://test.com/api",
|
"https://test.com/api",
|
||||||
null,
|
null,
|
||||||
HttpMethod.Get,
|
HttpMethod.Get,
|
||||||
new KeyValuePair<string, string[]>[0],
|
new HttpRequestMessage().Headers,
|
||||||
ResultDataSource.Server,
|
ResultDataSource.Server,
|
||||||
new TestObjectResult(),
|
new TestObjectResult(),
|
||||||
null);
|
null);
|
||||||
|
|||||||
@@ -1,19 +1,15 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\CryptoExchange.Net\.editorconfig" Link=".editorconfig" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference>
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"></PackageReference>
|
|
||||||
<PackageReference Include="Moq" Version="4.20.72" />
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
<PackageReference Include="NUnit" Version="4.3.2"></PackageReference>
|
<PackageReference Include="NUnit" Version="4.4.0"></PackageReference>
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference>
|
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1"></PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
@@ -32,6 +30,7 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.532, 0.532)]
|
||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Down, 0.5516592, 0.5516)]
|
||||||
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
[TestCase(0.1, 1, 0.0001, RoundingType.Closest, 0.5516592, 0.5517)]
|
||||||
|
[TestCase(0, 1, 0.000000001, RoundingType.Closest, 0.0000097232, 0.000009723)]
|
||||||
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
public void AdjustValueStepTests(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal input, decimal expected)
|
||||||
{
|
{
|
||||||
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
var result = ExchangeHelpers.AdjustValueStep(min, max, step, roundingType, input);
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[TestFixture()]
|
||||||
|
public class ExchangeSymbolCacheTests
|
||||||
|
{
|
||||||
|
private SharedSpotSymbol[] CreateTestSymbols()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
|
||||||
|
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
|
||||||
|
new SharedSpotSymbol("BTC", "EUR", "BTCEUR", true, TradingMode.Spot),
|
||||||
|
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
|
||||||
|
new SharedSpotSymbol("XRP", "USDT", "XRPUSDT", false, TradingMode.Spot)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private SharedSpotSymbol[] CreateFuturesSymbols()
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT-PERP", true, TradingMode.PerpetualLinear),
|
||||||
|
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT-PERP", true, TradingMode.PerpetualLinear)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void UpdateSymbolInfo_NewTopic_Should_AddToCache()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "NewExchange";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
|
||||||
|
// act
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
var hasCached = ExchangeSymbolCache.HasCached(topicId);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(hasCached, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void UpdateSymbolInfo_Should_StoreAllSymbols()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeWithSymbols";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
|
||||||
|
// act
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.True);
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCEUR"), Is.True);
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHBTC"), Is.True);
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "XRPUSDT"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void UpdateSymbolInfo_CalledTwiceWithinAnHour_Should_NotUpdate()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeNoUpdate";
|
||||||
|
var initialSymbols = new[]
|
||||||
|
{
|
||||||
|
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
|
||||||
|
};
|
||||||
|
var updatedSymbols = new[]
|
||||||
|
{
|
||||||
|
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot),
|
||||||
|
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, initialSymbols);
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, updatedSymbols);
|
||||||
|
|
||||||
|
// assert - should still have only the initial symbol since less than 60 minutes passed
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT"), Is.True);
|
||||||
|
// The second update should not have been applied
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, "ETHUSDT"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void UpdateSymbolInfo_WithEmptyArray_Should_CreateEmptyCache()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "EmptyExchange";
|
||||||
|
var symbols = Array.Empty<SharedSpotSymbol>();
|
||||||
|
|
||||||
|
// act
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
var hasCached = ExchangeSymbolCache.HasCached(topicId);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(hasCached, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void HasCached_NonExistentTopic_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.HasCached(nonExistentTopic);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void HasCached_ExistingTopicWithSymbols_Should_ReturnTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeWithData";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.HasCached(topicId);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void HasCached_ExistingTopicWithNoSymbols_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeNoData";
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, Array.Empty<SharedSpotSymbol>());
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.HasCached(topicId);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SupportsSymbol_ByName_ExistingSymbol_Should_ReturnTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeSupports";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "BTCUSDT");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SupportsSymbol_ByName_NonExistingSymbol_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeNoSupport";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, "LINKUSDT");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SupportsSymbol_ByName_NonExistentTopic_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, "BTCUSDT");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SupportsSymbol_BySharedSymbol_ExistingSymbol_Should_ReturnTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeSharedSymbol";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SupportsSymbol_BySharedSymbol_NonExistingSymbol_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeNoSharedSymbol";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "LINK", "USDT");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SupportsSymbol_BySharedSymbol_DifferentTradingMode_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeDifferentMode";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
var sharedSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.SupportsSymbol(topicId, sharedSymbol);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SupportsSymbol_BySharedSymbol_NonExistentTopic_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
var sharedSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.SupportsSymbol(nonExistentTopic, sharedSymbol);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbolsForBaseAsset_ExistingBaseAsset_Should_ReturnMatchingSymbols()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeBaseAsset";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.Not.Null);
|
||||||
|
Assert.That(result.Length, Is.EqualTo(2));
|
||||||
|
Assert.That(result.Any(x => x.QuoteAsset == "USDT"), Is.True);
|
||||||
|
Assert.That(result.Any(x => x.QuoteAsset == "EUR"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbolsForBaseAsset_CaseInsensitive_Should_ReturnMatchingSymbols()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeCaseInsensitive";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "btc");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.Not.Null);
|
||||||
|
Assert.That(result.Length, Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbolsForBaseAsset_NonExistingBaseAsset_Should_ReturnEmptyArray()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeNoBaseAsset";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "LINK");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.Not.Null);
|
||||||
|
Assert.That(result.Length, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbolsForBaseAsset_NonExistentTopic_Should_ReturnEmptyArray()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(nonExistentTopic, "BTC");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.Not.Null);
|
||||||
|
Assert.That(result.Length, Is.EqualTo(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ParseSymbol_ExistingSymbol_Should_ReturnSharedSymbol()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeParse";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.ParseSymbol(topicId, "BTCUSDT");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.Not.Null);
|
||||||
|
Assert.That(result.BaseAsset, Is.EqualTo("BTC"));
|
||||||
|
Assert.That(result.QuoteAsset, Is.EqualTo("USDT"));
|
||||||
|
Assert.That(result.TradingMode, Is.EqualTo(TradingMode.Spot));
|
||||||
|
Assert.That(result.SymbolName, Is.EqualTo("BTCUSDT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ParseSymbol_NonExistingSymbol_Should_ReturnNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeNoParse";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.ParseSymbol(topicId, "LINKUSDT");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ParseSymbol_NullSymbolName_Should_ReturnNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeNullSymbol";
|
||||||
|
var symbols = CreateTestSymbols();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.ParseSymbol(topicId, null);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ParseSymbol_NonExistentTopic_Should_ReturnNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var nonExistentTopic = "NonExistent_" + Guid.NewGuid();
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.ParseSymbol(nonExistentTopic, "BTCUSDT");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void MultipleTopics_Should_MaintainSeparateData()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topic1 = "Exchange1";
|
||||||
|
var topic2 = "Exchange2";
|
||||||
|
var symbols1 = new[]
|
||||||
|
{
|
||||||
|
new SharedSpotSymbol("BTC", "USDT", "BTCUSDT", true, TradingMode.Spot)
|
||||||
|
};
|
||||||
|
var symbols2 = new[]
|
||||||
|
{
|
||||||
|
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot)
|
||||||
|
};
|
||||||
|
|
||||||
|
// act
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topic1, symbols1);
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topic2, symbols2);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "BTCUSDT"), Is.True);
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic1, "ETHUSDT"), Is.False);
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "ETHUSDT"), Is.True);
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topic2, "BTCUSDT"), Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void UpdateSymbolInfo_WithDifferentTradingModes_Should_StoreCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeMixedModes";
|
||||||
|
var spotSymbols = CreateTestSymbols();
|
||||||
|
var futuresSymbols = CreateFuturesSymbols();
|
||||||
|
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
|
||||||
|
|
||||||
|
// act
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
var spotSymbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var futuresSymbol = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||||
|
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, spotSymbol), Is.True);
|
||||||
|
Assert.That(ExchangeSymbolCache.SupportsSymbol(topicId, futuresSymbol), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbolsForBaseAsset_Should_ReturnAllTradingModes()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeAllModes";
|
||||||
|
var spotSymbols = CreateTestSymbols();
|
||||||
|
var futuresSymbols = CreateFuturesSymbols();
|
||||||
|
var allSymbols = spotSymbols.Concat(futuresSymbols).ToArray();
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, allSymbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "BTC");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result.Length, Is.GreaterThanOrEqualTo(2));
|
||||||
|
Assert.That(result.Any(x => x.TradingMode == TradingMode.Spot), Is.True);
|
||||||
|
Assert.That(result.Any(x => x.TradingMode == TradingMode.PerpetualLinear), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbolsForBaseAsset_WithMultipleMatchingSymbols_Should_ReturnAll()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var topicId = "ExchangeMultiple";
|
||||||
|
var symbols = new[]
|
||||||
|
{
|
||||||
|
new SharedSpotSymbol("ETH", "USDT", "ETHUSDT", true, TradingMode.Spot),
|
||||||
|
new SharedSpotSymbol("ETH", "BTC", "ETHBTC", true, TradingMode.Spot),
|
||||||
|
new SharedSpotSymbol("ETH", "EUR", "ETHEUR", true, TradingMode.Spot)
|
||||||
|
};
|
||||||
|
ExchangeSymbolCache.UpdateSymbolInfo(topicId, symbols);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = ExchangeSymbolCache.GetSymbolsForBaseAsset(topicId, "ETH");
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result.Length, Is.EqualTo(3));
|
||||||
|
Assert.That(result.All(x => x.BaseAsset == "ETH"), Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,8 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ using System.Threading.Tasks;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using NUnit.Framework.Legacy;
|
using NUnit.Framework.Legacy;
|
||||||
using CryptoExchange.Net.RateLimiting;
|
using CryptoExchange.Net.RateLimiting;
|
||||||
using System.Net;
|
|
||||||
using CryptoExchange.Net.RateLimiting.Guards;
|
using CryptoExchange.Net.RateLimiting.Guards;
|
||||||
using CryptoExchange.Net.RateLimiting.Filters;
|
using CryptoExchange.Net.RateLimiting.Filters;
|
||||||
using CryptoExchange.Net.RateLimiting.Interfaces;
|
using CryptoExchange.Net.RateLimiting.Interfaces;
|
||||||
@@ -82,6 +81,25 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
Assert.That(result.Error is ServerError);
|
Assert.That(result.Error is ServerError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[TestCase]
|
||||||
|
public async Task ReceivingErrorAndNotParsingErrorAndInvalidJson_Should_ContainData()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var client = new TestRestClient();
|
||||||
|
var response = "<html>...</html>";
|
||||||
|
client.SetErrorWithResponse(response, System.Net.HttpStatusCode.BadRequest);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = await client.Api1.Request<TestObject>();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
ClassicAssert.IsFalse(result.Success);
|
||||||
|
Assert.That(result.Error != null);
|
||||||
|
Assert.That(result.Error is DeserializeError);
|
||||||
|
Assert.That(result.Error.Message.Contains(response));
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase]
|
[TestCase]
|
||||||
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
public async Task ReceivingErrorAndParsingError_Should_ResultInParsedError()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,633 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[TestFixture()]
|
||||||
|
public class SharedQuantityTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantityReference_IsZero_AllNull_Should_ReturnTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity = new SharedOrderQuantity(null, null, null);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity.IsZero, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantityReference_IsZero_AllZero_Should_ReturnTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity = new SharedOrderQuantity(0, 0, 0);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity.IsZero, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantityReference_IsZero_BaseAssetSet_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity = new SharedOrderQuantity(1.5m, null, null);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity.IsZero, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantityReference_IsZero_QuoteAssetSet_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity = new SharedOrderQuantity(null, 100m, null);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity.IsZero, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantityReference_IsZero_ContractsSet_Should_ReturnFalse()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity = new SharedOrderQuantity(null, null, 10m);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity.IsZero, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantityReference_IsZero_NegativeValue_Should_ReturnTrue()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity = new SharedOrderQuantity(-1m, 0, 0);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity.IsZero, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
|
||||||
|
{
|
||||||
|
// arrange & act
|
||||||
|
var quantity = new SharedQuantity();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
Assert.That(quantity.IsZero, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_Base_Should_SetBaseAssetQuantity()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var expectedQuantity = 1.5m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.Base(expectedQuantity);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuantity));
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_Base_WithZero_Should_SetZeroQuantity()
|
||||||
|
{
|
||||||
|
// arrange & act
|
||||||
|
var quantity = SharedQuantity.Base(0m);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0m));
|
||||||
|
Assert.That(quantity.IsZero, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_Base_WithLargeValue_Should_SetCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var largeValue = 999999.123456789m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.Base(largeValue);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(largeValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_Quote_Should_SetQuoteAssetQuantity()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var expectedQuantity = 100m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.Quote(expectedQuantity);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_Quote_WithDecimal_Should_PreserveDecimals()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var expectedQuantity = 50.123456m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.Quote(expectedQuantity);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(expectedQuantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_Contracts_Should_SetContractQuantity()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var expectedQuantity = 10m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.Contracts(expectedQuantity);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_Contracts_WithFractionalValue_Should_SetCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var expectedQuantity = 2.5m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.Contracts(expectedQuantity);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.EqualTo(expectedQuantity));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_BaseFromQuote_Should_CalculateCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 100m;
|
||||||
|
var price = 50m;
|
||||||
|
var expectedBase = 2m; // 100 / 50 = 2
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedBase));
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_BaseFromQuote_WithCustomDecimals_Should_RoundCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 100m;
|
||||||
|
var price = 3m;
|
||||||
|
var decimalPlaces = 2;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 100 / 3 = 33.333... should round to 33.33
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_BaseFromQuote_WithLotSize_Should_AdjustToLotSize()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 100m;
|
||||||
|
var price = 7m;
|
||||||
|
var lotSize = 0.1m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, 8, lotSize);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 100 / 7 = 14.285714... should adjust to nearest 0.1 = 14.3
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.3m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_BaseFromQuote_WithHighPrecision_Should_HandleCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 1000m;
|
||||||
|
var price = 0.00001m;
|
||||||
|
var decimalPlaces = 8;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price, decimalPlaces);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_QuoteFromBase_Should_CalculateCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 2m;
|
||||||
|
var price = 50m;
|
||||||
|
var expectedQuote = 100m; // 2 * 50 = 100
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedQuote));
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_QuoteFromBase_WithCustomDecimals_Should_RoundCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 1.234567m;
|
||||||
|
var price = 10m;
|
||||||
|
var decimalPlaces = 2;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 1.234567 * 10 = 12.34567 should round to 12.35
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(12.35m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_QuoteFromBase_WithLotSize_Should_AdjustToLotSize()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 3.456m;
|
||||||
|
var price = 10m;
|
||||||
|
var lotSize = 1m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, 8, lotSize);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 3.456 * 10 = 34.56 should adjust to nearest 1 = 35
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(35m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_QuoteFromBase_WithSmallValues_Should_HandleCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 0.001m;
|
||||||
|
var price = 0.1m;
|
||||||
|
var decimalPlaces = 8;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price, decimalPlaces);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(0.0001m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromBase_Should_CalculateCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 100m;
|
||||||
|
var contractSize = 10m;
|
||||||
|
var expectedContracts = 10m; // 100 / 10 = 10
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromBase_WithCustomDecimals_Should_RoundCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 100m;
|
||||||
|
var contractSize = 3m;
|
||||||
|
var decimalPlaces = 2;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, decimalPlaces);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 100 / 3 = 33.333... should round to 33.33
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(33.33m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromBase_WithLotSize_Should_AdjustToLotSize()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 100m;
|
||||||
|
var contractSize = 7m;
|
||||||
|
var lotSize = 0.5m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize, 8, lotSize);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 100 / 7 = 14.285714... should adjust to nearest 0.5 = 14.5
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(14.5m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromBase_WithFractionalContract_Should_HandleCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 1m;
|
||||||
|
var contractSize = 0.1m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(10m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromQuote_Should_CalculateCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 1000m;
|
||||||
|
var contractSize = 10m;
|
||||||
|
var price = 50m;
|
||||||
|
var expectedContracts = 2m; // 1000 / 50 / 10 = 2
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(expectedContracts));
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromQuote_WithCustomDecimals_Should_RoundCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 100m;
|
||||||
|
var contractSize = 3m;
|
||||||
|
var price = 7m;
|
||||||
|
var decimalPlaces = 2;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 100 / 7 / 3 = 4.761904... should round to 4.76
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(4.76m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromQuote_WithLotSize_Should_AdjustToLotSize()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 1000m;
|
||||||
|
var contractSize = 7m;
|
||||||
|
var price = 13m;
|
||||||
|
var lotSize = 0.5m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, 8, lotSize);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 1000 / 13 / 7 = 10.989... should adjust to nearest 0.5 = 11.0
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(11.0m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromQuote_WithComplexValues_Should_CalculateCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 5000m;
|
||||||
|
var contractSize = 0.01m;
|
||||||
|
var price = 25000m;
|
||||||
|
var decimalPlaces = 4;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price, decimalPlaces);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// 5000 / 25000 / 0.01 = 20
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(20m));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedOrderQuantity_DefaultConstructor_Should_SetAllPropertiesToNull()
|
||||||
|
{
|
||||||
|
// arrange & act
|
||||||
|
var quantity = new SharedOrderQuantity();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
Assert.That(quantity.IsZero, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetBaseAsset()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseAsset = 5m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = new SharedOrderQuantity(baseAssetQuantity: baseAsset);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetQuoteAsset()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteAsset = 100m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = new SharedOrderQuantity(quoteAssetQuantity: quoteAsset);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetContracts()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var contracts = 10m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = new SharedOrderQuantity(contractQuantity: contracts);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedOrderQuantity_ParameterizedConstructor_Should_SetAllValues()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseAsset = 1m;
|
||||||
|
var quoteAsset = 50m;
|
||||||
|
var contracts = 5m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = new SharedOrderQuantity(baseAsset, quoteAsset, contracts);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.EqualTo(baseAsset));
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.EqualTo(quoteAsset));
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.EqualTo(contracts));
|
||||||
|
Assert.That(quantity.IsZero, Is.False);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedOrderQuantity_ParameterizedConstructor_WithNullValues_Should_HandleCorrectly()
|
||||||
|
{
|
||||||
|
// arrange & act
|
||||||
|
var quantity = new SharedOrderQuantity(null, null, null);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInQuoteAsset, Is.Null);
|
||||||
|
Assert.That(quantity.QuantityInContracts, Is.Null);
|
||||||
|
Assert.That(quantity.IsZero, Is.True);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_RecordEquality_SameValues_Should_BeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity1 = SharedQuantity.Base(10m);
|
||||||
|
var quantity2 = SharedQuantity.Base(10m);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity1, Is.EqualTo(quantity2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_RecordEquality_DifferentValues_Should_NotBeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity1 = SharedQuantity.Base(10m);
|
||||||
|
var quantity2 = SharedQuantity.Base(20m);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_RecordEquality_DifferentTypes_Should_NotBeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity1 = SharedQuantity.Base(10m);
|
||||||
|
var quantity2 = SharedQuantity.Quote(10m);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity1, Is.Not.EqualTo(quantity2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedOrderQuantity_RecordEquality_SameValues_Should_BeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quantity1 = new SharedOrderQuantity(5m, 100m, 2m);
|
||||||
|
var quantity2 = new SharedOrderQuantity(5m, 100m, 2m);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(quantity1, Is.EqualTo(quantity2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_BaseFromQuote_WithDefaultParameters_Should_UseDefaults()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 100m;
|
||||||
|
var price = 3m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.BaseFromQuote(quoteQuantity, price);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
// Default decimalPlaces = 8, default lotSize = 0.00000001
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_QuoteFromBase_WithDefaultParameters_Should_UseDefaults()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 1.234567m;
|
||||||
|
var price = 10m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.QuoteFromBase(baseQuantity, price);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromBase_WithDefaultParameters_Should_UseDefaults()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseQuantity = 100m;
|
||||||
|
var contractSize = 3m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromBase(baseQuantity, contractSize);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedQuantity_ContractsFromQuote_WithDefaultParameters_Should_UseDefaults()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var quoteQuantity = 1000m;
|
||||||
|
var contractSize = 10m;
|
||||||
|
var price = 50m;
|
||||||
|
|
||||||
|
// act
|
||||||
|
var quantity = SharedQuantity.ContractsFromQuote(quoteQuantity, contractSize, price);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.Not.Null);
|
||||||
|
Assert.That(quantity.QuantityInBaseAsset, Is.GreaterThan(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,400 @@
|
|||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
{
|
||||||
|
[TestFixture()]
|
||||||
|
public class SharedSymbolTests
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_Constructor_Should_SetAllProperties()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var tradingMode = TradingMode.Spot;
|
||||||
|
var baseAsset = "BTC";
|
||||||
|
var quoteAsset = "USDT";
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
|
||||||
|
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
|
||||||
|
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
|
||||||
|
Assert.That(symbol.DeliverTime, Is.Null);
|
||||||
|
Assert.That(symbol.SymbolName, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_Constructor_WithDeliveryTime_Should_SetDeliveryTime()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var tradingMode = TradingMode.DeliveryLinear;
|
||||||
|
var baseAsset = "BTC";
|
||||||
|
var quoteAsset = "USDT";
|
||||||
|
var deliveryTime = new DateTime(2026, 6, 25, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliveryTime);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
|
||||||
|
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
|
||||||
|
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
|
||||||
|
Assert.That(symbol.DeliverTime, Is.EqualTo(deliveryTime));
|
||||||
|
Assert.That(symbol.SymbolName, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_Constructor_WithNullDeliveryTime_Should_SetToNull()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var tradingMode = TradingMode.Spot;
|
||||||
|
var baseAsset = "ETH";
|
||||||
|
var quoteAsset = "BTC";
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime: null);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.DeliverTime, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(TradingMode.Spot)]
|
||||||
|
[TestCase(TradingMode.PerpetualLinear)]
|
||||||
|
[TestCase(TradingMode.PerpetualInverse)]
|
||||||
|
[TestCase(TradingMode.DeliveryLinear)]
|
||||||
|
[TestCase(TradingMode.DeliveryInverse)]
|
||||||
|
public void SharedSymbol_Constructor_WithDifferentTradingModes_Should_SetCorrectly(TradingMode tradingMode)
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseAsset = "BTC";
|
||||||
|
var quoteAsset = "USDT";
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_ConstructorWithSymbolName_Should_SetSymbolName()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var tradingMode = TradingMode.Spot;
|
||||||
|
var baseAsset = "BTC";
|
||||||
|
var quoteAsset = "USDT";
|
||||||
|
var symbolName = "BTC-USDT";
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.TradingMode, Is.EqualTo(tradingMode));
|
||||||
|
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
|
||||||
|
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
|
||||||
|
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
|
||||||
|
Assert.That(symbol.DeliverTime, Is.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_ConstructorWithSymbolName_WithCustomFormat_Should_SetCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var tradingMode = TradingMode.PerpetualLinear;
|
||||||
|
var baseAsset = "ETH";
|
||||||
|
var quoteAsset = "USDT";
|
||||||
|
var symbolName = "ETHUSDT-PERP";
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.SymbolName, Is.EqualTo(symbolName));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_ConstructorWithSymbolName_WithEmptyString_Should_SetEmptyString()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var tradingMode = TradingMode.Spot;
|
||||||
|
var baseAsset = "BTC";
|
||||||
|
var quoteAsset = "USDT";
|
||||||
|
var symbolName = "";
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(tradingMode, baseAsset, quoteAsset, symbolName);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.SymbolName, Is.EqualTo(""));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbol_WithSymbolNameSet_Should_ReturnSymbolName()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "CUSTOM-BTC-USDT");
|
||||||
|
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
|
||||||
|
(b, q, t, d) => $"{b}{q}");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = symbol.GetSymbol(formatFunc);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.EqualTo("CUSTOM-BTC-USDT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbol_WithSymbolNameNull_Should_UseFormatFunction()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
|
||||||
|
(b, q, t, d) => $"{b}/{q}");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = symbol.GetSymbol(formatFunc);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.EqualTo("BTC/USDT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbol_WithComplexFormatFunction_Should_ApplyCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol = new SharedSymbol(TradingMode.PerpetualLinear, "ETH", "USDT");
|
||||||
|
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
|
||||||
|
(b, q, t, d) => t == TradingMode.PerpetualLinear ? $"{b}{q}-PERP" : $"{b}{q}");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = symbol.GetSymbol(formatFunc);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.EqualTo("ETHUSDT-PERP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbol_WithDeliveryTime_Should_PassDeliveryTimeToFormatter()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var deliveryTime = new DateTime(2026, 6, 25);
|
||||||
|
var symbol = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime);
|
||||||
|
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
|
||||||
|
(b, q, t, d) => d.HasValue ? $"{b}{q}_{d.Value:yyyyMMdd}" : $"{b}{q}");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = symbol.GetSymbol(formatFunc);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.EqualTo("BTCUSDT_20260625"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbol_WithTradingMode_Should_PassTradingModeToFormatter()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol = new SharedSymbol(TradingMode.PerpetualInverse, "BTC", "USD");
|
||||||
|
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
|
||||||
|
(b, q, t, d) =>
|
||||||
|
{
|
||||||
|
return t switch
|
||||||
|
{
|
||||||
|
TradingMode.Spot => $"{b}{q}",
|
||||||
|
TradingMode.PerpetualLinear => $"{b}{q}-PERP",
|
||||||
|
TradingMode.PerpetualInverse => $"{b}{q}I-PERP",
|
||||||
|
_ => $"{b}{q}"
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = symbol.GetSymbol(formatFunc);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.EqualTo("BTCUSDI-PERP"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbol_WithEmptySymbolName_Should_UseFormatFunction()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "");
|
||||||
|
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
|
||||||
|
(b, q, t, d) => $"{b}-{q}");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = symbol.GetSymbol(formatFunc);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.EqualTo("BTC-USDT"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void GetSymbol_WithWhitespaceSymbolName_Should_ReturnWhitespace()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", " ");
|
||||||
|
var formatFunc = new Func<string, string, TradingMode, DateTime?, string>(
|
||||||
|
(b, q, t, d) => $"{b}-{q}");
|
||||||
|
|
||||||
|
// act
|
||||||
|
var result = symbol.GetSymbol(formatFunc);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(result, Is.EqualTo(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_RecordEquality_SameValues_Should_BeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(symbol1, Is.EqualTo(symbol2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_RecordEquality_DifferentBaseAsset_Should_NotBeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var symbol2 = new SharedSymbol(TradingMode.Spot, "ETH", "USDT");
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_RecordEquality_DifferentQuoteAsset_Should_NotBeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "EUR");
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_RecordEquality_DifferentTradingMode_Should_NotBeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
var symbol2 = new SharedSymbol(TradingMode.PerpetualLinear, "BTC", "USDT");
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_RecordEquality_DifferentDeliveryTime_Should_NotBeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var deliveryTime1 = new DateTime(2026, 6, 25);
|
||||||
|
var deliveryTime2 = new DateTime(2026, 9, 25);
|
||||||
|
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime1);
|
||||||
|
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime2);
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_RecordEquality_DifferentSymbolName_Should_NotBeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
|
||||||
|
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTC-USDT");
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_RecordEquality_OneWithSymbolNameOneWithout_Should_NotBeEqual()
|
||||||
|
{
|
||||||
|
// NOTE; although this should probably be equal it's considered not because the SymbolName property isn't equal
|
||||||
|
// Overridding equality to ignore SymbolName would be possible but would break the default record equality behavior and cause confusion
|
||||||
|
|
||||||
|
// arrange
|
||||||
|
var symbol1 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT", "BTCUSDT");
|
||||||
|
var symbol2 = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(symbol1, Is.Not.EqualTo(symbol2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_RecordEquality_WithAllPropertiesSet_Should_BeEqual()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var deliveryTime = new DateTime(2026, 6, 25);
|
||||||
|
var symbol1 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
|
||||||
|
{
|
||||||
|
SymbolName = "BTCUSDT-0625"
|
||||||
|
};
|
||||||
|
var symbol2 = new SharedSymbol(TradingMode.DeliveryLinear, "BTC", "USDT", deliveryTime)
|
||||||
|
{
|
||||||
|
SymbolName = "BTCUSDT-0625"
|
||||||
|
};
|
||||||
|
|
||||||
|
// act & assert
|
||||||
|
Assert.That(symbol1, Is.EqualTo(symbol2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_Properties_Should_BeSettable()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, "BTC", "USDT");
|
||||||
|
|
||||||
|
// act
|
||||||
|
symbol.BaseAsset = "ETH";
|
||||||
|
symbol.QuoteAsset = "EUR";
|
||||||
|
symbol.TradingMode = TradingMode.PerpetualLinear;
|
||||||
|
symbol.SymbolName = "CUSTOM";
|
||||||
|
symbol.DeliverTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.BaseAsset, Is.EqualTo("ETH"));
|
||||||
|
Assert.That(symbol.QuoteAsset, Is.EqualTo("EUR"));
|
||||||
|
Assert.That(symbol.TradingMode, Is.EqualTo(TradingMode.PerpetualLinear));
|
||||||
|
Assert.That(symbol.SymbolName, Is.EqualTo("CUSTOM"));
|
||||||
|
Assert.That(symbol.DeliverTime, Is.Not.Null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_WithSpecialCharactersInAssets_Should_HandleCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseAsset = "BTC-123";
|
||||||
|
var quoteAsset = "USDT_2.0";
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
|
||||||
|
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void SharedSymbol_WithLongAssetNames_Should_HandleCorrectly()
|
||||||
|
{
|
||||||
|
// arrange
|
||||||
|
var baseAsset = "VERYLONGASSETNAMEFORTESTING";
|
||||||
|
var quoteAsset = "ANOTHERVERYLONGASSETNAME";
|
||||||
|
|
||||||
|
// act
|
||||||
|
var symbol = new SharedSymbol(TradingMode.Spot, baseAsset, quoteAsset);
|
||||||
|
|
||||||
|
// assert
|
||||||
|
Assert.That(symbol.BaseAsset, Is.EqualTo(baseAsset));
|
||||||
|
Assert.That(symbol.QuoteAsset, Is.EqualTo(quoteAsset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,231 +1,234 @@
|
|||||||
using System;
|
//using CryptoExchange.Net.Objects;
|
||||||
using System.Collections.Generic;
|
//using CryptoExchange.Net.Objects.Sockets;
|
||||||
using System.Text.Json;
|
//using CryptoExchange.Net.Sockets;
|
||||||
using System.Threading;
|
//using CryptoExchange.Net.Testing.Implementations;
|
||||||
using System.Threading.Tasks;
|
//using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using CryptoExchange.Net.Objects;
|
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
//using Microsoft.Extensions.Logging;
|
||||||
using CryptoExchange.Net.Sockets;
|
//using Moq;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
//using NUnit.Framework;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
|
//using NUnit.Framework.Legacy;
|
||||||
using Microsoft.Extensions.Logging;
|
//using System;
|
||||||
using Moq;
|
//using System.Collections.Generic;
|
||||||
using NUnit.Framework;
|
//using System.Net.Sockets;
|
||||||
using NUnit.Framework.Legacy;
|
//using System.Text.Json;
|
||||||
|
//using System.Threading;
|
||||||
|
//using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
//namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
//{
|
||||||
[TestFixture]
|
// [TestFixture]
|
||||||
public class SocketClientTests
|
// public class SocketClientTests
|
||||||
{
|
// {
|
||||||
[TestCase]
|
// [TestCase]
|
||||||
public void SettingOptions_Should_ResultInOptionsSet()
|
// public void SettingOptions_Should_ResultInOptionsSet()
|
||||||
{
|
// {
|
||||||
//arrange
|
// //arrange
|
||||||
//act
|
// //act
|
||||||
var client = new TestSocketClient(options =>
|
// var client = new TestSocketClient(options =>
|
||||||
{
|
// {
|
||||||
options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
// options.SubOptions.ApiCredentials = new Authentication.ApiCredentials("1", "2");
|
||||||
options.SubOptions.MaxSocketConnections = 1;
|
// options.SubOptions.MaxSocketConnections = 1;
|
||||||
});
|
// });
|
||||||
|
|
||||||
//assert
|
// //assert
|
||||||
ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
// ClassicAssert.NotNull(client.SubClient.ApiOptions.ApiCredentials);
|
||||||
Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
// Assert.That(1 == client.SubClient.ApiOptions.MaxSocketConnections);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase(true)]
|
// [TestCase(true)]
|
||||||
[TestCase(false)]
|
// [TestCase(false)]
|
||||||
public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
// public void ConnectSocket_Should_ReturnConnectionResult(bool canConnect)
|
||||||
{
|
// {
|
||||||
//arrange
|
// //arrange
|
||||||
var client = new TestSocketClient();
|
// var client = new TestSocketClient();
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = canConnect;
|
// socket.CanConnect = canConnect;
|
||||||
|
|
||||||
//act
|
// //act
|
||||||
var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
|
// var connectResult = client.SubClient.ConnectSocketSub(
|
||||||
|
// new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
//assert
|
// //assert
|
||||||
Assert.That(connectResult.Success == canConnect);
|
// Assert.That(connectResult.Success == canConnect);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase]
|
// [TestCase]
|
||||||
public void SocketMessages_Should_BeProcessedInDataHandlers()
|
// public void SocketMessages_Should_BeProcessedInDataHandlers()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options => {
|
// var client = new TestSocketClient(options => {
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
// options.ReconnectInterval = TimeSpan.Zero;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var rstEvent = new ManualResetEvent(false);
|
// var rstEvent = new ManualResetEvent(false);
|
||||||
Dictionary<string, string> result = null;
|
// Dictionary<string, string> result = null;
|
||||||
|
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
// client.SubClient.ConnectSocketSub(sub);
|
||||||
|
|
||||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||||
{
|
// {
|
||||||
result = messageEvent.Data;
|
// result = messageEvent.Data;
|
||||||
rstEvent.Set();
|
// rstEvent.Set();
|
||||||
});
|
// });
|
||||||
sub.AddSubscription(subObj);
|
// sub.AddSubscription(subObj);
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
// socket.InvokeMessage("{\"property\": \"123\", \"action\": \"update\", \"topic\": \"topic\"}");
|
||||||
rstEvent.WaitOne(1000);
|
// rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(result["property"] == "123");
|
// Assert.That(result["property"] == "123");
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase(false)]
|
// [TestCase(false)]
|
||||||
[TestCase(true)]
|
// [TestCase(true)]
|
||||||
public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
// public void SocketMessages_Should_ContainOriginalDataIfEnabled(bool enabled)
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options =>
|
// var client = new TestSocketClient(options =>
|
||||||
{
|
// {
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
// options.ReconnectInterval = TimeSpan.Zero;
|
||||||
options.SubOptions.OutputOriginalData = enabled;
|
// options.SubOptions.OutputOriginalData = enabled;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var rstEvent = new ManualResetEvent(false);
|
// var rstEvent = new ManualResetEvent(false);
|
||||||
string original = null;
|
// string original = null;
|
||||||
|
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
// client.SubClient.ConnectSocketSub(sub);
|
||||||
var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
// var subObj = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) =>
|
||||||
{
|
// {
|
||||||
original = messageEvent.OriginalData;
|
// original = messageEvent.OriginalData;
|
||||||
rstEvent.Set();
|
// rstEvent.Set();
|
||||||
});
|
// });
|
||||||
sub.AddSubscription(subObj);
|
// sub.AddSubscription(subObj);
|
||||||
var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
|
// var msgToSend = JsonSerializer.Serialize(new { topic = "topic", action = "update", property = "123" });
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
socket.InvokeMessage(msgToSend);
|
// socket.InvokeMessage(msgToSend);
|
||||||
rstEvent.WaitOne(1000);
|
// rstEvent.WaitOne(1000);
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(original == (enabled ? msgToSend : null));
|
// Assert.That(original == (enabled ? msgToSend : null));
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public void UnsubscribingStream_Should_CloseTheSocket()
|
// public void UnsubscribingStream_Should_CloseTheSocket()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options =>
|
// var client = new TestSocketClient(options =>
|
||||||
{
|
// {
|
||||||
options.ReconnectInterval = TimeSpan.Zero;
|
// options.ReconnectInterval = TimeSpan.Zero;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
// var sub = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
client.SubClient.ConnectSocketSub(sub);
|
// client.SubClient.ConnectSocketSub(sub);
|
||||||
|
|
||||||
var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
// var subscription = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||||
var ups = new UpdateSubscription(sub, subscription);
|
// var ups = new UpdateSubscription(sub, subscription);
|
||||||
sub.AddSubscription(subscription);
|
// sub.AddSubscription(subscription);
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
client.UnsubscribeAsync(ups).Wait();
|
// client.UnsubscribeAsync(ups).Wait();
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(socket.Connected == false);
|
// Assert.That(socket.Connected == false);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public void UnsubscribingAll_Should_CloseAllSockets()
|
// public void UnsubscribingAll_Should_CloseAllSockets()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||||
var socket1 = client.CreateSocket();
|
// var socket1 = client.CreateSocket();
|
||||||
var socket2 = client.CreateSocket();
|
// var socket2 = client.CreateSocket();
|
||||||
socket1.CanConnect = true;
|
// socket1.CanConnect = true;
|
||||||
socket2.CanConnect = true;
|
// socket2.CanConnect = true;
|
||||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
|
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
|
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
client.SubClient.ConnectSocketSub(sub1);
|
// client.SubClient.ConnectSocketSub(sub1);
|
||||||
client.SubClient.ConnectSocketSub(sub2);
|
// client.SubClient.ConnectSocketSub(sub2);
|
||||||
var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
// var subscription1 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||||
var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
// var subscription2 = new TestSubscription<Dictionary<string, string>>(Mock.Of<ILogger>(), (messageEvent) => { });
|
||||||
|
|
||||||
sub1.AddSubscription(subscription1);
|
// sub1.AddSubscription(subscription1);
|
||||||
sub2.AddSubscription(subscription2);
|
// sub2.AddSubscription(subscription2);
|
||||||
var ups1 = new UpdateSubscription(sub1, subscription1);
|
// var ups1 = new UpdateSubscription(sub1, subscription1);
|
||||||
var ups2 = new UpdateSubscription(sub2, subscription2);
|
// var ups2 = new UpdateSubscription(sub2, subscription2);
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
client.UnsubscribeAllAsync().Wait();
|
// client.UnsubscribeAllAsync().Wait();
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(socket1.Connected == false);
|
// Assert.That(socket1.Connected == false);
|
||||||
Assert.That(socket2.Connected == false);
|
// Assert.That(socket2.Connected == false);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public void FailingToConnectSocket_Should_ReturnError()
|
// public void FailingToConnectSocket_Should_ReturnError()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
// var client = new TestSocketClient(options => { options.ReconnectInterval = TimeSpan.Zero; });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = false;
|
// socket.CanConnect = false;
|
||||||
var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
|
// var sub1 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "");
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
// var connectResult = client.SubClient.ConnectSocketSub(sub1);
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
ClassicAssert.IsFalse(connectResult.Success);
|
// ClassicAssert.IsFalse(connectResult.Success);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
// public async Task ErrorResponse_ShouldNot_ConfirmSubscription()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var channel = "trade_btcusd";
|
// var channel = "trade_btcusd";
|
||||||
var client = new TestSocketClient(opt =>
|
// var client = new TestSocketClient(opt =>
|
||||||
{
|
// {
|
||||||
opt.OutputOriginalData = true;
|
// opt.OutputOriginalData = true;
|
||||||
opt.SocketSubscriptionsCombineTarget = 1;
|
// opt.SocketSubscriptionsCombineTarget = 1;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||||
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
|
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "error" }));
|
||||||
await sub;
|
// await sub;
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
|
// ClassicAssert.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed);
|
||||||
}
|
// }
|
||||||
|
|
||||||
[TestCase()]
|
// [TestCase()]
|
||||||
public async Task SuccessResponse_Should_ConfirmSubscription()
|
// public async Task SuccessResponse_Should_ConfirmSubscription()
|
||||||
{
|
// {
|
||||||
// arrange
|
// // arrange
|
||||||
var channel = "trade_btcusd";
|
// var channel = "trade_btcusd";
|
||||||
var client = new TestSocketClient(opt =>
|
// var client = new TestSocketClient(opt =>
|
||||||
{
|
// {
|
||||||
opt.OutputOriginalData = true;
|
// opt.OutputOriginalData = true;
|
||||||
opt.SocketSubscriptionsCombineTarget = 1;
|
// opt.SocketSubscriptionsCombineTarget = 1;
|
||||||
});
|
// });
|
||||||
var socket = client.CreateSocket();
|
// var socket = client.CreateSocket();
|
||||||
socket.CanConnect = true;
|
// socket.CanConnect = true;
|
||||||
client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
|
// client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""));
|
||||||
|
|
||||||
// act
|
// // act
|
||||||
var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
// var sub = client.SubClient.SubscribeToSomethingAsync(channel, onUpdate => {}, ct: default);
|
||||||
socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
|
// socket.InvokeMessage(JsonSerializer.Serialize(new { channel, action = "subscribe", status = "confirmed" }));
|
||||||
await sub;
|
// await sub;
|
||||||
|
|
||||||
// assert
|
// // assert
|
||||||
Assert.That(client.SubClient.TestSubscription.Confirmed);
|
// Assert.That(client.SubClient.TestSubscription.Status == SubscriptionStatus.Subscribed);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
//}
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ using System.Text.Json;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using NUnit.Framework.Legacy;
|
|
||||||
using CryptoExchange.Net.Converters;
|
using CryptoExchange.Net.Converters;
|
||||||
using CryptoExchange.Net.Testing.Comparers;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using CryptoExchange.Net.Objects.Errors;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using CryptoExchange.Net.Sockets;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations.Sockets
|
|
||||||
{
|
|
||||||
internal class SubResponse
|
|
||||||
{
|
|
||||||
|
|
||||||
[JsonPropertyName("action")]
|
|
||||||
public string Action { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("channel")]
|
|
||||||
public string Channel { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public string Status { get; set; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class UnsubResponse
|
|
||||||
{
|
|
||||||
[JsonPropertyName("action")]
|
|
||||||
public string Action { get; set; } = null!;
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public string Status { get; set; } = null!;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal class TestChannelQuery : Query<SubResponse>
|
|
||||||
{
|
|
||||||
public TestChannelQuery(string channel, string request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
|
||||||
{
|
|
||||||
MessageMatcher = MessageMatcher.Create<SubResponse>(request + "-" + channel, HandleMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CallResult<SubResponse> HandleMessage(SocketConnection connection, DataEvent<SubResponse> message)
|
|
||||||
{
|
|
||||||
if (!message.Data.Status.Equals("confirmed", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return new CallResult<SubResponse>(new ServerError(ErrorInfo.Unknown with { Message = message.Data.Status }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return message.ToCallResult();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
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 TestQuery(string identifier, object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
|
|
||||||
{
|
|
||||||
MessageMatcher = MessageMatcher.Create<object>(identifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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 TestSubscription(ILogger logger, Action<DataEvent<T>> handler) : base(logger, false)
|
|
||||||
{
|
|
||||||
_handler = handler;
|
|
||||||
|
|
||||||
MessageMatcher = MessageMatcher.Create<T>("update-topic", DoHandleMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
|
|
||||||
{
|
|
||||||
_handler.Invoke(message);
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Query GetSubQuery(SocketConnection connection) => new TestQuery("sub", new object(), false, 1);
|
|
||||||
protected override Query GetUnsubQuery(SocketConnection connection) => new TestQuery("unsub", new object(), false, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-34
@@ -1,34 +0,0 @@
|
|||||||
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 TestSubscriptionWithResponseCheck(string channel, Action<DataEvent<T>> handler) : base(Mock.Of<ILogger>(), false)
|
|
||||||
{
|
|
||||||
MessageMatcher = MessageMatcher.Create<T>(channel, DoHandleMessage);
|
|
||||||
_handler = handler;
|
|
||||||
_channel = channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CallResult DoHandleMessage(SocketConnection connection, DataEvent<T> message)
|
|
||||||
{
|
|
||||||
_handler.Invoke(message);
|
|
||||||
return new CallResult(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Query GetSubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "subscribe", false, 1);
|
|
||||||
protected override Query GetUnsubQuery(SocketConnection connection) => new TestChannelQuery(_channel, "unsubscribe", false, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Objects.Errors;
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
@@ -46,27 +44,19 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
public class TestSubClient : RestApiClient
|
public class TestSubClient : RestApiClient
|
||||||
{
|
{
|
||||||
|
protected override IRestMessageHandler MessageHandler => throw new NotImplementedException();
|
||||||
|
|
||||||
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
public TestSubClient(RestExchangeOptions<TestEnvironment> options, RestApiOptions apiOptions) : base(new TraceLogger(), null, "https://localhost:123", options, apiOptions)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public CallResult<T> Deserialize<T>(string data)
|
public CallResult<T> Deserialize<T>(string data)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
|
return new CallResult<T>(JsonSerializer.Deserialize<T>(data));
|
||||||
var accessor = CreateAccessor();
|
|
||||||
var valid = accessor.Read(stream, true).Result;
|
|
||||||
if (!valid)
|
|
||||||
return new CallResult<T>(new ServerError(ErrorInfo.Unknown with { Message = data }));
|
|
||||||
|
|
||||||
var deserializeResult = accessor.Deserialize<T>();
|
|
||||||
return deserializeResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
public override TimeSpan? GetTimeOffset() => null;
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo() => null;
|
|
||||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials) => throw new NotImplementedException();
|
||||||
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
protected override Task<WebCallResult<DateTime>> GetServerTimestampAsync() => throw new NotImplementedException();
|
||||||
@@ -74,6 +64,8 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
|
|
||||||
public class TestAuthProvider : AuthenticationProvider
|
public class TestAuthProvider : AuthenticationProvider
|
||||||
{
|
{
|
||||||
|
public override ApiCredentialsType[] SupportedCredentialTypes => [ApiCredentialsType.Hmac];
|
||||||
|
|
||||||
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
public TestAuthProvider(ApiCredentials credentials) : base(credentials)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -85,4 +77,14 @@ namespace CryptoExchange.Net.UnitTests
|
|||||||
public string GetKey() => _credentials.Key;
|
public string GetKey() => _credentials.Key;
|
||||||
public string GetSecret() => _credentials.Secret;
|
public string GetSecret() => _credentials.Secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class TestEnvironment : TradeEnvironment
|
||||||
|
{
|
||||||
|
public string TestAddress { get; }
|
||||||
|
|
||||||
|
public TestEnvironment(string name, string url) : base(name)
|
||||||
|
{
|
||||||
|
TestAddress = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ using CryptoExchange.Net.Authentication;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using CryptoExchange.Net.Clients;
|
using CryptoExchange.Net.Clients;
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
using CryptoExchange.Net.Converters.SystemTextJson;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using CryptoExchange.Net.Objects.Errors;
|
using System.Net.Http.Headers;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
{
|
{
|
||||||
@@ -49,19 +50,19 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
var response = new Mock<IResponse>();
|
var response = new Mock<IResponse>();
|
||||||
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
response.Setup(c => c.IsSuccessStatusCode).Returns(true);
|
||||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new Dictionary<string, string[]>();
|
var headers = new HttpRequestMessage().Headers;
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
request.Setup(c => c.SetContent(It.IsAny<string>(), It.IsAny<string>())).Callback(new Action<string, string>((content, type) => { request.Setup(r => r.Content).Returns(content); }));
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(key, new string[] { val }));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
|
request.Setup(c => c.GetHeaders()).Returns(() => headers);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||||
{
|
{
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
@@ -69,8 +70,8 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) =>
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) =>
|
||||||
{
|
{
|
||||||
request.Setup(a => a.Uri).Returns(uri);
|
request.Setup(a => a.Uri).Returns(uri);
|
||||||
request.Setup(a => a.Method).Returns(method);
|
request.Setup(a => a.Method).Returns(method);
|
||||||
@@ -86,16 +87,16 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
|
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Throws(we);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,29 +109,31 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
|
|
||||||
var response = new Mock<IResponse>();
|
var response = new Mock<IResponse>();
|
||||||
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
response.Setup(c => c.IsSuccessStatusCode).Returns(false);
|
||||||
response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
|
response.Setup(c => c.GetResponseStreamAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream));
|
||||||
|
|
||||||
var headers = new List<KeyValuePair<string, string[]>>();
|
var headers = new List<KeyValuePair<string, string[]>>();
|
||||||
var request = new Mock<IRequest>();
|
var request = new Mock<IRequest>();
|
||||||
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
request.Setup(c => c.Uri).Returns(new Uri("http://www.test.com"));
|
||||||
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
request.Setup(c => c.GetResponseAsync(It.IsAny<CancellationToken>())).Returns(Task.FromResult(response.Object));
|
||||||
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
request.Setup(c => c.AddHeader(It.IsAny<string>(), It.IsAny<string>())).Callback<string, string>((key, val) => headers.Add(new KeyValuePair<string, string[]>(key, new string[] { val })));
|
||||||
request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
|
request.Setup(c => c.GetHeaders()).Returns(new HttpRequestMessage().Headers);
|
||||||
|
|
||||||
var factory = Mock.Get(Api1.RequestFactory);
|
var factory = Mock.Get(Api1.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
|
|
||||||
factory = Mock.Get(Api2.RequestFactory);
|
factory = Mock.Get(Api2.RequestFactory);
|
||||||
factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
|
||||||
.Callback<HttpMethod, Uri, int>((method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri))
|
||||||
.Returns(request.Object);
|
.Returns(request.Object);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi1Client : RestApiClient
|
public class TestRestApi1Client : RestApiClient
|
||||||
{
|
{
|
||||||
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||||
|
|
||||||
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
public TestRestApi1Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api1Options)
|
||||||
{
|
{
|
||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
@@ -139,7 +142,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
||||||
|
|
||||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions() { TypeInfoResolver = new TestSerializerContext() });
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
|
||||||
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
public async Task<CallResult<T>> Request<T>(CancellationToken ct = default) where T : class
|
||||||
@@ -157,11 +159,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
ParameterPositions[method] = position;
|
ParameterPositions[method] = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
@@ -169,21 +166,17 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestRestApi2Client : RestApiClient
|
public class TestRestApi2Client : RestApiClient
|
||||||
{
|
{
|
||||||
|
protected override IRestMessageHandler MessageHandler { get; } = new TestRestMessageHandler();
|
||||||
|
|
||||||
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
public TestRestApi2Client(TestClientOptions options) : base(new TraceLogger(), null, "https://localhost:123", options, options.Api2Options)
|
||||||
{
|
{
|
||||||
RequestFactory = new Mock<IRequestFactory>().Object;
|
RequestFactory = new Mock<IRequestFactory>().Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override IStreamMessageAccessor CreateAccessor() => new SystemTextJsonStreamMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
protected override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -193,19 +186,7 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
{
|
{
|
||||||
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
return await SendAsync<T>("http://www.test.com", new RequestDefinition("/", HttpMethod.Get) { Weight = 0 }, null, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception exception)
|
|
||||||
{
|
|
||||||
var errorData = accessor.Deserialize<TestError>();
|
|
||||||
|
|
||||||
return new ServerError(errorData.Data.ErrorCode, GetErrorInfo(errorData.Data.ErrorCode, errorData.Data.ErrorMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
public override TimeSpan? GetTimeOffset()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
||||||
=> new TestAuthProvider(credentials);
|
=> new TestAuthProvider(credentials);
|
||||||
|
|
||||||
@@ -214,10 +195,6 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TimeSyncInfo GetTimeSyncInfo()
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TestError
|
public class TestError
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
||||||
|
{
|
||||||
|
internal class TestRestMessageHandler : JsonRestMessageHandler
|
||||||
|
{
|
||||||
|
private ErrorMapping _errorMapping = new ErrorMapping([]);
|
||||||
|
public override JsonSerializerOptions Options => new JsonSerializerOptions();
|
||||||
|
|
||||||
|
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
|
||||||
|
{
|
||||||
|
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
|
||||||
|
if (result.Item1 != null)
|
||||||
|
return result.Item1;
|
||||||
|
|
||||||
|
var errorData = result.Item2.Deserialize<TestError>();
|
||||||
|
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
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 CryptoExchange.Net.Testing.Implementations;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using CryptoExchange.Net.Converters.SystemTextJson;
|
|
||||||
using System.Net.WebSockets;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests.TestImplementations
|
|
||||||
{
|
|
||||||
internal class TestSocketClient: BaseSocketClient
|
|
||||||
{
|
|
||||||
public TestSubSocketClient SubClient { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new instance of KucoinSocketClient
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="optionsFunc">Configure the options to use for this client</param>
|
|
||||||
public TestSocketClient(Action<TestSocketOptions> optionsDelegate = null)
|
|
||||||
: this(Options.Create(ApplyOptionsDelegate(optionsDelegate)), null)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public TestSocketClient(IOptions<TestSocketOptions> options, ILoggerFactory loggerFactory = null) : base(loggerFactory, "Test")
|
|
||||||
{
|
|
||||||
Initialize(options.Value);
|
|
||||||
|
|
||||||
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("https://test.com"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public TestSocket CreateSocket()
|
|
||||||
{
|
|
||||||
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/");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestEnvironment : TradeEnvironment
|
|
||||||
{
|
|
||||||
public string TestAddress { get; }
|
|
||||||
|
|
||||||
public TestEnvironment(string name, string url) : base(name)
|
|
||||||
{
|
|
||||||
TestAddress = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestSocketOptions: SocketExchangeOptions<TestEnvironment>
|
|
||||||
{
|
|
||||||
public static TestSocketOptions Default = new TestSocketOptions
|
|
||||||
{
|
|
||||||
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 Subscription TestSubscription { get; private set; } = null;
|
|
||||||
|
|
||||||
public TestSubSocketClient(TestSocketOptions options, SocketApiOptions apiOptions) : base(new TraceLogger(), options.Environment.TestAddress, options, apiOptions)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected internal override IByteMessageAccessor CreateAccessor(WebSocketMessageType type) => new SystemTextJsonByteMessageAccessor(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
protected internal override IMessageSerializer CreateSerializer() => new SystemTextJsonMessageSerializer(new System.Text.Json.JsonSerializerOptions());
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override string FormatSymbol(string baseAsset, string quoteAsset, TradingMode futuresType, DateTime? deliverDate = null) => $"{baseAsset.ToUpperInvariant()}{quoteAsset.ToUpperInvariant()}";
|
|
||||||
|
|
||||||
internal IWebsocket CreateSocketInternal(string address)
|
|
||||||
{
|
|
||||||
return CreateSocket(address);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials)
|
|
||||||
=> new TestAuthProvider(credentials);
|
|
||||||
|
|
||||||
public CallResult ConnectSocketSub(SocketConnection sub)
|
|
||||||
{
|
|
||||||
return ConnectSocketAsync(sub, default).Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string GetListenerIdentifier(IMessageAccessor message)
|
|
||||||
{
|
|
||||||
if (!message.IsValid)
|
|
||||||
{
|
|
||||||
return "topic";
|
|
||||||
}
|
|
||||||
|
|
||||||
var id = message.GetValue<string>(_channelPath);
|
|
||||||
id ??= message.GetValue<string>(_topicPath);
|
|
||||||
|
|
||||||
return message.GetValue<string>(_actionPath) + "-" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<CallResult<UpdateSubscription>> SubscribeToSomethingAsync(string channel, Action<DataEvent<string>> onUpdate, CancellationToken ct)
|
|
||||||
{
|
|
||||||
TestSubscription = new TestSubscriptionWithResponseCheck<string>(channel, onUpdate);
|
|
||||||
return SubscribeAsync(TestSubscription, ct);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
using CryptoExchange.Net.UnitTests.TestImplementations;
|
using CryptoExchange.Net.UnitTests.TestImplementations;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.UnitTests
|
namespace CryptoExchange.Net.UnitTests
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,183 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
|
|
||||||
# Indentation and spacing
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
charset = utf-8
|
|
||||||
max_line_length = 140
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
# ReSharper code style properties
|
|
||||||
resharper_csharp_keep_existing_embedded_arrangement = false
|
|
||||||
resharper_csharp_place_accessorholder_attribute_on_same_line = false
|
|
||||||
resharper_csharp_wrap_after_declaration_lpar = true
|
|
||||||
resharper_csharp_wrap_parameters_style = chop_if_long
|
|
||||||
resharper_csharp_blank_lines_around_single_line_auto_property = 1
|
|
||||||
resharper_csharp_keep_blank_lines_in_declarations = 1
|
|
||||||
resharper_trailing_comma_in_multiline_lists = true
|
|
||||||
|
|
||||||
[*.cs]
|
|
||||||
|
|
||||||
indent_size = 4
|
|
||||||
|
|
||||||
# Code style conventions
|
|
||||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
|
||||||
dotnet_style_collection_initializer = true:suggestion
|
|
||||||
dotnet_style_object_initializer = true:suggestion
|
|
||||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
|
||||||
csharp_style_expression_bodied_methods = true:suggestion
|
|
||||||
csharp_style_namespace_declarations = file_scoped:warning
|
|
||||||
dotnet_style_coalesce_expression = true:suggestion
|
|
||||||
dotnet_style_null_propagation = true:suggestion
|
|
||||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
|
||||||
dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
|
|
||||||
csharp_prefer_braces = when_multiline:warning
|
|
||||||
|
|
||||||
# Analyzer preferences
|
|
||||||
dotnet_diagnostic.CA2007.severity = warning # Call ConfigureAwait on the awaited Task.
|
|
||||||
dotnet_code_quality.CA2007.exclude_async_void_methods = true
|
|
||||||
dotnet_code_quality.CA2007.output_kind = DynamicallyLinkedLibrary
|
|
||||||
|
|
||||||
dotnet_diagnostic.CA1000.severity = none # Do not declare static members on generic types
|
|
||||||
dotnet_diagnostic.CA1051.severity = none # Do not declare visible instance fields
|
|
||||||
dotnet_diagnostic.CA1510.severity = none # Use ArgumentNullException throw helper
|
|
||||||
dotnet_diagnostic.CA1720.severity = none # Identifiers should not contain type names
|
|
||||||
dotnet_diagnostic.CA1716.severity = none # Identifiers should not match keywords
|
|
||||||
dotnet_diagnostic.CA1835.severity = none # Use ArgumentNullException throw helper
|
|
||||||
dotnet_diagnostic.CA1846.severity = none # Prefer AsSpan over Substring
|
|
||||||
dotnet_diagnostic.CA1848.severity = none # Use the LoggerMessage delegates
|
|
||||||
dotnet_diagnostic.CA1850.severity = none # Prefer static HashData method over ComputeHash
|
|
||||||
dotnet_diagnostic.CA1866.severity = none # Use 'string.Method(char)' instead of 'string.Method(string)' for string with single char
|
|
||||||
dotnet_diagnostic.CA2201.severity = none # Do not raise reserved exception types
|
|
||||||
dotnet_diagnostic.CA2208.severity = none # Do not raise reserved exception types
|
|
||||||
dotnet_diagnostic.IDE0005.severity = warning # Using directive is unnecessary
|
|
||||||
|
|
||||||
[*.xml]
|
|
||||||
ij_xml_space_inside_empty_tag = true
|
|
||||||
[*.cs]
|
|
||||||
#### Naming styles ####
|
|
||||||
|
|
||||||
# Naming rules
|
|
||||||
|
|
||||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = warning
|
|
||||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
|
||||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
|
||||||
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = warning
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
|
||||||
|
|
||||||
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.severity = warning
|
|
||||||
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.symbols = private_or_internal_field
|
|
||||||
dotnet_naming_rule.private_or_internal_field_should_be_fields_start_with__.style = fields_start_with__
|
|
||||||
|
|
||||||
# Symbol specifications
|
|
||||||
|
|
||||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
|
||||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.interface.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
|
||||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
|
|
||||||
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
|
|
||||||
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
|
|
||||||
|
|
||||||
# Naming styles
|
|
||||||
|
|
||||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
|
||||||
dotnet_naming_style.begins_with_i.required_suffix =
|
|
||||||
dotnet_naming_style.begins_with_i.word_separator =
|
|
||||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
|
||||||
|
|
||||||
dotnet_naming_style.pascal_case.required_prefix =
|
|
||||||
dotnet_naming_style.pascal_case.required_suffix =
|
|
||||||
dotnet_naming_style.pascal_case.word_separator =
|
|
||||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
|
||||||
|
|
||||||
dotnet_naming_style.fields_start_with__.required_prefix = _
|
|
||||||
dotnet_naming_style.fields_start_with__.required_suffix =
|
|
||||||
dotnet_naming_style.fields_start_with__.word_separator =
|
|
||||||
dotnet_naming_style.fields_start_with__.capitalization = camel_case
|
|
||||||
csharp_indent_labels = one_less_than_current
|
|
||||||
csharp_using_directive_placement = outside_namespace:suggestion
|
|
||||||
csharp_prefer_simple_using_statement = true:suggestion
|
|
||||||
csharp_style_prefer_method_group_conversion = true:silent
|
|
||||||
csharp_style_prefer_top_level_statements = true:silent
|
|
||||||
csharp_style_prefer_primary_constructors = true:suggestion
|
|
||||||
csharp_prefer_system_threading_lock = true:suggestion
|
|
||||||
csharp_style_expression_bodied_constructors = false:silent
|
|
||||||
csharp_style_expression_bodied_operators = false:silent
|
|
||||||
csharp_style_expression_bodied_properties = true:suggestion
|
|
||||||
csharp_style_expression_bodied_indexers = true:suggestion
|
|
||||||
csharp_style_expression_bodied_accessors = true:suggestion
|
|
||||||
csharp_style_expression_bodied_lambdas = true:silent
|
|
||||||
csharp_style_expression_bodied_local_functions = true:silent
|
|
||||||
|
|
||||||
[*.vb]
|
|
||||||
#### Naming styles ####
|
|
||||||
|
|
||||||
# Naming rules
|
|
||||||
|
|
||||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
|
||||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
|
||||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
|
||||||
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
|
||||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
|
||||||
|
|
||||||
# Symbol specifications
|
|
||||||
|
|
||||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
|
||||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
|
|
||||||
dotnet_naming_symbols.interface.required_modifiers =
|
|
||||||
|
|
||||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
|
||||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, friend, private, protected, protected_friend, private_protected
|
|
||||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
|
||||||
|
|
||||||
# Naming styles
|
|
||||||
|
|
||||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
|
||||||
dotnet_naming_style.begins_with_i.required_suffix =
|
|
||||||
dotnet_naming_style.begins_with_i.word_separator =
|
|
||||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
|
||||||
|
|
||||||
dotnet_naming_style.pascal_case.required_prefix =
|
|
||||||
dotnet_naming_style.pascal_case.required_suffix =
|
|
||||||
dotnet_naming_style.pascal_case.word_separator =
|
|
||||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
|
||||||
|
|
||||||
[*.{cs,vb}]
|
|
||||||
#### Naming styles ####
|
|
||||||
|
|
||||||
# Naming rules
|
|
||||||
|
|
||||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
|
||||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
|
||||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
|
||||||
|
|
||||||
# Symbol specifications
|
|
||||||
|
|
||||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
|
||||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
|
||||||
dotnet_naming_symbols.types.required_modifiers =
|
|
||||||
|
|
||||||
# Naming styles
|
|
||||||
|
|
||||||
dotnet_naming_style.pascal_case.required_prefix =
|
|
||||||
dotnet_naming_style.pascal_case.required_suffix =
|
|
||||||
dotnet_naming_style.pascal_case.word_separator =
|
|
||||||
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
|
||||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
|
||||||
tab_width = 4
|
|
||||||
end_of_line = crlf
|
|
||||||
dotnet_style_coalesce_expression = true:suggestion
|
|
||||||
dotnet_style_null_propagation = true:suggestion
|
|
||||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("CryptoExchange.Net.UnitTests")]
|
||||||
|
|
||||||
namespace System.Runtime.CompilerServices;
|
namespace System.Runtime.CompilerServices
|
||||||
|
{
|
||||||
internal static class IsExternalInit { }
|
internal static class IsExternalInit { }
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Attributes;
|
namespace CryptoExchange.Net.Attributes
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used for conversion in ArrayConverter
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public class JsonConversionAttribute: Attribute
|
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Used for conversion in ArrayConverter
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class JsonConversionAttribute: Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Attributes;
|
namespace CryptoExchange.Net.Attributes
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Map a enum entry to string values
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Field)]
|
|
||||||
public class MapAttribute : Attribute
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Values mapping to the enum entry
|
/// Map a enum entry to string values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] Values { get; set; }
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
|
public class MapAttribute : Attribute
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="maps"></param>
|
|
||||||
public MapAttribute(params string[] maps)
|
|
||||||
{
|
{
|
||||||
Values = maps;
|
/// <summary>
|
||||||
|
/// Values mapping to the enum entry
|
||||||
|
/// </summary>
|
||||||
|
public string[] Values { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maps"></param>
|
||||||
|
public MapAttribute(params string[] maps)
|
||||||
|
{
|
||||||
|
Values = maps;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,101 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Authentication;
|
namespace CryptoExchange.Net.Authentication
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Api credentials, used to sign requests accessing private endpoints
|
|
||||||
/// </summary>
|
|
||||||
public class ApiCredentials
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The api key / label to authenticate requests
|
/// Api credentials, used to sign requests accessing private endpoints
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Key { get; set; }
|
public class ApiCredentials
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The api secret or private key to authenticate requests
|
|
||||||
/// </summary>
|
|
||||||
public string Secret { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The api passphrase. Not needed on all exchanges
|
|
||||||
/// </summary>
|
|
||||||
public string? Pass { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Type of the credentials
|
|
||||||
/// </summary>
|
|
||||||
public ApiCredentialsType CredentialType { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create Api credentials providing an api key and secret for authentication
|
|
||||||
/// </summary>
|
|
||||||
/// <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="pass">The api pass for the key. Not always needed</param>
|
|
||||||
/// <param name="credentialType">The type of credentials</param>
|
|
||||||
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
/// <summary>
|
||||||
throw new ArgumentException("Key and secret can't be null/empty");
|
/// The api key / label to authenticate requests
|
||||||
|
/// </summary>
|
||||||
|
public string Key { get; set; }
|
||||||
|
|
||||||
CredentialType = credentialType;
|
/// <summary>
|
||||||
Key = key;
|
/// The api secret or private key to authenticate requests
|
||||||
Secret = secret;
|
/// </summary>
|
||||||
Pass = pass;
|
public string Secret { get; set; }
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Copy the credentials
|
/// The api passphrase. Not needed on all exchanges
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
public string? Pass { get; set; }
|
||||||
public virtual ApiCredentials Copy()
|
|
||||||
{
|
/// <summary>
|
||||||
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
/// Type of the credentials
|
||||||
|
/// </summary>
|
||||||
|
public ApiCredentialsType CredentialType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create Api credentials providing an api key and secret for authentication
|
||||||
|
/// </summary>
|
||||||
|
/// <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="pass">The api pass for the key. Not always needed</param>
|
||||||
|
/// <param name="credentialType">The type of credentials</param>
|
||||||
|
public ApiCredentials(string key, string secret, string? pass = null, ApiCredentialsType credentialType = ApiCredentialsType.Hmac)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(secret))
|
||||||
|
throw new ArgumentException("Key and secret can't be null/empty");
|
||||||
|
|
||||||
|
CredentialType = credentialType;
|
||||||
|
Key = key;
|
||||||
|
Secret = secret;
|
||||||
|
Pass = pass;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create API credentials using an API key and secret generated by the server
|
||||||
|
/// </summary>
|
||||||
|
public static ApiCredentials HmacCredentials(string apiKey, string apiSecret, string? pass)
|
||||||
|
{
|
||||||
|
return new ApiCredentials(apiKey, apiSecret, pass, ApiCredentialsType.Hmac);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create API credentials using an API key and an RSA private key in PEM format
|
||||||
|
/// </summary>
|
||||||
|
public static ApiCredentials RsaPemCredentials(string apiKey, string privateKey)
|
||||||
|
{
|
||||||
|
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaPem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create API credentials using an API key and an RSA private key in XML format
|
||||||
|
/// </summary>
|
||||||
|
public static ApiCredentials RsaXmlCredentials(string apiKey, string privateKey)
|
||||||
|
{
|
||||||
|
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.RsaXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create API credentials using an API key and an Ed25519 private key
|
||||||
|
/// </summary>
|
||||||
|
public static ApiCredentials Ed25519Credentials(string apiKey, string privateKey)
|
||||||
|
{
|
||||||
|
return new ApiCredentials(apiKey, privateKey, credentialType: ApiCredentialsType.Ed25519);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Load a key from a file
|
||||||
|
/// </summary>
|
||||||
|
public static string ReadFromFile(string path)
|
||||||
|
{
|
||||||
|
using var fileStream = File.OpenRead(path);
|
||||||
|
using var streamReader = new StreamReader(fileStream);
|
||||||
|
return streamReader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copy the credentials
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual ApiCredentials Copy()
|
||||||
|
{
|
||||||
|
return new ApiCredentials(Key, Secret, Pass, CredentialType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
namespace CryptoExchange.Net.Authentication;
|
namespace CryptoExchange.Net.Authentication
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Credentials type
|
|
||||||
/// </summary>
|
|
||||||
public enum ApiCredentialsType
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hmac keys credentials
|
/// Credentials type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Hmac,
|
public enum ApiCredentialsType
|
||||||
/// <summary>
|
{
|
||||||
/// Rsa keys credentials in xml format
|
/// <summary>
|
||||||
/// </summary>
|
/// Hmac keys credentials
|
||||||
RsaXml,
|
/// </summary>
|
||||||
/// <summary>
|
Hmac,
|
||||||
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
/// <summary>
|
||||||
/// </summary>
|
/// Rsa keys credentials in xml format
|
||||||
RsaPem
|
/// </summary>
|
||||||
|
RsaXml,
|
||||||
|
/// <summary>
|
||||||
|
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
|
||||||
|
/// </summary>
|
||||||
|
RsaPem,
|
||||||
|
/// <summary>
|
||||||
|
/// Ed25519 keys credentials
|
||||||
|
/// </summary>
|
||||||
|
Ed25519
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,17 @@
|
|||||||
namespace CryptoExchange.Net.Authentication;
|
namespace CryptoExchange.Net.Authentication
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Output string type
|
|
||||||
/// </summary>
|
|
||||||
public enum SignOutputType
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Hex string
|
/// Output string type
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Hex,
|
public enum SignOutputType
|
||||||
/// <summary>
|
{
|
||||||
/// Base64 string
|
/// <summary>
|
||||||
/// </summary>
|
/// Hex string
|
||||||
Base64
|
/// </summary>
|
||||||
|
Hex,
|
||||||
|
/// <summary>
|
||||||
|
/// Base64 string
|
||||||
|
/// </summary>
|
||||||
|
Base64
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,58 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Caching;
|
namespace CryptoExchange.Net.Caching
|
||||||
|
|
||||||
internal class MemoryCache
|
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
internal class MemoryCache
|
||||||
private readonly object _lock = new object();
|
|
||||||
|
|
||||||
/// <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);
|
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
|
||||||
_cache.AddOrUpdate(key, cacheItem, (key, val1) => cacheItem);
|
#if NET9_0_OR_GREATER
|
||||||
}
|
private readonly Lock _lock = new Lock();
|
||||||
|
#else
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
#endif
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get a cached value
|
/// Add a new cache entry. Will override an existing entry if it already exists
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="key">The key identifier</param>
|
/// <param name="key">The key identifier</param>
|
||||||
/// <param name="maxAge">The max age of the cached entry</param>
|
/// <param name="value">Cache value</param>
|
||||||
/// <returns>Cached value if it was in cache</returns>
|
public void Add(string key, object value)
|
||||||
public object? Get(string key, TimeSpan maxAge)
|
|
||||||
{
|
|
||||||
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
|
|
||||||
_cache.TryRemove(item.Key, out _);
|
|
||||||
|
|
||||||
_cache.TryGetValue(key, out CacheItem? value);
|
|
||||||
if (value == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private class CacheItem
|
|
||||||
{
|
|
||||||
public DateTime CacheTime { get; }
|
|
||||||
public object Value { get; }
|
|
||||||
|
|
||||||
public CacheItem(DateTime cacheTime, object value)
|
|
||||||
{
|
{
|
||||||
CacheTime = cacheTime;
|
var cacheItem = new CacheItem(DateTime.UtcNow, value);
|
||||||
Value = 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)
|
||||||
|
{
|
||||||
|
foreach (var item in _cache.Where(x => DateTime.UtcNow - x.Value.CacheTime > maxAge).ToList())
|
||||||
|
_cache.TryRemove(item.Key, out _);
|
||||||
|
|
||||||
|
_cache.TryGetValue(key, out CacheItem? value);
|
||||||
|
if (value == null)
|
||||||
|
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,140 +1,152 @@
|
|||||||
using System;
|
using System;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using CryptoExchange.Net.Objects.Errors;
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients;
|
namespace CryptoExchange.Net.Clients
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base API for all API clients
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logger
|
/// Base API for all API clients
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected ILogger _logger;
|
public abstract class BaseApiClient : IDisposable, IBaseApiClient
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If we are disposing
|
|
||||||
/// </summary>
|
|
||||||
protected bool _disposing;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The authentication provider for this API client. (null if no credentials are set)
|
|
||||||
/// </summary>
|
|
||||||
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The environment this client communicates to
|
|
||||||
/// </summary>
|
|
||||||
public string BaseAddress { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Output the original string data along with the deserialized object
|
|
||||||
/// </summary>
|
|
||||||
public bool OutputOriginalData { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Authenticated => ApiCredentials != null;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public ApiCredentials? ApiCredentials { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Api options
|
|
||||||
/// </summary>
|
|
||||||
public ApiOptions ApiOptions { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client Options
|
|
||||||
/// </summary>
|
|
||||||
public ExchangeOptions ClientOptions { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Mapping of a response code to known error types
|
|
||||||
/// </summary>
|
|
||||||
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="logger">Logger</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>
|
|
||||||
/// <param name="apiOptions">Api options</param>
|
|
||||||
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
/// <summary>
|
||||||
|
/// Client name
|
||||||
|
/// </summary>
|
||||||
|
protected string? _clientName;
|
||||||
|
|
||||||
ClientOptions = clientOptions;
|
/// <summary>
|
||||||
ApiOptions = apiOptions;
|
/// Logger
|
||||||
OutputOriginalData = outputOriginalData;
|
/// </summary>
|
||||||
BaseAddress = baseAddress;
|
protected ILogger _logger;
|
||||||
ApiCredentials = apiCredentials?.Copy();
|
|
||||||
|
|
||||||
if (ApiCredentials != null)
|
/// <summary>
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
/// If we are disposing
|
||||||
}
|
/// </summary>
|
||||||
|
protected bool _disposing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
/// Name of the client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="credentials"></param>
|
protected internal string ClientName
|
||||||
/// <returns></returns>
|
{
|
||||||
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
get
|
||||||
|
{
|
||||||
|
if (_clientName != null)
|
||||||
|
return _clientName;
|
||||||
|
|
||||||
/// <inheritdoc />
|
_clientName = GetType().Name;
|
||||||
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
return _clientName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get error info for a response code
|
/// The authentication provider for this API client. (null if no credentials are set)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
|
public AuthenticationProvider? AuthenticationProvider { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get error info for a response code
|
/// The environment this client communicates to
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
public string BaseAddress { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
/// Output the original string data along with the deserialized object
|
||||||
{
|
/// </summary>
|
||||||
ApiCredentials = credentials?.Copy();
|
public bool OutputOriginalData { get; }
|
||||||
if (ApiCredentials != null)
|
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
public bool Authenticated => ApiCredentials != null;
|
||||||
{
|
|
||||||
ClientOptions.Proxy = options.Proxy;
|
|
||||||
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
|
||||||
|
|
||||||
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
/// <inheritdoc />
|
||||||
if (ApiCredentials != null)
|
public ApiCredentials? ApiCredentials { get; set; }
|
||||||
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Api options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public ApiOptions ApiOptions { get; }
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Client Options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void Dispose(bool disposing)
|
public ExchangeOptions ClientOptions { get; }
|
||||||
{
|
|
||||||
_disposing = true;
|
/// <summary>
|
||||||
|
/// Mapping of a response code to known error types
|
||||||
|
/// </summary>
|
||||||
|
protected internal virtual ErrorMapping ErrorMapping { get; } = new ErrorMapping([]);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger</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>
|
||||||
|
/// <param name="apiOptions">Api options</param>
|
||||||
|
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
|
||||||
|
ClientOptions = clientOptions;
|
||||||
|
ApiOptions = apiOptions;
|
||||||
|
OutputOriginalData = outputOriginalData;
|
||||||
|
BaseAddress = baseAddress;
|
||||||
|
ApiCredentials = apiCredentials?.Copy();
|
||||||
|
|
||||||
|
if (ApiCredentials != null)
|
||||||
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an AuthenticationProvider implementation instance based on the provided credentials
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract string FormatSymbol(string baseAsset, string quoteAsset, TradingMode tradingMode, DateTime? deliverDate = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get error info for a response code
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo GetErrorInfo(int code, string? message = null) => GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get error info for a response code
|
||||||
|
/// </summary>
|
||||||
|
public ErrorInfo GetErrorInfo(string code, string? message = null) => ErrorMapping.GetErrorInfo(code.ToString(), message);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
|
{
|
||||||
|
ApiCredentials = credentials?.Copy();
|
||||||
|
if (ApiCredentials != null)
|
||||||
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual void SetOptions<T>(UpdateOptions<T> options) where T : ApiCredentials
|
||||||
|
{
|
||||||
|
ClientOptions.Proxy = options.Proxy;
|
||||||
|
ClientOptions.RequestTimeout = options.RequestTimeout ?? ClientOptions.RequestTimeout;
|
||||||
|
|
||||||
|
ApiCredentials = options.ApiCredentials?.Copy() ?? ApiCredentials;
|
||||||
|
if (ApiCredentials != null)
|
||||||
|
AuthenticationProvider = CreateAuthenticationProvider(ApiCredentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispose
|
||||||
|
/// </summary>
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
_disposing = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,142 +1,139 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Objects.Options;
|
using CryptoExchange.Net.Objects.Options;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients;
|
namespace CryptoExchange.Net.Clients
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The base for all clients, websocket client and rest client
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseClient : IDisposable
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version of the CryptoExchange.Net base library
|
/// The base for all clients, websocket client and rest client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
|
public abstract class BaseClient : IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Version of the CryptoExchange.Net base library
|
||||||
|
/// </summary>
|
||||||
|
public Version CryptoExchangeLibVersion { get; } = typeof(BaseClient).Assembly.GetName().Version!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version of the client implementation
|
/// Version of the client implementation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Version ExchangeLibVersion
|
public Version ExchangeLibVersion
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
|
||||||
lock(_versionLock)
|
|
||||||
{
|
{
|
||||||
if (_exchangeVersion == null)
|
lock(_versionLock)
|
||||||
_exchangeVersion = GetType().Assembly.GetName().Version!;
|
{
|
||||||
|
if (_exchangeVersion == null)
|
||||||
|
_exchangeVersion = GetType().Assembly.GetName().Version!;
|
||||||
|
|
||||||
return _exchangeVersion;
|
return _exchangeVersion;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The name of the API the client is for
|
/// The name of the API the client is for
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Exchange { get; }
|
public string Exchange { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Api clients in this client
|
/// Whether client is disposed
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
public bool Disposed { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The log object
|
/// Api clients in this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal ILogger _logger;
|
internal List<BaseApiClient> ApiClients { get; } = new List<BaseApiClient>();
|
||||||
|
|
||||||
private readonly object _versionLock = new object();
|
/// <summary>
|
||||||
private Version _exchangeVersion;
|
/// The log object
|
||||||
|
/// </summary>
|
||||||
|
protected internal ILogger _logger;
|
||||||
|
|
||||||
/// <summary>
|
#if NET9_0_OR_GREATER
|
||||||
/// Provided client options
|
private readonly Lock _versionLock = new Lock();
|
||||||
/// </summary>
|
#else
|
||||||
public ExchangeOptions ClientOptions { get; private set; }
|
private readonly object _versionLock = new object();
|
||||||
|
#endif
|
||||||
|
private Version _exchangeVersion;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Provided client options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">Logger</param>
|
public ExchangeOptions ClientOptions { get; private set; }
|
||||||
/// <param name="exchange">The name of the exchange this client is for</param>
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">Logger</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.
|
#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 exchange)
|
protected BaseClient(ILoggerFactory? logger, string exchange)
|
||||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||||
{
|
|
||||||
Exchange = exchange;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initialize the client with the specified options
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="options"></param>
|
|
||||||
/// <exception cref="ArgumentNullException"></exception>
|
|
||||||
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{CryptoExchangeVersion}, {Exchange}.Net: v{ExchangeVersion}", options, CryptoExchangeLibVersion, Exchange, ExchangeLibVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="credentials">The credentials to set</param>
|
|
||||||
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetApiCredentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Register an API client
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The client</param>
|
|
||||||
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
|
|
||||||
{
|
|
||||||
if (ClientOptions == null)
|
|
||||||
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
|
||||||
|
|
||||||
_logger.Log(LogLevel.Trace, " {ApiClient}, base address: {BaseAddress}", apiClient.GetType().Name, apiClient.BaseAddress);
|
|
||||||
ApiClients.Add(apiClient);
|
|
||||||
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>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose
|
|
||||||
/// </summary>
|
|
||||||
public virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
{
|
||||||
_logger.Log(LogLevel.Debug, "Disposing client");
|
Exchange = exchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initialize the client with the specified options
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options"></param>
|
||||||
|
/// <exception cref="ArgumentNullException"></exception>
|
||||||
|
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{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials">The credentials to set</param>
|
||||||
|
protected virtual void SetApiCredentials<T>(T credentials) where T : ApiCredentials
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetApiCredentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register an API client
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="apiClient">The client</param>
|
||||||
|
protected T AddApiClient<T>(T apiClient) where T : BaseApiClient
|
||||||
|
{
|
||||||
|
if (ClientOptions == null)
|
||||||
|
throw new InvalidOperationException("Client should have called Initialize before adding API clients");
|
||||||
|
|
||||||
|
ApiClients.Add(apiClient);
|
||||||
|
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>
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
Disposed = true;
|
||||||
|
|
||||||
foreach (var client in ApiClients)
|
foreach (var client in ApiClients)
|
||||||
client.Dispose();
|
client.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients;
|
namespace CryptoExchange.Net.Clients
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base rest client
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Base rest client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="loggerFactory">Logger factory</param>
|
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
|
||||||
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
|
||||||
{
|
{
|
||||||
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
/// <inheritdoc />
|
||||||
|
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">Logger factory</param>
|
||||||
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
|
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
|
{
|
||||||
|
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
|
|
||||||
|
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,130 +1,137 @@
|
|||||||
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
|
using CryptoExchange.Net.Logging.Extensions;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Logging.Extensions;
|
|
||||||
using CryptoExchange.Net.Objects.Sockets;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients;
|
namespace CryptoExchange.Net.Clients
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base for socket client implementations
|
|
||||||
/// </summary>
|
|
||||||
public abstract class BaseSocketClient : BaseClient, ISocketClient
|
|
||||||
{
|
{
|
||||||
#region fields
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If client is disposing
|
/// Base for socket client implementations
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected bool _disposing;
|
public abstract class BaseSocketClient : BaseClient, ISocketClient
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
|
||||||
/// <inheritdoc />
|
|
||||||
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="loggerFactory">Logger factory</param>
|
|
||||||
/// <param name="name">The name of the exchange this client is for</param>
|
|
||||||
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
|
||||||
{
|
{
|
||||||
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
#region fields
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe an update subscription
|
/// If client is disposing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
protected bool _disposing;
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
/// <inheritdoc />
|
||||||
{
|
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||||
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
/// <inheritdoc />
|
||||||
|
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||||
|
/// <inheritdoc />
|
||||||
|
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggerFactory">Logger factory</param>
|
||||||
|
/// <param name="name">The name of the exchange this client is for</param>
|
||||||
|
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
|
||||||
{
|
{
|
||||||
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
|
||||||
if (result)
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
|
||||||
/// Unsubscribe an update subscription
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="subscription">The subscription to unsubscribe</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
|
|
||||||
{
|
|
||||||
if (subscription == null)
|
|
||||||
throw new ArgumentNullException(nameof(subscription));
|
|
||||||
|
|
||||||
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
|
||||||
await subscription.CloseAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Unsubscribe all subscriptions
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task UnsubscribeAllAsync()
|
|
||||||
{
|
|
||||||
var tasks = new List<Task>();
|
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
|
||||||
tasks.Add(client.UnsubscribeAllAsync());
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reconnect all connections
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
public virtual async Task ReconnectAsync()
|
|
||||||
{
|
|
||||||
_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);
|
/// <summary>
|
||||||
}
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||||
/// Log the current state of connections and subscriptions
|
/// <returns></returns>
|
||||||
/// </summary>
|
public virtual async Task UnsubscribeAsync(int subscriptionId)
|
||||||
public string GetSubscriptionsState()
|
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>().Where(c => c.CurrentSubscriptions > 0))
|
|
||||||
{
|
{
|
||||||
result.AppendLine(client.GetSubscriptionsState());
|
foreach (var socket in ApiClients.OfType<SocketApiClient>())
|
||||||
|
{
|
||||||
|
var result = await socket.UnsubscribeAsync(subscriptionId).ConfigureAwait(false);
|
||||||
|
if (result)
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.ToString();
|
/// <summary>
|
||||||
}
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
/// Returns the state of all socket api clients
|
/// <returns></returns>
|
||||||
/// </summary>
|
public virtual async Task UnsubscribeAsync(UpdateSubscription subscription)
|
||||||
/// <returns></returns>
|
|
||||||
public List<SocketApiClient.SocketApiClientState> GetSocketApiClientStates()
|
|
||||||
{
|
|
||||||
var result = new List<SocketApiClient.SocketApiClientState>();
|
|
||||||
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
|
||||||
{
|
{
|
||||||
result.Add(client.GetState());
|
if (subscription == null)
|
||||||
|
throw new ArgumentNullException(nameof(subscription));
|
||||||
|
|
||||||
|
_logger.UnsubscribingSubscription(subscription.SocketId, subscription.Id);
|
||||||
|
await subscription.CloseAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
/// <summary>
|
||||||
|
/// Unsubscribe all subscriptions
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task UnsubscribeAllAsync()
|
||||||
|
{
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
foreach (var client in ApiClients.OfType<SocketApiClient>())
|
||||||
|
tasks.Add(client.UnsubscribeAllAsync());
|
||||||
|
|
||||||
|
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect all connections
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public virtual async Task ReconnectAsync()
|
||||||
|
{
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Log the current state of connections and subscriptions
|
||||||
|
/// </summary>
|
||||||
|
public string GetSubscriptionsState()
|
||||||
|
{
|
||||||
|
var result = new StringBuilder();
|
||||||
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +1,67 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients;
|
namespace CryptoExchange.Net.Clients
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base crypto client
|
|
||||||
/// </summary>
|
|
||||||
public class CryptoBaseClient : IDisposable
|
|
||||||
{
|
{
|
||||||
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Service provider
|
/// Base crypto client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly IServiceProvider? _serviceProvider;
|
public class CryptoBaseClient : IDisposable
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public CryptoBaseClient() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="serviceProvider"></param>
|
|
||||||
public CryptoBaseClient(IServiceProvider serviceProvider)
|
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
private readonly Dictionary<Type, object> _serviceCache = new Dictionary<Type, object>();
|
||||||
_serviceCache = new Dictionary<Type, object>();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Try get a client by type for the service collection
|
/// Service provider
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
protected readonly IServiceProvider? _serviceProvider;
|
||||||
/// <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)
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CryptoBaseClient() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serviceProvider"></param>
|
||||||
|
public CryptoBaseClient(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
// Create with default options
|
_serviceProvider = serviceProvider;
|
||||||
var createResult = createFunc();
|
_serviceCache = new Dictionary<Type, object>();
|
||||||
_serviceCache.Add(typeof(T), createResult!);
|
|
||||||
return createResult;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = _serviceProvider.GetService<T>()
|
/// <summary>
|
||||||
?? 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");
|
/// Try get a client by type for the service collection
|
||||||
_serviceCache.Add(type, result!);
|
/// </summary>
|
||||||
return result;
|
/// <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;
|
||||||
|
|
||||||
/// <summary>
|
if (_serviceProvider == null)
|
||||||
/// Dispose
|
{
|
||||||
/// </summary>
|
// Create with default options
|
||||||
public void Dispose(bool disposing)
|
var createResult = createFunc();
|
||||||
{
|
_serviceCache.Add(typeof(T), createResult!);
|
||||||
if (disposing)
|
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();
|
_serviceCache.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Dispose
|
|
||||||
/// </summary>
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Dispose(true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,24 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients;
|
namespace CryptoExchange.Net.Clients
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// ctor
|
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
|
||||||
/// </summary>
|
|
||||||
public CryptoRestClient()
|
|
||||||
{
|
{
|
||||||
}
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CryptoRestClient()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceProvider"></param>
|
/// <param name="serviceProvider"></param>
|
||||||
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
public CryptoRestClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
||||||
{
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,24 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces.Clients;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Clients;
|
namespace CryptoExchange.Net.Clients
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// ctor
|
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
|
||||||
/// </summary>
|
|
||||||
public CryptoSocketClient()
|
|
||||||
{
|
{
|
||||||
}
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CryptoSocketClient()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="serviceProvider"></param>
|
/// <param name="serviceProvider"></param>
|
||||||
public CryptoSocketClient(IServiceProvider serviceProvider) : base(serviceProvider)
|
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,24 +1,25 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters;
|
namespace CryptoExchange.Net.Converters
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Mark property as an index in the array
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(AttributeTargets.Property)]
|
|
||||||
public class ArrayPropertyAttribute : Attribute
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The index in the array
|
/// Mark property as an index in the array
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Index { get; }
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class ArrayPropertyAttribute : Attribute
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="index"></param>
|
|
||||||
public ArrayPropertyAttribute(int index)
|
|
||||||
{
|
{
|
||||||
Index = index;
|
/// <summary>
|
||||||
|
/// The index in the array
|
||||||
|
/// </summary>
|
||||||
|
public int Index { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index"></param>
|
||||||
|
public ArrayPropertyAttribute(int index)
|
||||||
|
{
|
||||||
|
Index = index;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,29 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters;
|
namespace CryptoExchange.Net.Converters
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Caching for JsonSerializerContext instances
|
|
||||||
/// </summary>
|
|
||||||
public static class JsonSerializerContextCache
|
|
||||||
{
|
{
|
||||||
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
|
/// Caching for JsonSerializerContext instances
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
|
public static class JsonSerializerContextCache
|
||||||
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
|
|
||||||
{
|
{
|
||||||
var contextType = typeof(T);
|
private static ConcurrentDictionary<Type, JsonSerializerContext> _cache = new ConcurrentDictionary<Type, JsonSerializerContext>();
|
||||||
if (_cache.TryGetValue(contextType, out var context))
|
|
||||||
return context;
|
|
||||||
|
|
||||||
var instance = new T();
|
/// <summary>
|
||||||
_cache[contextType] = instance;
|
/// Get the instance of the provided type T. It will be created if it doesn't exist yet.
|
||||||
return instance;
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Implementation type of the JsonSerializerContext</typeparam>
|
||||||
|
public static JsonSerializerContext GetOrCreate<T>() where T: JsonSerializerContext, new()
|
||||||
|
{
|
||||||
|
var contextType = typeof(T);
|
||||||
|
if (_cache.TryGetValue(contextType, out var context))
|
||||||
|
return context;
|
||||||
|
|
||||||
|
var instance = new T();
|
||||||
|
_cache[contextType] = instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// REST message handler
|
||||||
|
/// </summary>
|
||||||
|
public interface IRestMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The `accept` HTTP response header for the request
|
||||||
|
/// </summary>
|
||||||
|
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a seekable stream is required
|
||||||
|
/// </summary>
|
||||||
|
bool RequiresSeekableStream { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse the response when the HTTP response status indicated an error
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<Error> ParseErrorResponse(
|
||||||
|
int httpStatusCode,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse the response when the HTTP response status indicated a rate limit error
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||||
|
int httpStatusCode,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the response is an error response; if so return the error.<br />
|
||||||
|
/// Note that if the API returns a standard result wrapper, something like this:
|
||||||
|
/// <code>{ "code": 400, "msg": "error", "data": {} }</code>
|
||||||
|
/// then the `CheckDeserializedResponse` method should be used for checking the result
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<Error?> CheckForErrorResponse(
|
||||||
|
RequestDefinition request,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize the response stream
|
||||||
|
/// </summary>
|
||||||
|
ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(
|
||||||
|
Stream responseStream,
|
||||||
|
CancellationToken ct);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the resulting T object indicates an error or not
|
||||||
|
/// </summary>
|
||||||
|
Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// WebSocket message handler
|
||||||
|
/// </summary>
|
||||||
|
public interface ISocketMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get an identifier for the message which can be used to determine the type of the message
|
||||||
|
/// </summary>
|
||||||
|
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get optional topic filter, for example a symbol name
|
||||||
|
/// </summary>
|
||||||
|
string? GetTopicFilter(object deserializedObject);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize to the provided type
|
||||||
|
/// </summary>
|
||||||
|
object Deserialize(ReadOnlySpan<byte> data, Type type);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Message type definition
|
||||||
|
/// </summary>
|
||||||
|
public class MessageTypeDefinition
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to immediately select the definition when it is matched. Can only be used when the evaluator has a single unique field to look for
|
||||||
|
/// </summary>
|
||||||
|
public bool ForceIfFound { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The fields a message needs to contain for this definition
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference[] Fields { get; set; } = [];
|
||||||
|
/// <summary>
|
||||||
|
/// The callback for getting the identifier string
|
||||||
|
/// </summary>
|
||||||
|
public Func<SearchResult, string>? TypeIdentifierCallback { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The static identifier string to return when this evaluator is matched
|
||||||
|
/// </summary>
|
||||||
|
public string? StaticIdentifier { get; set; }
|
||||||
|
|
||||||
|
internal string? GetMessageType(SearchResult result)
|
||||||
|
{
|
||||||
|
if (StaticIdentifier != null)
|
||||||
|
return StaticIdentifier;
|
||||||
|
|
||||||
|
return TypeIdentifierCallback!(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool Satisfied(SearchResult result)
|
||||||
|
{
|
||||||
|
foreach(var field in Fields)
|
||||||
|
{
|
||||||
|
if (!result.Contains(field))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
internal class MessageEvalutorFieldReference
|
||||||
|
{
|
||||||
|
public bool SkipReading { get; set; }
|
||||||
|
public bool OverlappingField { get; set; }
|
||||||
|
public MessageFieldReference Field { get; set; }
|
||||||
|
public MessageTypeDefinition? ForceEvaluator { get; set; }
|
||||||
|
|
||||||
|
public MessageEvalutorFieldReference(MessageFieldReference field)
|
||||||
|
{
|
||||||
|
Field = field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to a message field
|
||||||
|
/// </summary>
|
||||||
|
public abstract class MessageFieldReference
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name for this search field
|
||||||
|
/// </summary>
|
||||||
|
public string SearchName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The depth at which to look for this field
|
||||||
|
/// </summary>
|
||||||
|
public int Depth { get; set; } = 1;
|
||||||
|
/// <summary>
|
||||||
|
/// Callback to check if the field value matches an expected constraint
|
||||||
|
/// </summary>
|
||||||
|
public Func<string?, bool>? Constraint { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value is one of the string values in the set
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithFilterConstraint(HashSet<string?> set)
|
||||||
|
{
|
||||||
|
Constraint = set.Contains;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value is equal to a string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithEqualConstraint(string compare)
|
||||||
|
{
|
||||||
|
Constraint = x => x != null && x.Equals(compare, StringComparison.Ordinal);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value is not equal to a string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithNotEqualConstraint(string compare)
|
||||||
|
{
|
||||||
|
Constraint = x => x == null || !x.Equals(compare, StringComparison.Ordinal);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value is not null
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithNotNullConstraint()
|
||||||
|
{
|
||||||
|
Constraint = x => x != null;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value starts with a certain string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithStartsWithConstraint(string start)
|
||||||
|
{
|
||||||
|
Constraint = x => x != null && x.StartsWith(start, StringComparison.Ordinal);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value starts with a certain string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithStartsWithConstraints(params string[] startValues)
|
||||||
|
{
|
||||||
|
Constraint = x =>
|
||||||
|
{
|
||||||
|
if (x == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
foreach (var item in startValues)
|
||||||
|
{
|
||||||
|
if (x!.StartsWith(item, StringComparison.Ordinal))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check whether the value starts with a certain string
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference WithCustomConstraint(Func<string?, bool> constraint)
|
||||||
|
{
|
||||||
|
Constraint = constraint;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference(string searchName)
|
||||||
|
{
|
||||||
|
SearchName = searchName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to a property message field
|
||||||
|
/// </summary>
|
||||||
|
public class PropertyFieldReference : MessageFieldReference
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The property name in the JSON
|
||||||
|
/// </summary>
|
||||||
|
public byte[] PropertyName { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the property value is array values
|
||||||
|
/// </summary>
|
||||||
|
public bool ArrayValues { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public PropertyFieldReference(string propertyName) : base(propertyName)
|
||||||
|
{
|
||||||
|
PropertyName = Encoding.UTF8.GetBytes(propertyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reference to an array message field
|
||||||
|
/// </summary>
|
||||||
|
public class ArrayFieldReference : MessageFieldReference
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The index in the array
|
||||||
|
/// </summary>
|
||||||
|
public int ArrayIndex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
|
||||||
|
{
|
||||||
|
Depth = depth;
|
||||||
|
ArrayIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The results of a search for fields in a JSON message
|
||||||
|
/// </summary>
|
||||||
|
public class SearchResult
|
||||||
|
{
|
||||||
|
private List<SearchResultItem> _items = new List<SearchResultItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the value of a field
|
||||||
|
/// </summary>
|
||||||
|
public string? FieldValue(string searchName)
|
||||||
|
{
|
||||||
|
foreach (var item in _items)
|
||||||
|
{
|
||||||
|
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
|
||||||
|
return item.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Exception($"No field value found for {searchName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of found search field values
|
||||||
|
/// </summary>
|
||||||
|
public int Count => _items.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear the search result
|
||||||
|
/// </summary>
|
||||||
|
public void Clear() => _items.Clear();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the value for a specific field was found
|
||||||
|
/// </summary>
|
||||||
|
public bool Contains(MessageFieldReference field)
|
||||||
|
{
|
||||||
|
foreach (var item in _items)
|
||||||
|
{
|
||||||
|
if (item.Field == field)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Write a value to the result
|
||||||
|
/// </summary>
|
||||||
|
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
|
||||||
|
{
|
||||||
|
Field = field,
|
||||||
|
Value = value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Search result value
|
||||||
|
/// </summary>
|
||||||
|
public struct SearchResultItem
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The field the values is for
|
||||||
|
/// </summary>
|
||||||
|
public MessageFieldReference Field { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// The value of the field
|
||||||
|
/// </summary>
|
||||||
|
public string? Value { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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); }
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,232 +1,246 @@
|
|||||||
|
using CryptoExchange.Net.Exceptions;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Collections.Generic;
|
using System.Text.Json.Serialization;
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
#endif
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
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>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
|
||||||
#else
|
|
||||||
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
|
||||||
#endif
|
|
||||||
{
|
{
|
||||||
private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
|
/// <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
|
||||||
/// <inheritdoc />
|
/// with [ArrayProperty(x)] where x is the index of the property in the array
|
||||||
|
/// </summary>
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
public class ArrayConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter<T> where T : new()
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteStartArray();
|
|
||||||
|
|
||||||
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
|
|
||||||
var last = -1;
|
|
||||||
foreach (var prop in ordered)
|
|
||||||
{
|
|
||||||
if (prop.ArrayProperty.Index == last)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
while (prop.ArrayProperty.Index != last + 1)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
last += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
last = prop.ArrayProperty.Index;
|
|
||||||
|
|
||||||
var objValue = prop.PropertyInfo.GetValue(value);
|
|
||||||
if (objValue == null)
|
|
||||||
{
|
|
||||||
writer.WriteNullValue();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonSerializerOptions? typeOptions = null;
|
|
||||||
if (prop.JsonConverter != null)
|
|
||||||
{
|
|
||||||
typeOptions = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
|
||||||
PropertyNameCaseInsensitive = false,
|
|
||||||
TypeInfoResolver = options.TypeInfoResolver,
|
|
||||||
};
|
|
||||||
typeOptions.Converters.Add(prop.JsonConverter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
|
|
||||||
{
|
|
||||||
if (prop.TargetType == typeof(string))
|
|
||||||
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
|
||||||
else if (prop.TargetType == typeof(bool))
|
|
||||||
writer.WriteBooleanValue((bool)objValue);
|
|
||||||
else
|
|
||||||
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
|
||||||
return default;
|
|
||||||
|
|
||||||
var result = new T();
|
|
||||||
return ParseObject(ref reader, result, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
|
||||||
#else
|
#else
|
||||||
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
public class ArrayConverter<T> : JsonConverter<T> where T : new()
|
||||||
#endif
|
#endif
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo;
|
||||||
throw new Exception("Not an array");
|
|
||||||
|
|
||||||
int index = 0;
|
/// <inheritdoc />
|
||||||
while (reader.Read())
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.EndArray)
|
if (value == null)
|
||||||
break;
|
|
||||||
|
|
||||||
var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
|
|
||||||
if (!indexAttributes.Any())
|
|
||||||
{
|
{
|
||||||
index++;
|
writer.WriteNullValue();
|
||||||
continue;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var attribute in indexAttributes)
|
if (_typePropertyInfo == null)
|
||||||
|
_typePropertyInfo = CacheTypeAttributes();
|
||||||
|
|
||||||
|
writer.WriteStartArray();
|
||||||
|
var last = -1;
|
||||||
|
foreach (var indexProps in _typePropertyInfo)
|
||||||
{
|
{
|
||||||
var targetType = attribute.TargetType;
|
foreach (var prop in indexProps.Value)
|
||||||
object? value = null;
|
|
||||||
if (attribute.JsonConverter != null)
|
|
||||||
{
|
{
|
||||||
if (attribute.JsonSerializerOptions == null)
|
if (prop.ArrayProperty.Index == last)
|
||||||
|
// Don't write the same index twice
|
||||||
|
continue;
|
||||||
|
|
||||||
|
while (prop.ArrayProperty.Index != last + 1)
|
||||||
{
|
{
|
||||||
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
writer.WriteNullValue();
|
||||||
|
last += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
last = prop.ArrayProperty.Index;
|
||||||
|
|
||||||
|
var objValue = prop.PropertyInfo.GetValue(value);
|
||||||
|
if (objValue == null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonSerializerOptions? typeOptions = null;
|
||||||
|
if (prop.JsonConverter != null)
|
||||||
|
{
|
||||||
|
typeOptions = new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
PropertyNameCaseInsensitive = false,
|
PropertyNameCaseInsensitive = false,
|
||||||
Converters = { attribute.JsonConverter },
|
|
||||||
TypeInfoResolver = options.TypeInfoResolver,
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
};
|
};
|
||||||
|
typeOptions.Converters.Add(prop.JsonConverter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop.JsonConverter == null && IsSimple(prop.PropertyInfo.PropertyType))
|
||||||
|
{
|
||||||
|
if (prop.TargetType == typeof(string))
|
||||||
|
writer.WriteStringValue(Convert.ToString(objValue, CultureInfo.InvariantCulture));
|
||||||
|
else if (prop.TargetType == typeof(bool))
|
||||||
|
writer.WriteBooleanValue((bool)objValue);
|
||||||
|
else
|
||||||
|
writer.WriteRawValue(Convert.ToString(objValue, CultureInfo.InvariantCulture)!);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
var result = new T();
|
||||||
|
return ParseObject(ref reader, result, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||||
|
#else
|
||||||
|
private static T ParseObject(ref Utf8JsonReader reader, T result, JsonSerializerOptions options)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
throw new CeDeserializationException("Not an array");
|
||||||
|
|
||||||
|
|
||||||
|
if (_typePropertyInfo == null)
|
||||||
|
_typePropertyInfo = CacheTypeAttributes();
|
||||||
|
|
||||||
|
int index = 0;
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attribute in indexAttributes)
|
||||||
|
{
|
||||||
|
var targetType = attribute.TargetType;
|
||||||
|
object? value = null;
|
||||||
|
if (attribute.JsonConverter != null)
|
||||||
|
{
|
||||||
|
if (attribute.JsonSerializerOptions == null)
|
||||||
|
{
|
||||||
|
attribute.JsonSerializerOptions = new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
|
PropertyNameCaseInsensitive = false,
|
||||||
|
Converters = { attribute.JsonConverter },
|
||||||
|
TypeInfoResolver = options.TypeInfoResolver,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var doc = JsonDocument.ParseValue(ref reader);
|
||||||
|
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
|
||||||
|
}
|
||||||
|
else if (attribute.DefaultDeserialization)
|
||||||
|
{
|
||||||
|
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
value = reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.Null => null,
|
||||||
|
JsonTokenType.False => false,
|
||||||
|
JsonTokenType.True => true,
|
||||||
|
JsonTokenType.String => reader.GetString(),
|
||||||
|
JsonTokenType.Number => reader.GetDecimal(),
|
||||||
|
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
||||||
|
_ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
var doc = JsonDocument.ParseValue(ref reader);
|
if (targetType.IsAssignableFrom(value?.GetType()))
|
||||||
value = doc.Deserialize(attribute.PropertyInfo.PropertyType, attribute.JsonSerializerOptions);
|
attribute.PropertyInfo.SetValue(result, value);
|
||||||
}
|
else
|
||||||
else if (attribute.DefaultDeserialization)
|
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
||||||
{
|
|
||||||
value = JsonDocument.ParseValue(ref reader).Deserialize(options.GetTypeInfo(attribute.PropertyInfo.PropertyType));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
value = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.Null => null,
|
|
||||||
JsonTokenType.False => false,
|
|
||||||
JsonTokenType.True => true,
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetDecimal(),
|
|
||||||
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
|
|
||||||
_ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetType.IsAssignableFrom(value?.GetType()))
|
index++;
|
||||||
attribute.PropertyInfo.SetValue(result, value);
|
|
||||||
else
|
|
||||||
attribute.PropertyInfo.SetValue(result, value == null ? null : Convert.ChangeType(value, targetType, CultureInfo.InvariantCulture));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
index++;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
private static bool IsSimple(Type type)
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsSimple(Type type)
|
|
||||||
{
|
|
||||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
|
||||||
{
|
{
|
||||||
// nullable type, check if the nested type is simple.
|
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
|
||||||
return IsSimple(type.GetGenericArguments()[0]);
|
{
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
return type.IsPrimitive
|
|
||||||
|| type.IsEnum
|
|
||||||
|| type == typeof(string)
|
|
||||||
|| type == typeof(decimal);
|
|
||||||
}
|
|
||||||
|
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||||
#else
|
#else
|
||||||
private static List<ArrayPropertyInfo> CacheTypeAttributes()
|
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes()
|
||||||
#endif
|
#endif
|
||||||
{
|
|
||||||
var attributes = new List<ArrayPropertyInfo>();
|
|
||||||
var properties = typeof(T).GetProperties();
|
|
||||||
foreach (var property in properties)
|
|
||||||
{
|
{
|
||||||
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
var result = new SortedDictionary<int, List<ArrayPropertyInfo>>();
|
||||||
if (att == null)
|
var properties = typeof(T).GetProperties();
|
||||||
continue;
|
foreach (var property in properties)
|
||||||
|
|
||||||
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
|
||||||
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
|
||||||
attributes.Add(new ArrayPropertyInfo
|
|
||||||
{
|
{
|
||||||
ArrayProperty = att,
|
var att = property.GetCustomAttribute<ArrayPropertyAttribute>();
|
||||||
PropertyInfo = property,
|
if (att == null)
|
||||||
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
continue;
|
||||||
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
|
||||||
TargetType = targetType
|
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
|
||||||
});
|
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
|
||||||
|
if (!result.TryGetValue(att.Index, out var indexList))
|
||||||
|
{
|
||||||
|
indexList = new List<ArrayPropertyInfo>();
|
||||||
|
result[att.Index] = indexList;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexList.Add(new ArrayPropertyInfo
|
||||||
|
{
|
||||||
|
ArrayProperty = att,
|
||||||
|
PropertyInfo = property,
|
||||||
|
DefaultDeserialization = property.GetCustomAttribute<CryptoExchange.Net.Attributes.JsonConversionAttribute>() != null,
|
||||||
|
JsonConverter = converterType == null ? null : (JsonConverter)Activator.CreateInstance(converterType)!,
|
||||||
|
TargetType = targetType
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return attributes;
|
private class ArrayPropertyInfo
|
||||||
}
|
{
|
||||||
|
public PropertyInfo PropertyInfo { get; set; } = null!;
|
||||||
private class ArrayPropertyInfo
|
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
||||||
{
|
public JsonConverter? JsonConverter { get; set; }
|
||||||
public PropertyInfo PropertyInfo { get; set; } = null!;
|
public bool DefaultDeserialization { get; set; }
|
||||||
public ArrayPropertyAttribute ArrayProperty { get; set; } = null!;
|
public Type TargetType { get; set; } = null!;
|
||||||
public JsonConverter? JsonConverter { get; set; }
|
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null;
|
||||||
public bool DefaultDeserialization { get; set; }
|
}
|
||||||
public Type TargetType { get; set; } = null!;
|
|
||||||
public JsonSerializerOptions? JsonSerializerOptions { get; set; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +1,46 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
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 />
|
/// <summary>
|
||||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
|
||||||
|
/// </summary>
|
||||||
|
public class BigDecimalConverter : JsonConverter<decimal>
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
/// <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
|
try
|
||||||
{
|
{
|
||||||
return decimal.Parse(reader.GetString()!, NumberStyles.Float, CultureInfo.InvariantCulture);
|
return reader.GetDecimal();
|
||||||
}
|
}
|
||||||
catch(OverflowException)
|
catch(FormatException)
|
||||||
{
|
{
|
||||||
// Value doesn't fit decimal, default to max value
|
// Format issue, assume value is too large
|
||||||
return decimal.MaxValue;
|
return decimal.MaxValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
return reader.GetDecimal();
|
writer.WriteNumberValue(value);
|
||||||
}
|
}
|
||||||
catch(FormatException)
|
|
||||||
{
|
|
||||||
// Format issue, assume value is too large
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteNumberValue(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,54 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.Serialization;
|
using System.Runtime.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bool converter
|
|
||||||
/// </summary>
|
|
||||||
public class BoolConverter : JsonConverterFactory
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override bool CanConvert(Type typeToConvert)
|
/// Bool converter
|
||||||
|
/// </summary>
|
||||||
|
public class BoolConverter : JsonConverterFactory
|
||||||
{
|
{
|
||||||
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
|
/// <inheritdoc />
|
||||||
}
|
public override bool CanConvert(Type typeToConvert)
|
||||||
|
{
|
||||||
|
return typeToConvert == typeof(bool) || typeToConvert == typeof(bool?);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
|
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable();
|
||||||
}
|
}
|
||||||
|
|
||||||
private class BoolConverterInner<T> : JsonConverter<T>
|
private class BoolConverterInnerNullable : JsonConverter<bool?>
|
||||||
{
|
{
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
=> (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
|
=> ReadBool(ref reader, typeToConvert, options);
|
||||||
|
|
||||||
public static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is bool boolVal)
|
||||||
|
writer.WriteBooleanValue(boolVal);
|
||||||
|
else
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BoolConverterInner : JsonConverter<bool>
|
||||||
|
{
|
||||||
|
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> ReadBool(ref reader, typeToConvert, options) ?? false;
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteBooleanValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.True)
|
if (reader.TokenType == JsonTokenType.True)
|
||||||
return true;
|
return true;
|
||||||
@@ -47,7 +67,7 @@ public class BoolConverter : JsonConverterFactory
|
|||||||
if (string.IsNullOrEmpty(value))
|
if (string.IsNullOrEmpty(value))
|
||||||
{
|
{
|
||||||
if (typeToConvert == typeof(bool))
|
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");
|
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +91,5 @@ public class BoolConverter : JsonConverterFactory
|
|||||||
throw new SerializationException($"Can't convert bool value {value}");
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
using System;
|
using System;
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
#endif
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converter for comma separated enum values
|
/// Converter for comma separated enum values
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
|
public class CommaSplitEnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T> : JsonConverter<T[]> where T : struct, Enum
|
||||||
#else
|
#else
|
||||||
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
|
public class CommaSplitEnumConverter<T> : JsonConverter<T[]> where T : struct, Enum
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
{
|
||||||
var str = reader.GetString();
|
/// <inheritdoc />
|
||||||
if (string.IsNullOrEmpty(str))
|
public override T[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
return [];
|
{
|
||||||
|
var str = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return [];
|
||||||
|
|
||||||
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
return str!.Split(',').Select(x => (T)EnumConverter.ParseString<T>(x)!).ToArray() ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T[] value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for comma separated string values
|
||||||
|
/// </summary>
|
||||||
|
public class CommaSplitStringConverter : JsonConverter<string[]>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string[]? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var str = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(str))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return str!.Split(',').ToArray() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, string[] value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStringValue(string.Join(",", value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,68 +1,98 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
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);
|
/// <summary>
|
||||||
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
/// Date time converter
|
||||||
private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
|
/// </summary>
|
||||||
private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
|
public class DateTimeConverter : JsonConverterFactory
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool CanConvert(Type typeToConvert)
|
|
||||||
{
|
{
|
||||||
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
}
|
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
|
||||||
|
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m;
|
||||||
|
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
public override bool CanConvert(Type typeToConvert)
|
||||||
{
|
{
|
||||||
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
|
return typeToConvert == typeof(DateTime) || typeToConvert == typeof(DateTime?);
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DateTimeConverterInner<T> : JsonConverter<T>
|
/// <inheritdoc />
|
||||||
{
|
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
{
|
||||||
=> (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
|
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NullableDateTimeConverterInner : JsonConverter<DateTime?>
|
||||||
|
{
|
||||||
|
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> ReadDateTime(ref reader, typeToConvert, options);
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Value == default)
|
||||||
|
writer.WriteStringValue(default(DateTime));
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue((long)Math.Round((value.Value - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DateTimeConverterInner : JsonConverter<DateTime>
|
||||||
|
{
|
||||||
|
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
=> ReadDateTime(ref reader, typeToConvert, options) ?? default;
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var dtValue = value;
|
||||||
|
if (dtValue == default)
|
||||||
|
writer.WriteStringValue(default(DateTime));
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
private static DateTime? ReadDateTime(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
{
|
{
|
||||||
if (typeToConvert == typeof(DateTime))
|
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");
|
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name);
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader.TokenType is JsonTokenType.Number)
|
if (reader.TokenType is JsonTokenType.Number)
|
||||||
{
|
{
|
||||||
var longValue = reader.GetDouble();
|
var decValue = reader.GetDecimal();
|
||||||
if (longValue == 0 || longValue < 0)
|
if (decValue == 0 || decValue < 0)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return ParseFromDouble(longValue);
|
return ParseFromDecimal(decValue);
|
||||||
}
|
}
|
||||||
else if (reader.TokenType is JsonTokenType.String)
|
else if (reader.TokenType is JsonTokenType.String)
|
||||||
{
|
{
|
||||||
var stringValue = reader.GetString();
|
var stringValue = reader.GetString();
|
||||||
if (string.IsNullOrWhiteSpace(stringValue)
|
if (string.IsNullOrWhiteSpace(stringValue)
|
||||||
|| stringValue == "-1"
|
|| stringValue!.Equals("-1", StringComparison.Ordinal)
|
||||||
|| stringValue == "0001-01-01T00:00:00Z"
|
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase)
|
||||||
|| double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
|
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0)
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseFromString(stringValue!);
|
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -70,172 +100,174 @@ public class DateTimeConverter : JsonConverterFactory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
/// <summary>
|
||||||
|
/// Parse a double value to datetime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ParseFromDouble(double value)
|
||||||
|
=> ParseFromDecimal((decimal)value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a decimal value to datetime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ParseFromDecimal(decimal value)
|
||||||
{
|
{
|
||||||
if (value == null)
|
if (value < 19999999999)
|
||||||
{
|
return ConvertFromSeconds(value);
|
||||||
writer.WriteNullValue();
|
if (value < 19999999999999)
|
||||||
}
|
return ConvertFromMilliseconds(value);
|
||||||
else
|
if (value < 19999999999999999)
|
||||||
{
|
return ConvertFromMicroseconds(value);
|
||||||
var dtValue = (DateTime)(object)value;
|
|
||||||
if (dtValue == default)
|
return ConvertFromNanoseconds(value);
|
||||||
writer.WriteStringValue(default(DateTime));
|
|
||||||
else
|
|
||||||
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a string value to datetime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ParseFromString(string stringValue, string? resolverName)
|
||||||
|
{
|
||||||
|
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// 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))
|
||||||
|
{
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue))
|
||||||
|
{
|
||||||
|
// Parse 1637745563.000 format
|
||||||
|
if (decimalValue <= 0)
|
||||||
|
return default;
|
||||||
|
if (decimalValue < 19999999999)
|
||||||
|
return ConvertFromSeconds(decimalValue);
|
||||||
|
if (decimalValue < 19999999999999)
|
||||||
|
return ConvertFromMilliseconds(decimalValue);
|
||||||
|
if (decimalValue < 19999999999999999)
|
||||||
|
return ConvertFromMicroseconds(decimalValue);
|
||||||
|
|
||||||
|
return ConvertFromNanoseconds(decimalValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName);
|
||||||
|
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>
|
||||||
|
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromSeconds(double seconds) => ConvertFromSeconds((decimal)seconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromSeconds(long seconds) => ConvertFromSeconds((decimal)seconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(double milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMilliseconds(long milliseconds) => ConvertFromMilliseconds((decimal)milliseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a microseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromMicroseconds(long microseconds) => ConvertFromMicroseconds((decimal)microseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(decimal nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(double nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
|
||||||
|
/// </summary>
|
||||||
|
public static DateTime ConvertFromNanoseconds(long nanoseconds) => ConvertFromNanoseconds((decimal)nanoseconds);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a DateTime value to seconds since epoch (01-01-1970) value
|
||||||
|
/// </summary>
|
||||||
|
[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>
|
||||||
|
[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>
|
||||||
|
[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>
|
||||||
|
[return: NotNullIfNotNull("time")]
|
||||||
|
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Decimal converter
|
|
||||||
/// </summary>
|
|
||||||
public class DecimalConverter : JsonConverter<decimal?>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// Decimal converter
|
||||||
|
/// </summary>
|
||||||
|
public class DecimalConverter : JsonConverter<decimal?>
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
/// <inheritdoc />
|
||||||
return null;
|
public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
{
|
||||||
var value = reader.GetString();
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
return ExchangeHelpers.ParseDecimal(value);
|
return null;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
{
|
||||||
|
var value = reader.GetString();
|
||||||
|
return ExchangeHelpers.ParseDecimal(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return reader.GetDecimal();
|
||||||
|
}
|
||||||
|
catch(FormatException)
|
||||||
|
{
|
||||||
|
// Format issue, assume value is too large
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
return reader.GetDecimal();
|
if (value == null)
|
||||||
|
writer.WriteNullValue();
|
||||||
|
else
|
||||||
|
writer.WriteNumberValue(value.Value);
|
||||||
}
|
}
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for serializing decimal values as string
|
|
||||||
/// </summary>
|
|
||||||
public class DecimalStringWriterConverter : JsonConverter<decimal>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// Converter for serializing decimal values as string
|
||||||
|
/// </summary>
|
||||||
|
public class DecimalStringWriterConverter : JsonConverter<decimal>
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
/// <inheritdoc />
|
||||||
}
|
public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options)
|
||||||
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
|
=> writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture) ?? null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,288 +1,365 @@
|
|||||||
using CryptoExchange.Net.Attributes;
|
using CryptoExchange.Net.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
#endif
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Static EnumConverter methods
|
|
||||||
/// </summary>
|
|
||||||
public static class EnumConverter
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the enum value from a string
|
/// Static EnumConverter methods
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">String value</param>
|
public static class EnumConverter
|
||||||
/// <returns></returns>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
|
|
||||||
#else
|
|
||||||
public static T? ParseString<T>(string value) where T : struct, Enum
|
|
||||||
#endif
|
|
||||||
=> EnumConverter<T>.ParseString(value);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
|
|
||||||
#else
|
|
||||||
public static string GetString<T>(T enumValue) where T : struct, Enum
|
|
||||||
#endif
|
|
||||||
=> EnumConverter<T>.GetString(enumValue);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
|
|
||||||
#else
|
|
||||||
public static string? GetString<T>(T? enumValue) where T : struct, Enum
|
|
||||||
#endif
|
|
||||||
=> EnumConverter<T>.GetString(enumValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
|
||||||
/// </summary>
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
|
|
||||||
#else
|
|
||||||
public class EnumConverter<T>
|
|
||||||
#endif
|
|
||||||
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
|
||||||
{
|
|
||||||
private static List<KeyValuePair<T, string>>? _mapping;
|
|
||||||
private NullableEnumConverter? _nullableEnumConverter;
|
|
||||||
|
|
||||||
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
|
||||||
|
|
||||||
internal class NullableEnumConverter : JsonConverter<T?>
|
|
||||||
{
|
{
|
||||||
private readonly EnumConverter<T> _enumConverter;
|
/// <summary>
|
||||||
|
/// Get the enum value from a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public static T? ParseString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(string value) where T : struct, Enum
|
||||||
|
#else
|
||||||
|
public static T? ParseString<T>(string value) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.ParseString(value);
|
||||||
|
|
||||||
public NullableEnumConverter(EnumConverter<T> enumConverter)
|
/// <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
|
||||||
_enumConverter = enumConverter;
|
/// </summary>
|
||||||
}
|
/// <param name="enumValue"></param>
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// <returns></returns>
|
||||||
{
|
#if NET5_0_OR_GREATER
|
||||||
return EnumConverter<T>.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
public static string GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T enumValue) where T : struct, Enum
|
||||||
}
|
#else
|
||||||
|
public static string GetString<T>(T enumValue) where T : struct, Enum
|
||||||
|
#endif
|
||||||
|
=> EnumConverter<T>.GetString(enumValue);
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
/// <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
|
||||||
if (value == null)
|
/// </summary>
|
||||||
{
|
/// <param name="enumValue"></param>
|
||||||
writer.WriteNullValue();
|
/// <returns></returns>
|
||||||
}
|
[return: NotNullIfNotNull("enumValue")]
|
||||||
else
|
#if NET5_0_OR_GREATER
|
||||||
{
|
public static string? GetString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>(T? enumValue) where T : struct, Enum
|
||||||
_enumConverter.Write(writer, value.Value, options);
|
#else
|
||||||
}
|
public static string? GetString<T>(T? enumValue) where T : struct, Enum
|
||||||
}
|
#endif
|
||||||
|
=> EnumConverter<T>.GetString(enumValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// Converter for enum values. Enums entries should be noted with a MapAttribute to map the enum value to a string value
|
||||||
|
/// </summary>
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
public class EnumConverter<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | DynamicallyAccessedMemberTypes.PublicFields)] T>
|
||||||
|
#else
|
||||||
|
public class EnumConverter<T>
|
||||||
|
#endif
|
||||||
|
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum
|
||||||
{
|
{
|
||||||
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
|
class EnumMapping
|
||||||
if (t == null)
|
|
||||||
{
|
{
|
||||||
if (warn)
|
public T Value { get; set; }
|
||||||
|
public string StringValue { get; set; }
|
||||||
|
|
||||||
|
public EnumMapping(T value, string stringValue)
|
||||||
{
|
{
|
||||||
if (isEmptyString)
|
Value = value;
|
||||||
|
StringValue = stringValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
private static FrozenSet<EnumMapping>? _mappingToEnum = null;
|
||||||
|
private static FrozenDictionary<T, string>? _mappingToString = null;
|
||||||
|
#else
|
||||||
|
private static List<EnumMapping>? _mappingToEnum = null;
|
||||||
|
private static Dictionary<T, string>? _mappingToString = null;
|
||||||
|
#endif
|
||||||
|
private NullableEnumConverter? _nullableEnumConverter = null;
|
||||||
|
|
||||||
|
private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
|
||||||
|
|
||||||
|
internal class NullableEnumConverter : JsonConverter<T?>
|
||||||
|
{
|
||||||
|
private readonly EnumConverter<T> _enumConverter;
|
||||||
|
|
||||||
|
public NullableEnumConverter(EnumConverter<T> enumConverter)
|
||||||
|
{
|
||||||
|
_enumConverter = enumConverter;
|
||||||
|
}
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
{
|
{
|
||||||
// We received an empty string and have no mapping for it, and the property isn't nullable
|
writer.WriteNullValue();
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received empty string as enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null enum value, but property type is not a nullable enum. EnumType: {typeof(T).Name}. If you think {typeof(T).Name} should be nullable please open an issue on the Github repo");
|
_enumConverter.Write(writer, value.Value, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new T(); // return default value
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
return t.Value;
|
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString);
|
||||||
}
|
if (t == null)
|
||||||
}
|
|
||||||
|
|
||||||
private static T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
|
|
||||||
{
|
|
||||||
isEmptyString = false;
|
|
||||||
warn = false;
|
|
||||||
var enumType = typeof(T);
|
|
||||||
if (_mapping == null)
|
|
||||||
_mapping = AddMapping();
|
|
||||||
|
|
||||||
var stringValue = reader.TokenType switch
|
|
||||||
{
|
|
||||||
JsonTokenType.String => reader.GetString(),
|
|
||||||
JsonTokenType.Number => reader.GetInt32().ToString(),
|
|
||||||
JsonTokenType.True => reader.GetBoolean().ToString(),
|
|
||||||
JsonTokenType.False => reader.GetBoolean().ToString(),
|
|
||||||
JsonTokenType.Null => null,
|
|
||||||
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(stringValue))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!GetValue(enumType, stringValue!, out var result))
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(stringValue))
|
|
||||||
{
|
{
|
||||||
isEmptyString = true;
|
if (isEmptyString && !_unknownValuesWarned.Contains(null))
|
||||||
|
{
|
||||||
|
// We received an empty string and have no mapping for it, and the property isn't nullable
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning($"Received null or empty enum value, but property type is not a nullable enum. EnumType: {typeof(T).FullName}. If you think {typeof(T).FullName} should be nullable please open an issue on the Github repo");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new T(); // return default value
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// We received an enum value but weren't able to parse it.
|
return t.Value;
|
||||||
if (!_unknownValuesWarned.Contains(stringValue))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString)
|
||||||
|
{
|
||||||
|
isEmptyString = false;
|
||||||
|
var enumType = typeof(T);
|
||||||
|
if (_mappingToEnum == null)
|
||||||
|
CreateMapping();
|
||||||
|
|
||||||
|
var stringValue = reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.String => reader.GetString(),
|
||||||
|
JsonTokenType.Number => reader.GetInt32().ToString(),
|
||||||
|
JsonTokenType.True => reader.GetBoolean().ToString(),
|
||||||
|
JsonTokenType.False => reader.GetBoolean().ToString(),
|
||||||
|
JsonTokenType.Null => null,
|
||||||
|
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stringValue is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!GetValue(enumType, stringValue, out var result))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
{
|
{
|
||||||
warn = true;
|
isEmptyString = true;
|
||||||
_unknownValuesWarned.Add(stringValue!);
|
}
|
||||||
Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Cannot map enum value. EnumType: {enumType.Name}, Value: {stringValue}, Known values: {string.Join(", ", _mapping.Select(m => m.Value))}. If you think {stringValue} should added please open an issue on the Github repo");
|
else
|
||||||
|
{
|
||||||
|
// We received an enum value but weren't able to parse it.
|
||||||
|
if (!_unknownValuesWarned.Contains(stringValue))
|
||||||
|
{
|
||||||
|
_unknownValuesWarned.Add(stringValue!);
|
||||||
|
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should added please open an issue on the Github repo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var stringValue = GetString(value);
|
||||||
|
writer.WriteStringValue(stringValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetValue(Type objectType, string value, out T? result)
|
||||||
|
{
|
||||||
|
if (_mappingToEnum != null)
|
||||||
|
{
|
||||||
|
EnumMapping? mapping = null;
|
||||||
|
// Try match on full equals
|
||||||
|
foreach (var item in _mappingToEnum)
|
||||||
|
{
|
||||||
|
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
mapping = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, try matching ignoring case
|
||||||
|
if (mapping == null)
|
||||||
|
{
|
||||||
|
foreach (var item in _mappingToEnum)
|
||||||
|
{
|
||||||
|
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mapping = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping != null)
|
||||||
|
{
|
||||||
|
result = mapping.Value;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
var stringValue = GetString(value);
|
|
||||||
writer.WriteStringValue(stringValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool GetValue(Type objectType, string value, out T? result)
|
|
||||||
{
|
|
||||||
if (_mapping != null)
|
|
||||||
{
|
|
||||||
// Check for exact match first, then if not found fallback to a case insensitive match
|
|
||||||
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
|
||||||
if (mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
{
|
{
|
||||||
result = mapping.Key;
|
var intValue = int.Parse(value);
|
||||||
|
result = (T)Enum.ToObject(objectType, intValue);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (objectType.IsDefined(typeof(FlagsAttribute)))
|
if (_unknownValuesWarned.Contains(value))
|
||||||
{
|
|
||||||
var intValue = int.Parse(value);
|
|
||||||
result = (T)Enum.ToObject(objectType, intValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_unknownValuesWarned.Contains(value))
|
|
||||||
{
|
|
||||||
// Check if it is an known unknown value
|
|
||||||
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// If no explicit mapping is found try to parse string
|
|
||||||
result = (T)Enum.Parse(objectType, value, true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<KeyValuePair<T, string>> AddMapping()
|
|
||||||
{
|
|
||||||
var mapping = new List<KeyValuePair<T, string>>();
|
|
||||||
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
|
||||||
var enumMembers = enumType.GetFields();
|
|
||||||
foreach (var member in enumMembers)
|
|
||||||
{
|
|
||||||
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
|
||||||
foreach (MapAttribute attribute in maps)
|
|
||||||
{
|
{
|
||||||
foreach (var value in attribute.Values)
|
// Check if it is an known unknown value
|
||||||
mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
|
// Done here to prevent lookup overhead for normal conversions, but prevent expensive exception throwing
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
// An empty/null value will always fail when parsing, so just return here
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// If no explicit mapping is found try to parse string
|
||||||
|
result = (T)Enum.Parse(objectType, value, true);
|
||||||
|
if (!Enum.IsDefined(objectType, result))
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_mapping = mapping;
|
private static void CreateMapping()
|
||||||
return mapping;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="enumValue"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNullIfNotNull("enumValue")]
|
|
||||||
public static string? GetString(T? enumValue)
|
|
||||||
{
|
|
||||||
if (_mapping == null)
|
|
||||||
_mapping = AddMapping();
|
|
||||||
|
|
||||||
return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Get the enum value from a string
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">String value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static T? ParseString(string value)
|
|
||||||
{
|
|
||||||
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
|
||||||
if (_mapping == null)
|
|
||||||
_mapping = AddMapping();
|
|
||||||
|
|
||||||
var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
|
|
||||||
if (mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
|
|
||||||
|
|
||||||
if (!mapping.Equals(default(KeyValuePair<T, string>)))
|
|
||||||
return mapping.Key;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
// If no explicit mapping is found try to parse string
|
var mappingToEnum = new List<EnumMapping>();
|
||||||
return (T)Enum.Parse(type, value, true);
|
var mappingToString = new Dictionary<T, string>();
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
public JsonConverter CreateNullableConverter()
|
var enumMembers = enumType.GetFields();
|
||||||
{
|
foreach (var member in enumMembers)
|
||||||
_nullableEnumConverter ??= new NullableEnumConverter(this);
|
{
|
||||||
return _nullableEnumConverter;
|
var maps = member.GetCustomAttributes(typeof(MapAttribute), false);
|
||||||
|
foreach (MapAttribute attribute in maps)
|
||||||
|
{
|
||||||
|
foreach (var value in attribute.Values)
|
||||||
|
{
|
||||||
|
var enumVal = (T)Enum.Parse(enumType, member.Name);
|
||||||
|
mappingToEnum.Add(new EnumMapping(enumVal, value));
|
||||||
|
if (!mappingToString.ContainsKey(enumVal))
|
||||||
|
mappingToString.Add(enumVal, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
_mappingToEnum = mappingToEnum.ToFrozenSet();
|
||||||
|
_mappingToString = mappingToString.ToFrozenDictionary();
|
||||||
|
#else
|
||||||
|
_mappingToEnum = mappingToEnum;
|
||||||
|
_mappingToString = mappingToString;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the string value for an enum value using the MapAttribute mapping. When multiple values are mapped for a enum entry the first value will be returned
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="enumValue"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[return: NotNullIfNotNull("enumValue")]
|
||||||
|
public static string? GetString(T? enumValue)
|
||||||
|
{
|
||||||
|
if (_mappingToString == null)
|
||||||
|
CreateMapping();
|
||||||
|
|
||||||
|
return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : enumValue.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the enum value from a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">String value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static T? ParseString(string value)
|
||||||
|
{
|
||||||
|
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
|
||||||
|
if (_mappingToEnum == null)
|
||||||
|
CreateMapping();
|
||||||
|
|
||||||
|
EnumMapping? mapping = null;
|
||||||
|
// Try match on full equals
|
||||||
|
foreach(var item in _mappingToEnum!)
|
||||||
|
{
|
||||||
|
if (item.StringValue.Equals(value, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
mapping = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, try matching ignoring case
|
||||||
|
if (mapping == null)
|
||||||
|
{
|
||||||
|
foreach (var item in _mappingToEnum)
|
||||||
|
{
|
||||||
|
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
mapping = item;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapping != null)
|
||||||
|
return mapping.Value;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// If no explicit mapping is found try to parse string
|
||||||
|
return (T)Enum.Parse(type, value, true);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public JsonConverter CreateNullableConverter()
|
||||||
|
{
|
||||||
|
_nullableEnumConverter ??= new NullableEnumConverter(this);
|
||||||
|
return _nullableEnumConverter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for serializing enum values as int
|
|
||||||
/// </summary>
|
|
||||||
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// Converter for serializing enum values as int
|
||||||
|
/// </summary>
|
||||||
|
public class EnumIntWriterConverter<T> : JsonConverter<T> where T: struct, Enum
|
||||||
{
|
{
|
||||||
throw new NotImplementedException();
|
/// <inheritdoc />
|
||||||
}
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
=> writer.WriteNumberValue((int)(object)value);
|
=> writer.WriteNumberValue((int)(object)value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
internal interface INullableConverterFactory
|
|
||||||
{
|
{
|
||||||
JsonConverter CreateNullableConverter();
|
internal interface INullableConverterFactory
|
||||||
|
{
|
||||||
|
JsonConverter CreateNullableConverter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Bool converter
|
||||||
|
/// </summary>
|
||||||
|
public class IntBoolConverter : JsonConverter<bool>
|
||||||
|
{
|
||||||
|
private readonly int _trueValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="trueValue">The int value representing the true value</param>
|
||||||
|
public IntBoolConverter(int trueValue)
|
||||||
|
{
|
||||||
|
_trueValue = trueValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.Number)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return reader.GetDecimal() == _trueValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteNumberValue(_trueValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,39 +1,40 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Int converter
|
|
||||||
/// </summary>
|
|
||||||
public class IntConverter : JsonConverter<int?>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// Int converter
|
||||||
|
/// </summary>
|
||||||
|
public class IntConverter : JsonConverter<int?>
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
/// <inheritdoc />
|
||||||
return null;
|
public override int? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
{
|
||||||
var value = reader.GetString();
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader.GetInt32();
|
/// <inheritdoc />
|
||||||
}
|
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
/// <inheritdoc />
|
if (value == null)
|
||||||
public override void Write(Utf8JsonWriter writer, int? value, JsonSerializerOptions options)
|
writer.WriteNullValue();
|
||||||
{
|
else
|
||||||
if (value == null)
|
writer.WriteNumberValue(value.Value);
|
||||||
writer.WriteNullValue();
|
}
|
||||||
else
|
|
||||||
writer.WriteNumberValue(value.Value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,40 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Int converter
|
|
||||||
/// </summary>
|
|
||||||
public class LongConverter : JsonConverter<long?>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// Int converter
|
||||||
|
/// </summary>
|
||||||
|
public class LongConverter : JsonConverter<long?>
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
/// <inheritdoc />
|
||||||
return null;
|
public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.String)
|
|
||||||
{
|
{
|
||||||
var value = reader.GetString();
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
return reader.GetInt64();
|
/// <inheritdoc />
|
||||||
}
|
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
/// <inheritdoc />
|
if (value == null)
|
||||||
public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options)
|
writer.WriteNullValue();
|
||||||
{
|
else
|
||||||
if (value == null)
|
writer.WriteNumberValue(value.Value);
|
||||||
writer.WriteNullValue();
|
}
|
||||||
else
|
|
||||||
writer.WriteNumberValue(value.Value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+131
@@ -0,0 +1,131 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Errors;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON REST message handler
|
||||||
|
/// </summary>
|
||||||
|
public abstract class JsonRestMessageHandler : IRestMessageHandler
|
||||||
|
{
|
||||||
|
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
|
||||||
|
private const int _errorResponseSnippetLimit = 128;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Empty rate limit error
|
||||||
|
/// </summary>
|
||||||
|
protected static readonly ServerRateLimitError _emptyRateLimitError = new ServerRateLimitError();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual bool RequiresSeekableStream => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The serializer options to use
|
||||||
|
/// </summary>
|
||||||
|
public abstract JsonSerializerOptions Options { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
|
||||||
|
int httpStatusCode,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream)
|
||||||
|
{
|
||||||
|
// Handle retry after header
|
||||||
|
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
|
||||||
|
if (retryAfterHeader.Value?.Any() != true)
|
||||||
|
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||||
|
|
||||||
|
var value = retryAfterHeader.Value.First();
|
||||||
|
if (int.TryParse(value, out var seconds))
|
||||||
|
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) });
|
||||||
|
|
||||||
|
if (DateTime.TryParse(value, out var datetime))
|
||||||
|
return new ValueTask<ServerRateLimitError>(new ServerRateLimitError() { RetryAfter = datetime });
|
||||||
|
|
||||||
|
return new ValueTask<ServerRateLimitError>(_emptyRateLimitError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public abstract ValueTask<Error> ParseErrorResponse(
|
||||||
|
int httpStatusCode,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual ValueTask<Error?> CheckForErrorResponse(
|
||||||
|
RequestDefinition request,
|
||||||
|
HttpResponseHeaders responseHeaders,
|
||||||
|
Stream responseStream) => new ValueTask<Error?>((Error?)null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read the response into a JsonDocument object
|
||||||
|
/// </summary>
|
||||||
|
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var document = await JsonDocument.ParseAsync(stream).ConfigureAwait(false);
|
||||||
|
return (null, document);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var errorMsg = "Deserialization failed, invalid JSON";
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
var dataSnippet = new char[_errorResponseSnippetLimit];
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
var written = new StreamReader(stream).ReadBlock(dataSnippet, 0, _errorResponseSnippetLimit);
|
||||||
|
var data = new string(dataSnippet, 0, written);
|
||||||
|
errorMsg += $": {data}";
|
||||||
|
if (data.Length == _errorResponseSnippetLimit)
|
||||||
|
errorMsg += " (truncated)";
|
||||||
|
}
|
||||||
|
|
||||||
|
var error = new DeserializeError(errorMsg, ex);
|
||||||
|
return (error, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
|
public async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
|
||||||
|
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
return (result, null);
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
||||||
|
return (default, new DeserializeError(info, ex));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (default, new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual Error? CheckDeserializedResponse<T>(HttpResponseHeaders responseHeaders, T result) => null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+362
@@ -0,0 +1,362 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON WebSocket message handler, sequentially read the JSON and looks for specific predefined fields to identify the message
|
||||||
|
/// </summary>
|
||||||
|
public abstract class JsonSocketMessageHandler : ISocketMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The serializer options to use
|
||||||
|
/// </summary>
|
||||||
|
public abstract JsonSerializerOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Message evaluators
|
||||||
|
/// </summary>
|
||||||
|
protected abstract MessageTypeDefinition[] TypeEvaluators { get; }
|
||||||
|
|
||||||
|
private readonly SearchResult _searchResult = new();
|
||||||
|
|
||||||
|
private bool _hasArraySearches;
|
||||||
|
private bool _initialized;
|
||||||
|
private int _maxSearchDepth;
|
||||||
|
private MessageTypeDefinition? _topEvaluator;
|
||||||
|
private List<MessageEvalutorFieldReference>? _searchFields;
|
||||||
|
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
|
||||||
|
private Dictionary<Type, Func<object, string?>>? _mapping;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a mapping of a specific object of a type to a specific topic
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">Type to get topic for</typeparam>
|
||||||
|
/// <param name="mapping">The topic retrieve delegate</param>
|
||||||
|
protected void AddTopicMapping<T>(Func<T, string?> mapping)
|
||||||
|
{
|
||||||
|
_mapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||||
|
_mapping.Add(typeof(T), x => mapping((T)x));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeConverter()
|
||||||
|
{
|
||||||
|
if (_initialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_maxSearchDepth = int.MinValue;
|
||||||
|
_searchFields = new List<MessageEvalutorFieldReference>();
|
||||||
|
foreach (var evaluator in TypeEvaluators)
|
||||||
|
{
|
||||||
|
_topEvaluator ??= evaluator;
|
||||||
|
foreach (var field in evaluator.Fields)
|
||||||
|
{
|
||||||
|
var overlapping = _searchFields.Where(otherField =>
|
||||||
|
{
|
||||||
|
if (field is PropertyFieldReference propRef
|
||||||
|
&& otherField.Field is PropertyFieldReference otherPropRef)
|
||||||
|
{
|
||||||
|
return field.Depth == otherPropRef.Depth && propRef.PropertyName.SequenceEqual(otherPropRef.PropertyName);
|
||||||
|
}
|
||||||
|
else if (field is ArrayFieldReference arrayRef
|
||||||
|
&& otherField.Field is ArrayFieldReference otherArrayPropRef)
|
||||||
|
{
|
||||||
|
return field.Depth == otherArrayPropRef.Depth && arrayRef.ArrayIndex == otherArrayPropRef.ArrayIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
if (overlapping.Any())
|
||||||
|
{
|
||||||
|
foreach (var overlap in overlapping)
|
||||||
|
overlap.OverlappingField = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<MessageEvalutorFieldReference>? existingSameSearchField = new();
|
||||||
|
if (field is ArrayFieldReference arrayField)
|
||||||
|
{
|
||||||
|
_hasArraySearches = true;
|
||||||
|
existingSameSearchField = _searchFields.Where(x =>
|
||||||
|
x.Field is ArrayFieldReference arrayFieldRef
|
||||||
|
&& arrayFieldRef.ArrayIndex == arrayField.ArrayIndex
|
||||||
|
&& arrayFieldRef.Depth == arrayField.Depth
|
||||||
|
&& arrayFieldRef.Constraint == null && arrayField.Constraint == null).ToList();
|
||||||
|
}
|
||||||
|
else if (field is PropertyFieldReference propField)
|
||||||
|
{
|
||||||
|
existingSameSearchField = _searchFields.Where(x =>
|
||||||
|
x.Field is PropertyFieldReference propFieldRef
|
||||||
|
&& propFieldRef.PropertyName.SequenceEqual(propField.PropertyName)
|
||||||
|
&& propFieldRef.Depth == propField.Depth
|
||||||
|
&& propFieldRef.Constraint == null && propFieldRef.Constraint == null).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach(var sameSearchField in existingSameSearchField)
|
||||||
|
{
|
||||||
|
if (sameSearchField.SkipReading == true
|
||||||
|
&& (evaluator.TypeIdentifierCallback != null || field.Constraint != null))
|
||||||
|
{
|
||||||
|
sameSearchField.SkipReading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (evaluator.ForceIfFound)
|
||||||
|
{
|
||||||
|
if (evaluator.Fields.Length > 1 || sameSearchField.ForceEvaluator != null)
|
||||||
|
throw new Exception("Invalid config");
|
||||||
|
|
||||||
|
//sameSearchField.ForceEvaluator = evaluator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_searchFields.Add(new MessageEvalutorFieldReference(field)
|
||||||
|
{
|
||||||
|
SkipReading = evaluator.TypeIdentifierCallback == null && field.Constraint == null,
|
||||||
|
ForceEvaluator = !existingSameSearchField.Any() ? evaluator.ForceIfFound ? evaluator : null : null,
|
||||||
|
OverlappingField = overlapping.Any()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (field.Depth > _maxSearchDepth)
|
||||||
|
_maxSearchDepth = field.Depth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual string? GetTopicFilter(object deserializedObject)
|
||||||
|
{
|
||||||
|
if (_mapping == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Cache the found type for future
|
||||||
|
var currentType = deserializedObject.GetType();
|
||||||
|
if (_baseTypeMapping != null)
|
||||||
|
{
|
||||||
|
if (_baseTypeMapping.TryGetValue(currentType, out var typeMapping))
|
||||||
|
return typeMapping(deserializedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
var mappedBase = false;
|
||||||
|
while (currentType != null)
|
||||||
|
{
|
||||||
|
if (_mapping.TryGetValue(currentType, out var mapping))
|
||||||
|
{
|
||||||
|
if (mappedBase)
|
||||||
|
{
|
||||||
|
_baseTypeMapping ??= new Dictionary<Type, Func<object, string?>>();
|
||||||
|
_baseTypeMapping.Add(deserializedObject.GetType(), mapping);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping(deserializedObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedBase = true;
|
||||||
|
currentType = currentType.BaseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return type identifier for non-json messages
|
||||||
|
/// </summary>
|
||||||
|
protected virtual string? GetTypeIdentifierNonJson(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||||
|
{
|
||||||
|
InitializeConverter();
|
||||||
|
|
||||||
|
int? arrayIndex = null;
|
||||||
|
|
||||||
|
_searchResult.Clear();
|
||||||
|
if (data[0] != 0x5B && data[0] != 0x7B)
|
||||||
|
{
|
||||||
|
// Message doesn't start with `{` or `[`, not valid for processing as json
|
||||||
|
return GetTypeIdentifierNonJson(data, webSocketMessageType);
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader = new Utf8JsonReader(data);
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if ((reader.TokenType == JsonTokenType.StartArray
|
||||||
|
|| reader.TokenType == JsonTokenType.StartObject)
|
||||||
|
&& reader.CurrentDepth == _maxSearchDepth)
|
||||||
|
{
|
||||||
|
// There is no field we need to search for on a depth deeper than this, skip
|
||||||
|
reader.Skip();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.StartArray)
|
||||||
|
arrayIndex = -1;
|
||||||
|
else if (reader.TokenType == JsonTokenType.EndArray)
|
||||||
|
arrayIndex = null;
|
||||||
|
else if (arrayIndex != null)
|
||||||
|
arrayIndex++;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.PropertyName
|
||||||
|
|| arrayIndex != null && _hasArraySearches)
|
||||||
|
{
|
||||||
|
bool written = false;
|
||||||
|
|
||||||
|
string? value = null;
|
||||||
|
byte[]? propName = null;
|
||||||
|
foreach (var field in _searchFields!)
|
||||||
|
{
|
||||||
|
if (field.Field.Depth != reader.CurrentDepth)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
bool readArrayValues = false;
|
||||||
|
if (field.Field is PropertyFieldReference propFieldRef)
|
||||||
|
{
|
||||||
|
if (propName == null)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!reader.ValueTextEquals(propFieldRef.PropertyName))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
propName = propFieldRef.PropertyName;
|
||||||
|
readArrayValues = propFieldRef.ArrayValues;
|
||||||
|
reader.Read();
|
||||||
|
}
|
||||||
|
else if (!propFieldRef.PropertyName.SequenceEqual(propName))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (field.Field is ArrayFieldReference arrayFieldRef)
|
||||||
|
{
|
||||||
|
if (propName != null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (arrayFieldRef.ArrayIndex != arrayIndex)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.SkipReading)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (readArrayValues)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
// error
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
reader.Read();// Read start array
|
||||||
|
bool first = true;
|
||||||
|
while(reader.TokenType != JsonTokenType.EndArray)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
sb.Append(",");
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
sb.Append(reader.GetString());
|
||||||
|
reader.Read();
|
||||||
|
}
|
||||||
|
|
||||||
|
value = first ? null : sb.ToString();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (reader.TokenType)
|
||||||
|
{
|
||||||
|
case JsonTokenType.Number:
|
||||||
|
value = reader.GetDecimal().ToString();
|
||||||
|
break;
|
||||||
|
case JsonTokenType.String:
|
||||||
|
value = reader.GetString()!;
|
||||||
|
break;
|
||||||
|
case JsonTokenType.True:
|
||||||
|
case JsonTokenType.False:
|
||||||
|
value = reader.GetBoolean().ToString()!;
|
||||||
|
break;
|
||||||
|
case JsonTokenType.Null:
|
||||||
|
value = null;
|
||||||
|
break;
|
||||||
|
case JsonTokenType.StartObject:
|
||||||
|
case JsonTokenType.StartArray:
|
||||||
|
value = null;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.Field.Constraint != null
|
||||||
|
&& !field.Field.Constraint(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_searchResult.Write(field.Field, value);
|
||||||
|
|
||||||
|
if (field.ForceEvaluator != null)
|
||||||
|
{
|
||||||
|
if (field.ForceEvaluator.StaticIdentifier != null)
|
||||||
|
return field.ForceEvaluator.StaticIdentifier;
|
||||||
|
|
||||||
|
// Force the immediate return upon encountering this field
|
||||||
|
return field.ForceEvaluator.GetMessageType(_searchResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
written = true;
|
||||||
|
if (!field.OverlappingField)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!written)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (_topEvaluator!.Satisfied(_searchResult))
|
||||||
|
return _topEvaluator.GetMessageType(_searchResult);
|
||||||
|
|
||||||
|
if (_searchFields.Count == _searchResult.Count)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var evaluator in TypeEvaluators)
|
||||||
|
{
|
||||||
|
if (evaluator.Satisfied(_searchResult))
|
||||||
|
return evaluator.GetMessageType(_searchResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
|
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||||
|
{
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||||
|
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+63
@@ -0,0 +1,63 @@
|
|||||||
|
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
|
||||||
|
using System;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON WebSocket message handler, reads the json data info a JsonDocument after which the data can be inspected to identify the message
|
||||||
|
/// </summary>
|
||||||
|
public abstract class JsonSocketPreloadMessageHandler : ISocketMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The serializer options to use
|
||||||
|
/// </summary>
|
||||||
|
public abstract JsonSerializerOptions Options { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType)
|
||||||
|
{
|
||||||
|
var reader = new Utf8JsonReader(data);
|
||||||
|
var jsonDocument = JsonDocument.ParseValue(ref reader);
|
||||||
|
|
||||||
|
return GetTypeIdentifier(jsonDocument);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the message identifier for this document
|
||||||
|
/// </summary>
|
||||||
|
protected abstract string? GetTypeIdentifier(JsonDocument document);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get optional topic filter, for example a symbol name
|
||||||
|
/// </summary>
|
||||||
|
public virtual string? GetTopicFilter(object deserializedObject) => null;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
|
||||||
|
{
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
return JsonSerializer.Deserialize(data, type, Options)!;
|
||||||
|
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the string value for a path, or an emtpy string if not found
|
||||||
|
/// </summary>
|
||||||
|
protected string StringOrEmpty(JsonDocument document, string path)
|
||||||
|
{
|
||||||
|
if (!document.RootElement.TryGetProperty(path, out var element))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
if (element.ValueKind == JsonValueKind.String)
|
||||||
|
return element.GetString() ?? string.Empty;
|
||||||
|
else if (element.ValueKind == JsonValueKind.Number)
|
||||||
|
return element.GetDecimal().ToString();
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,40 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization.Metadata;
|
using System.Text.Json.Serialization.Metadata;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
internal class NullableEnumConverterFactory : JsonConverterFactory
|
|
||||||
{
|
{
|
||||||
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
|
internal class NullableEnumConverterFactory : JsonConverterFactory
|
||||||
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions();
|
|
||||||
|
|
||||||
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver)
|
|
||||||
{
|
{
|
||||||
_jsonTypeInfoResolver = jsonTypeInfoResolver;
|
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
|
||||||
}
|
private static readonly JsonSerializerOptions _options = new JsonSerializerOptions();
|
||||||
|
|
||||||
public override bool CanConvert(Type typeToConvert)
|
public NullableEnumConverterFactory(IJsonTypeInfoResolver jsonTypeInfoResolver)
|
||||||
{
|
{
|
||||||
var b = Nullable.GetUnderlyingType(typeToConvert);
|
_jsonTypeInfoResolver = jsonTypeInfoResolver;
|
||||||
if (b == null)
|
}
|
||||||
return false;
|
|
||||||
|
|
||||||
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options);
|
public override bool CanConvert(Type typeToConvert)
|
||||||
if (typeInfo == null)
|
{
|
||||||
return false;
|
var b = Nullable.GetUnderlyingType(typeToConvert);
|
||||||
|
if (b == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
return typeInfo.Converter is INullableConverterFactory;
|
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options);
|
||||||
}
|
if (typeInfo == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
return typeInfo.Converter is INullableConverterFactory;
|
||||||
{
|
}
|
||||||
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
|
|
||||||
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
|
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
|
{
|
||||||
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
|
var b = Nullable.GetUnderlyingType(typeToConvert) ?? throw new ArgumentNullException($"Not nullable {typeToConvert.Name}");
|
||||||
|
var typeInfo = _jsonTypeInfoResolver.GetTypeInfo(b, _options) ?? throw new ArgumentNullException($"Can find type {typeToConvert.Name}");
|
||||||
return nullConverterFactory.CreateNullableConverter();
|
if (typeInfo.Converter is not INullableConverterFactory nullConverterFactory)
|
||||||
|
throw new ArgumentNullException($"Can find type converter for {typeToConvert.Name}");
|
||||||
|
|
||||||
|
return nullConverterFactory.CreateNullableConverter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,42 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Read string or number as string
|
|
||||||
/// </summary>
|
|
||||||
public class NumberStringConverter : JsonConverter<string?>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
/// Read string or number as string
|
||||||
|
/// </summary>
|
||||||
|
public class NumberStringConverter : JsonConverter<string?>
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
/// <inheritdoc />
|
||||||
return null;
|
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
|
||||||
if (reader.TokenType == JsonTokenType.Number)
|
|
||||||
{
|
{
|
||||||
if (reader.TryGetInt64(out var value))
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
return value.ToString();
|
return null;
|
||||||
|
|
||||||
return reader.GetDecimal().ToString();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
/// <inheritdoc />
|
||||||
|
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
return reader.GetString();
|
writer.WriteStringValue(value);
|
||||||
}
|
}
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WriteStringValue(value);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
|
||||||
|
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Converter for parsing object or array responses
|
||||||
|
/// </summary>
|
||||||
|
public class ObjectOrArrayConverter : JsonConverterFactory
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanConvert(Type typeToConvert) => true;
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
var type = typeof(InternalObjectOrArrayConverter<>).MakeGenericType(typeToConvert);
|
||||||
|
return (JsonConverter)Activator.CreateInstance(type)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class InternalObjectOrArrayConverter<T> : JsonConverter<T>
|
||||||
|
{
|
||||||
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.StartObject && !typeToConvert.IsArray)
|
||||||
|
{
|
||||||
|
// Object to object
|
||||||
|
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
|
||||||
|
}
|
||||||
|
else if (reader.TokenType == JsonTokenType.StartArray && typeToConvert.IsArray)
|
||||||
|
{
|
||||||
|
// Array to array
|
||||||
|
return JsonDocument.ParseValue(ref reader).Deserialize<T>(options);
|
||||||
|
}
|
||||||
|
else if (reader.TokenType == JsonTokenType.StartArray)
|
||||||
|
{
|
||||||
|
// Array to object
|
||||||
|
JsonDocument.ParseValue(ref reader).Deserialize<T[]>(options);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Object to array
|
||||||
|
JsonDocument.ParseValue(ref reader);
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +1,43 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Converter for values which contain a nested json value
|
|
||||||
/// </summary>
|
|
||||||
public class ObjectStringConverter<T> : JsonConverter<T>
|
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
#if NET5_0_OR_GREATER
|
/// Converter for values which contain a nested json value
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
/// </summary>
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
public class ObjectStringConverter<T> : JsonConverter<T>
|
||||||
#endif
|
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
{
|
||||||
if (reader.TokenType == JsonTokenType.Null)
|
/// <inheritdoc />
|
||||||
return default;
|
|
||||||
|
|
||||||
var value = reader.GetString();
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
return default;
|
|
||||||
|
|
||||||
return JsonDocument.Parse(value!).Deserialize<T>(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
#endif
|
#endif
|
||||||
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (value is null)
|
if (reader.TokenType == JsonTokenType.Null)
|
||||||
writer.WriteStringValue("");
|
return default;
|
||||||
|
|
||||||
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
var value = reader.GetString();
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
#if NET5_0_OR_GREATER
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
||||||
|
#endif
|
||||||
|
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
writer.WriteStringValue("");
|
||||||
|
|
||||||
|
writer.WriteStringValue(JsonSerializer.Serialize(value, options));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +1,41 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Replace a value on a string property
|
|
||||||
/// </summary>
|
|
||||||
public abstract class ReplaceConverter : JsonConverter<string>
|
|
||||||
{
|
{
|
||||||
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Replace a value on a string property
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ReplaceConverter(params string[] replaceSets)
|
public abstract class ReplaceConverter : JsonConverter<string>
|
||||||
{
|
{
|
||||||
_replacementSets = replaceSets.Select(x =>
|
private readonly (string ValueToReplace, string ValueToReplaceWith)[] _replacementSets;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public ReplaceConverter(params string[] replaceSets)
|
||||||
{
|
{
|
||||||
var split = x.Split(["->"], StringSplitOptions.None);
|
_replacementSets = replaceSets.Select(x =>
|
||||||
if (split.Length != 2)
|
{
|
||||||
throw new ArgumentException("Invalid replacement config");
|
var split = x.Split(new string[] { "->" }, StringSplitOptions.None);
|
||||||
return (split[0], split[1]);
|
if (split.Length != 2)
|
||||||
}).ToArray();
|
throw new ArgumentException("Invalid replacement config");
|
||||||
}
|
return (split[0], split[1]);
|
||||||
|
}).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var value = reader.GetString();
|
var value = reader.GetString();
|
||||||
foreach (var set in _replacementSets)
|
foreach (var set in _replacementSets)
|
||||||
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
value = value?.Replace(set.ValueToReplace, set.ValueToReplaceWith);
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) => writer.WriteStringValue(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attribute to mark a model as json serializable. Used for AOT compilation.
|
|
||||||
/// </summary>
|
|
||||||
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
|
|
||||||
public class SerializationModelAttribute : Attribute
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// Attribute to mark a model as json serializable. Used for AOT compilation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SerializationModelAttribute() { }
|
[AttributeUsage(System.AttributeTargets.Class | AttributeTargets.Enum | System.AttributeTargets.Interface)]
|
||||||
/// <summary>
|
public class SerializationModelAttribute : Attribute
|
||||||
/// ctor
|
{
|
||||||
/// </summary>
|
/// <summary>
|
||||||
/// <param name="type"></param>
|
/// ctor
|
||||||
public SerializationModelAttribute(Type type) { }
|
/// </summary>
|
||||||
|
public SerializationModelAttribute() { }
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type"></param>
|
||||||
|
public SerializationModelAttribute(Type type) { }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +1,47 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Serializer options
|
|
||||||
/// </summary>
|
|
||||||
public static class SerializerOptions
|
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
/// Serializer options
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
public static class SerializerOptions
|
||||||
{
|
{
|
||||||
if (!_cache.TryGetValue(typeResolver, out var options))
|
private static readonly ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions> _cache = new ConcurrentDictionary<JsonSerializerContext, JsonSerializerOptions>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get Json serializer settings which includes standard converters for DateTime, bool, enum and number types
|
||||||
|
/// </summary>
|
||||||
|
public static JsonSerializerOptions WithConverters(JsonSerializerContext typeResolver, params JsonConverter[] additionalConverters)
|
||||||
{
|
{
|
||||||
options = new JsonSerializerOptions
|
if (!_cache.TryGetValue(typeResolver, out var options))
|
||||||
{
|
{
|
||||||
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
options = new JsonSerializerOptions
|
||||||
PropertyNameCaseInsensitive = false,
|
|
||||||
Converters =
|
|
||||||
{
|
{
|
||||||
new DateTimeConverter(),
|
NumberHandling = JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||||
new BoolConverter(),
|
PropertyNameCaseInsensitive = false,
|
||||||
new DecimalConverter(),
|
Converters =
|
||||||
new IntConverter(),
|
{
|
||||||
new LongConverter(),
|
new DateTimeConverter(),
|
||||||
new NullableEnumConverterFactory(typeResolver)
|
new BoolConverter(),
|
||||||
},
|
new DecimalConverter(),
|
||||||
TypeInfoResolver = typeResolver,
|
new IntConverter(),
|
||||||
};
|
new LongConverter(),
|
||||||
|
new NullableEnumConverterFactory(typeResolver)
|
||||||
|
},
|
||||||
|
TypeInfoResolver = typeResolver,
|
||||||
|
};
|
||||||
|
|
||||||
foreach (var converter in additionalConverters)
|
foreach (var converter in additionalConverters)
|
||||||
options.Converters.Add(converter);
|
options.Converters.Add(converter);
|
||||||
|
|
||||||
options.TypeInfoResolver = typeResolver;
|
options.TypeInfoResolver = typeResolver;
|
||||||
_cache.TryAdd(typeResolver, options);
|
_cache.TryAdd(typeResolver, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
return options;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,58 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
|
|
||||||
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
|
|
||||||
|
|
||||||
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
|
|
||||||
{
|
{
|
||||||
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
internal class SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
|
||||||
|
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
|
||||||
|
|
||||||
|
internal class SharedQuantityReferenceConverter<T> : JsonConverter<T> where T: SharedQuantityReference, new()
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
throw new Exception("");
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
throw new Exception("");
|
||||||
|
|
||||||
reader.Read(); // Start array
|
reader.Read(); // Start array
|
||||||
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
var baseQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||||
reader.Read();
|
reader.Read();
|
||||||
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
var quoteQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||||
reader.Read();
|
reader.Read();
|
||||||
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
var contractQuantity = reader.TokenType == JsonTokenType.Null ? (decimal?)null : reader.GetDecimal();
|
||||||
reader.Read();
|
reader.Read();
|
||||||
|
|
||||||
if (reader.TokenType != JsonTokenType.EndArray)
|
if (reader.TokenType != JsonTokenType.EndArray)
|
||||||
throw new Exception("");
|
throw new Exception("");
|
||||||
|
|
||||||
reader.Read(); // End array
|
reader.Read(); // End array
|
||||||
|
|
||||||
var result = new T();
|
var result = new T();
|
||||||
result.QuantityInBaseAsset = baseQuantity;
|
result.QuantityInBaseAsset = baseQuantity;
|
||||||
result.QuantityInQuoteAsset = quoteQuantity;
|
result.QuantityInQuoteAsset = quoteQuantity;
|
||||||
result.QuantityInContracts = contractQuantity;
|
result.QuantityInContracts = contractQuantity;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
writer.WriteStartArray();
|
writer.WriteStartArray();
|
||||||
if (value.QuantityInBaseAsset == null)
|
if (value.QuantityInBaseAsset == null)
|
||||||
writer.WriteNullValue();
|
writer.WriteNullValue();
|
||||||
else
|
else
|
||||||
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
|
writer.WriteNumberValue(value.QuantityInBaseAsset.Value);
|
||||||
|
|
||||||
if (value.QuantityInQuoteAsset == null)
|
if (value.QuantityInQuoteAsset == null)
|
||||||
writer.WriteNullValue();
|
writer.WriteNullValue();
|
||||||
else
|
else
|
||||||
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
|
writer.WriteNumberValue(value.QuantityInQuoteAsset.Value);
|
||||||
|
|
||||||
if (value.QuantityInContracts == null)
|
if (value.QuantityInContracts == null)
|
||||||
writer.WriteNullValue();
|
writer.WriteNullValue();
|
||||||
else
|
else
|
||||||
writer.WriteNumberValue(value.QuantityInContracts.Value);
|
writer.WriteNumberValue(value.QuantityInContracts.Value);
|
||||||
writer.WriteEndArray();
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
|
|
||||||
{
|
{
|
||||||
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
|
||||||
{
|
{
|
||||||
if (reader.TokenType != JsonTokenType.StartArray)
|
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
throw new Exception("");
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartArray)
|
||||||
|
throw new Exception("");
|
||||||
|
|
||||||
reader.Read(); // Start array
|
reader.Read(); // Start array
|
||||||
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
var tradingMode = (TradingMode)Enum.Parse(typeof(TradingMode), reader.GetString()!);
|
||||||
reader.Read();
|
reader.Read();
|
||||||
var baseAsset = reader.GetString()!;
|
var baseAsset = reader.GetString()!;
|
||||||
reader.Read();
|
reader.Read();
|
||||||
var quoteAsset = reader.GetString()!;
|
var quoteAsset = reader.GetString()!;
|
||||||
reader.Read();
|
reader.Read();
|
||||||
var timeStr = reader.GetString()!;
|
var timeStr = reader.GetString()!;
|
||||||
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
|
var deliverTime = string.IsNullOrEmpty(timeStr) ? (DateTime?)null : DateTime.Parse(timeStr);
|
||||||
reader.Read();
|
reader.Read();
|
||||||
|
|
||||||
if (reader.TokenType != JsonTokenType.EndArray)
|
if (reader.TokenType != JsonTokenType.EndArray)
|
||||||
throw new Exception("");
|
throw new Exception("");
|
||||||
|
|
||||||
reader.Read(); // End array
|
reader.Read(); // End array
|
||||||
|
|
||||||
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
|
return new SharedSymbol(tradingMode, baseAsset, quoteAsset, deliverTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, SharedSymbol value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
writer.WriteStartArray();
|
writer.WriteStartArray();
|
||||||
writer.WriteStringValue(value.TradingMode.ToString());
|
writer.WriteStringValue(value.TradingMode.ToString());
|
||||||
writer.WriteStringValue(value.BaseAsset);
|
writer.WriteStringValue(value.BaseAsset);
|
||||||
writer.WriteStringValue(value.QuoteAsset);
|
writer.WriteStringValue(value.QuoteAsset);
|
||||||
writer.WriteStringValue(value.DeliverTime?.ToString());
|
writer.WriteStringValue(value.DeliverTime?.ToString());
|
||||||
writer.WriteEndArray();
|
writer.WriteEndArray();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,376 +0,0 @@
|
|||||||
using CryptoExchange.Net.Converters.MessageParsing;
|
|
||||||
using CryptoExchange.Net.Interfaces;
|
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System;
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
#endif
|
|
||||||
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 readonly JsonSerializerOptions? _customSerializerOptions;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsValid { get; set; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public abstract bool OriginalDataAvailable { get; }
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public object? Underlying => throw new NotImplementedException();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageAccessor(JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
_customSerializerOptions = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public CallResult<object> Deserialize(Type type, MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
return new CallResult<object>(GetOriginalString());
|
|
||||||
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _document.Deserialize(type, _customSerializerOptions);
|
|
||||||
return new CallResult<object>(result!);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
|
||||||
return new CallResult<object>(new DeserializeError(info, ex));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new CallResult<object>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public CallResult<T> Deserialize<T>(MessagePath? path = null)
|
|
||||||
{
|
|
||||||
if (_document == null)
|
|
||||||
throw new InvalidOperationException("No json document loaded");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = _document.Deserialize<T>(_customSerializerOptions);
|
|
||||||
return new CallResult<T>(result!);
|
|
||||||
}
|
|
||||||
catch (JsonException ex)
|
|
||||||
{
|
|
||||||
var info = $"Json deserialization failed: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
|
|
||||||
return new CallResult<T>(new DeserializeError(info, ex));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new CallResult<T>(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public NodeType? GetNodeType()
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
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 (!IsValid)
|
|
||||||
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 />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public T? GetValue<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
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>(_customSerializerOptions);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
|
|
||||||
#endif
|
|
||||||
public T?[]? GetValues<T>(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
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<T[]>(_customSerializerOptions)!;
|
|
||||||
}
|
|
||||||
|
|
||||||
private JsonElement? GetPathNode(MessagePath path)
|
|
||||||
{
|
|
||||||
if (!IsValid)
|
|
||||||
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>
|
|
||||||
#pragma warning disable CA1001 // Types that own disposable fields should be disposable
|
|
||||||
public class SystemTextJsonStreamMessageAccessor : SystemTextJsonMessageAccessor, IStreamMessageAccessor
|
|
||||||
#pragma warning restore CA1001 // Types that own disposable fields should be disposable
|
|
||||||
{
|
|
||||||
private Stream? _stream;
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool OriginalDataAvailable => _stream?.CanSeek == true;
|
|
||||||
|
|
||||||
/// <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);
|
|
||||||
IsValid = true;
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsValid = false;
|
|
||||||
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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(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
|
|
||||||
IsValid = false;
|
|
||||||
return new CallResult(new DeserializeError("Not a json value"));
|
|
||||||
}
|
|
||||||
|
|
||||||
_document = JsonDocument.Parse(data);
|
|
||||||
IsValid = true;
|
|
||||||
return CallResult.SuccessResult;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Not a json message
|
|
||||||
IsValid = false;
|
|
||||||
return new CallResult(new DeserializeError($"Json deserialization failed: {ex.Message}", ex));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,27 @@
|
|||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
#if NET5_0_OR_GREATER
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
#endif
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Converters.SystemTextJson;
|
namespace CryptoExchange.Net.Converters.SystemTextJson
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
|
||||||
{
|
{
|
||||||
private readonly JsonSerializerOptions _options;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ctor
|
|
||||||
/// </summary>
|
|
||||||
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
_options = options;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
public class SystemTextJsonMessageSerializer : IStringMessageSerializer
|
||||||
|
{
|
||||||
|
private readonly JsonSerializerOptions _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public SystemTextJsonMessageSerializer(JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
#if NET5_0_OR_GREATER
|
#if NET5_0_OR_GREATER
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
||||||
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "Everything referenced in the loaded assembly is manually preserved, so it's safe")]
|
||||||
#endif
|
#endif
|
||||||
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
|
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0</TargetFrameworks>
|
<TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PackageId>CryptoExchange.Net</PackageId>
|
<PackageId>CryptoExchange.Net</PackageId>
|
||||||
<Authors>JKorf</Authors>
|
<Authors>JKorf</Authors>
|
||||||
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
<Description>CryptoExchange.Net is a base library which is used to implement different cryptocurrency (exchange) API's. It provides a standardized way of implementing different API's, which results in a very similar experience for users of the API implementations.</Description>
|
||||||
<PackageVersion>9.6.0</PackageVersion>
|
<PackageVersion>10.7.1</PackageVersion>
|
||||||
<AssemblyVersion>9.6.0</AssemblyVersion>
|
<AssemblyVersion>10.7.1</AssemblyVersion>
|
||||||
<FileVersion>9.6.0</FileVersion>
|
<FileVersion>10.7.1</FileVersion>
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
||||||
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
<PackageTags>OKX;OKX.Net;Mexc;Mexc.Net;Kucoin;Kucoin.Net;Kraken;Kraken.Net;Huobi;Huobi.Net;CoinEx;CoinEx.Net;Bybit;Bybit.Net;Bitget;Bitget.Net;Bitfinex;Bitfinex.Net;Binance;Binance.Net;CryptoCurrency;CryptoCurrency Exchange;CryptoExchange.Net</PackageTags>
|
||||||
<RepositoryType>git</RepositoryType>
|
<RepositoryType>git</RepositoryType>
|
||||||
@@ -20,11 +20,10 @@
|
|||||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
<PackageReleaseNotes>https://github.com/JKorf/CryptoExchange.Net?tab=readme-ov-file#release-notes</PackageReleaseNotes>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>12.0</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="C:\Projects\CryptoExchange.Net\CryptoExchange.Net\.editorconfig" />
|
|
||||||
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
<None Include="Icon\icon.png" Pack="true" PackagePath="\" />
|
||||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -41,31 +40,24 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
<DocumentationFile>CryptoExchange.Net.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
|
||||||
<EnableNETAnalyzers>true</EnableNETAnalyzers>
|
|
||||||
<AnalysisMode>Recommended</AnalysisMode>
|
|
||||||
<AnalysisModeGlobalization>None</AnalysisModeGlobalization>
|
|
||||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
|
||||||
</PropertyGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
<PackageReference Include="ConfigureAwaitChecker.Analyzer" Version="5.0.0.1">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
|
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.101">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.1" />
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.6" />
|
<PackageReference Include="System.Text.Json" Version="10.0.1" />
|
||||||
|
<PackageReference Include="NSec.Cryptography" Version="25.4.0" Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Client Packages">
|
<ItemGroup Label="Transitive Client Packages">
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" />
|
||||||
</ItemGroup>
|
<PackageReference Include="System.Threading.Channels" Version="10.0.1" />
|
||||||
<ItemGroup>
|
|
||||||
<EditorConfigFiles Remove="C:\Projects\CryptoExchange.Net\CryptoExchange.Net\.editorconfig" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Exceptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Exception during deserialization
|
||||||
|
/// </summary>
|
||||||
|
public class CeDeserializationException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CeDeserializationException(string message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ctor
|
||||||
|
/// </summary>
|
||||||
|
public CeDeserializationException(string message, Exception innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,390 +1,535 @@
|
|||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
#endif
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace CryptoExchange.Net;
|
namespace CryptoExchange.Net
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// General helpers functions
|
|
||||||
/// </summary>
|
|
||||||
public static class ExchangeHelpers
|
|
||||||
{
|
{
|
||||||
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
|
||||||
private const string _allowedRandomHexChars = "0123456789ABCDEF";
|
|
||||||
|
|
||||||
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>
|
/// <summary>
|
||||||
/// The last used id, use NextId() to get the next id and up this
|
/// General helpers functions
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static int _lastId;
|
public static class ExchangeHelpers
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Clamp a value between a min and max
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min"></param>
|
|
||||||
/// <param name="max"></param>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static decimal ClampValue(decimal min, decimal max, decimal value)
|
|
||||||
{
|
{
|
||||||
value = Math.Min(max, value);
|
private const string _allowedRandomChars = "ABCDEFGHIJKLMONOPQRSTUVWXYZabcdefghijklmonopqrstuvwxyz0123456789";
|
||||||
value = Math.Max(min, value);
|
private const string _allowedRandomHexChars = "0123456789ABCDEF";
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
private static readonly Dictionary<int, string> _monthSymbols = new Dictionary<int, string>()
|
||||||
/// Adjust a value to be between the min and max parameters and rounded to the closest step.
|
{
|
||||||
/// </summary>
|
{ 1, "F" },
|
||||||
/// <param name="min">The min value</param>
|
{ 2, "G" },
|
||||||
/// <param name="max">The max value</param>
|
{ 3, "H" },
|
||||||
/// <param name="step">The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54</param>
|
{ 4, "J" },
|
||||||
/// <param name="roundingType">How to round</param>
|
{ 5, "K" },
|
||||||
/// <param name="value">The input value</param>
|
{ 6, "M" },
|
||||||
/// <returns></returns>
|
{ 7, "N" },
|
||||||
public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value)
|
{ 8, "Q" },
|
||||||
{
|
{ 9, "U" },
|
||||||
if(step == 0)
|
{ 10, "V" },
|
||||||
throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step));
|
{ 11, "X" },
|
||||||
|
{ 12, "Z" },
|
||||||
|
};
|
||||||
|
|
||||||
value = Math.Min(max, value);
|
/// <summary>
|
||||||
value = Math.Max(min, value);
|
/// The last used id, use NextId() to get the next id and up this
|
||||||
if (step == null)
|
/// </summary>
|
||||||
|
private static int _lastId;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clamp a value between a min and max
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="min"></param>
|
||||||
|
/// <param name="max"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal ClampValue(decimal min, decimal max, decimal value)
|
||||||
|
{
|
||||||
|
value = Math.Min(max, value);
|
||||||
|
value = Math.Max(min, value);
|
||||||
return value;
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
var offset = value % step.Value;
|
/// <summary>
|
||||||
if(roundingType == RoundingType.Down)
|
/// Adjust a value to be between the min and max parameters and rounded to the closest step.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="min">The min value</param>
|
||||||
|
/// <param name="max">The max value</param>
|
||||||
|
/// <param name="step">The step size the value should be floored to. For example, value 2.548 with a step size of 0.01 will output 2.54</param>
|
||||||
|
/// <param name="roundingType">How to round</param>
|
||||||
|
/// <param name="value">The input value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal AdjustValueStep(decimal min, decimal max, decimal? step, RoundingType roundingType, decimal value)
|
||||||
{
|
{
|
||||||
value -= offset;
|
if(step == 0)
|
||||||
}
|
throw new ArgumentException($"0 not allowed for parameter {nameof(step)}, pass in null to ignore the step size", nameof(step));
|
||||||
else if(roundingType == RoundingType.Up)
|
|
||||||
{
|
value = Math.Min(max, value);
|
||||||
if (offset != 0)
|
value = Math.Max(min, value);
|
||||||
value += (step.Value - offset);
|
if (step == null)
|
||||||
}
|
return value;
|
||||||
else
|
|
||||||
{
|
var offset = value % step.Value;
|
||||||
if (offset < step / 2)
|
if(roundingType == RoundingType.Down)
|
||||||
|
{
|
||||||
value -= offset;
|
value -= offset;
|
||||||
else value += (step.Value - offset);
|
}
|
||||||
}
|
else if(roundingType == RoundingType.Up)
|
||||||
|
|
||||||
value = RoundDown(value, 8);
|
|
||||||
|
|
||||||
return value.Normalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Adjust a value to be between the min and max parameters and rounded to the closest precision.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="min">The min value</param>
|
|
||||||
/// <param name="max">The max value</param>
|
|
||||||
/// <param name="precision">The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542</param>
|
|
||||||
/// <param name="roundingType">How to round</param>
|
|
||||||
/// <param name="value">The input value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value)
|
|
||||||
{
|
|
||||||
value = Math.Min(max, value);
|
|
||||||
value = Math.Max(min, value);
|
|
||||||
if (precision == null)
|
|
||||||
return value;
|
|
||||||
|
|
||||||
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Apply the provided rules to the value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Value to be adjusted</param>
|
|
||||||
/// <param name="decimals">Max decimal places</param>
|
|
||||||
/// <param name="valueStep">The value step for increase/decrease value</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static decimal ApplyRules(
|
|
||||||
decimal value,
|
|
||||||
int? decimals = null,
|
|
||||||
decimal? valueStep = null)
|
|
||||||
{
|
|
||||||
if (valueStep.HasValue)
|
|
||||||
{
|
|
||||||
var offset = value % valueStep.Value;
|
|
||||||
if (offset != 0)
|
|
||||||
{
|
{
|
||||||
if (offset < valueStep.Value / 2)
|
if (offset != 0)
|
||||||
|
value += (step.Value - offset);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (offset < step / 2)
|
||||||
value -= offset;
|
value -= offset;
|
||||||
else value += (valueStep.Value - offset);
|
else value += (step.Value - offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return value.Normalize();
|
||||||
}
|
}
|
||||||
if (decimals.HasValue)
|
|
||||||
value = Math.Round(value, decimals.Value);
|
|
||||||
|
|
||||||
return value;
|
/// <summary>
|
||||||
}
|
/// Adjust a value to be between the min and max parameters and rounded to the closest precision.
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="min">The min value</param>
|
||||||
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
/// <param name="max">The max value</param>
|
||||||
/// </summary>
|
/// <param name="precision">The precision the value should be rounded to. For example, value 2.554215 with a precision of 5 will output 2.5542</param>
|
||||||
/// <param name="value">The value to round</param>
|
/// <param name="roundingType">How to round</param>
|
||||||
/// <param name="digits">The total amount of digits (NOT decimal places) to round to</param>
|
/// <param name="value">The input value</param>
|
||||||
/// <param name="roundingType">How to round</param>
|
/// <returns></returns>
|
||||||
/// <returns></returns>
|
public static decimal AdjustValuePrecision(decimal min, decimal max, int? precision, RoundingType roundingType, decimal value)
|
||||||
public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType)
|
|
||||||
{
|
|
||||||
var val = (double)value;
|
|
||||||
if (value == 0)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1);
|
|
||||||
if(roundingType == RoundingType.Closest)
|
|
||||||
return (decimal)(scale * Math.Round(val / scale, digits));
|
|
||||||
else
|
|
||||||
return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Rounds a value down
|
|
||||||
/// </summary>
|
|
||||||
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>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static decimal Normalize(this decimal value)
|
|
||||||
{
|
|
||||||
return value / 1.000000000000000000000000000000000m;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
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="length">Length of the random string</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string RandomHexString(int length)
|
|
||||||
{
|
|
||||||
#if NET9_0_OR_GREATER
|
|
||||||
return "0x" + RandomNumberGenerator.GetHexString(length * 2);
|
|
||||||
#else
|
|
||||||
var randomChars = new char[length * 2];
|
|
||||||
var random = new Random();
|
|
||||||
for (int i = 0; i < length * 2; i++)
|
|
||||||
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
|
|
||||||
return "0x" + new string(randomChars);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a long value
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="maxLength">Max character length</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static long RandomLong(int maxLength)
|
|
||||||
{
|
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
|
||||||
var value = RandomNumberGenerator.GetInt32(0, int.MaxValue);
|
|
||||||
#else
|
|
||||||
var random = new Random();
|
|
||||||
var value = random.Next(0, int.MaxValue);
|
|
||||||
#endif
|
|
||||||
var val = value.ToString();
|
|
||||||
if (val.Length > maxLength)
|
|
||||||
return int.Parse(val.Substring(0, maxLength));
|
|
||||||
else
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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="TResult">Type of the client</typeparam>
|
|
||||||
/// <typeparam name="TRequest">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<TResult[]>> ExecutePages<TResult, TRequest>(Func<TRequest, INextPageToken?, CancellationToken, Task<ExchangeWebResult<TResult[]>>> paginatedFunc, TRequest request, [EnumeratorCancellation]CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
var result = new List<TResult>();
|
|
||||||
ExchangeWebResult<TResult[]> batch;
|
|
||||||
INextPageToken? nextPageToken = null;
|
|
||||||
while (true)
|
|
||||||
{
|
{
|
||||||
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
value = Math.Min(max, value);
|
||||||
yield return batch;
|
value = Math.Max(min, value);
|
||||||
if (!batch || ct.IsCancellationRequested)
|
if (precision == null)
|
||||||
break;
|
return value;
|
||||||
|
|
||||||
result.AddRange(batch.Data);
|
return RoundToSignificantDigits(value, precision.Value, roundingType);
|
||||||
nextPageToken = batch.NextPageToken;
|
|
||||||
if (nextPageToken == null)
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Apply the rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
|
/// Apply the provided rules to the value
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="symbol">The symbol as retrieved from the exchange</param>
|
/// <param name="value">Value to be adjusted</param>
|
||||||
/// <param name="quantity">Quantity to trade</param>
|
/// <param name="decimals">Max decimal places</param>
|
||||||
/// <param name="price">Price to trade at</param>
|
/// <param name="valueStep">The value step for increase/decrease value</param>
|
||||||
/// <param name="adjustedQuantity">Quantity adjusted to match all trading rules</param>
|
/// <returns></returns>
|
||||||
/// <param name="adjustedPrice">Price adjusted to match all trading rules</param>
|
public static decimal ApplyRules(
|
||||||
public static void ApplySymbolRules(SharedSpotSymbol symbol, decimal quantity, decimal? price, out decimal adjustedQuantity, out decimal? adjustedPrice)
|
decimal value,
|
||||||
{
|
int? decimals = null,
|
||||||
adjustedPrice = price;
|
decimal? valueStep = null)
|
||||||
adjustedQuantity = quantity;
|
|
||||||
var minNotionalAdjust = false;
|
|
||||||
|
|
||||||
if (price != null)
|
|
||||||
{
|
{
|
||||||
adjustedPrice = AdjustValueStep(0, decimal.MaxValue, symbol.PriceStep, RoundingType.Down, price.Value);
|
if (valueStep.HasValue)
|
||||||
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;
|
var offset = value % valueStep.Value;
|
||||||
minNotionalAdjust = true;
|
if (offset != 0)
|
||||||
|
{
|
||||||
|
if (offset < valueStep.Value / 2)
|
||||||
|
value -= offset;
|
||||||
|
else value += (valueStep.Value - offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (decimals.HasValue)
|
||||||
|
value = Math.Round(value, decimals.Value);
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Round a value to have the provided total number of digits. For example, value 253.12332 with 5 digits would be 253.12
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value to round</param>
|
||||||
|
/// <param name="digits">The total amount of digits (NOT decimal places) to round to</param>
|
||||||
|
/// <param name="roundingType">How to round</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal RoundToSignificantDigits(decimal value, int digits, RoundingType roundingType)
|
||||||
|
{
|
||||||
|
var val = (double)value;
|
||||||
|
if (value == 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
double scale = Math.Pow(10, Math.Floor(Math.Log10(Math.Abs(val))) + 1);
|
||||||
|
if(roundingType == RoundingType.Closest)
|
||||||
|
return (decimal)(scale * Math.Round(val / scale, digits));
|
||||||
|
else
|
||||||
|
return (decimal)(scale * (double)RoundDown((decimal)(val / scale), digits));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Rounds a value down
|
||||||
|
/// </summary>
|
||||||
|
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>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static decimal Normalize(this decimal value)
|
||||||
|
{
|
||||||
|
return value / 1.000000000000000000000000000000000m;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a new unique id. The id is statically stored so it is guaranteed to be unique
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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="length">Length of the random string</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static string RandomHexString(int length)
|
||||||
|
{
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
return "0x" + RandomNumberGenerator.GetHexString(length * 2);
|
||||||
|
#else
|
||||||
|
var randomChars = new char[length * 2];
|
||||||
|
var random = new Random();
|
||||||
|
for (int i = 0; i < length * 2; i++)
|
||||||
|
randomChars[i] = _allowedRandomHexChars[random.Next(0, _allowedRandomHexChars.Length)];
|
||||||
|
return "0x" + new string(randomChars);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a long value
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxLength">Max number of digits</param>
|
||||||
|
public static long RandomLong(int maxLength)
|
||||||
|
{
|
||||||
|
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
|
||||||
|
var value = RandomNumberGenerator.GetInt32(0, int.MaxValue);
|
||||||
|
#else
|
||||||
|
var random = new Random();
|
||||||
|
var value = random.Next(0, int.MaxValue);
|
||||||
|
#endif
|
||||||
|
var val = value.ToString();
|
||||||
|
if (val.Length > maxLength)
|
||||||
|
return int.Parse(val.Substring(0, maxLength));
|
||||||
|
else
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a long value between two values
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="minValue">Min value</param>
|
||||||
|
/// <param name="maxValue">Max value</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static long RandomLong(long minValue, long maxValue)
|
||||||
|
{
|
||||||
|
#if NET8_0_OR_GREATER
|
||||||
|
var buf = RandomNumberGenerator.GetBytes(8);
|
||||||
|
#else
|
||||||
|
byte[] buf = new byte[8];
|
||||||
|
var random = new Random();
|
||||||
|
random.NextBytes(buf);
|
||||||
|
#endif
|
||||||
|
long longRand = BitConverter.ToInt64(buf, 0);
|
||||||
|
return (Math.Abs(longRand % (maxValue - minValue)) + minValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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<T[]>> ExecutePages<T, U>(Func<U, PageRequest?, CancellationToken, Task<ExchangeWebResult<T[]>>> paginatedFunc, U request, [EnumeratorCancellation]CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var result = new List<T>();
|
||||||
|
ExchangeWebResult<T[]> batch;
|
||||||
|
PageRequest? nextPageToken = null;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
|
||||||
|
yield return batch;
|
||||||
|
if (!batch || ct.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
result.AddRange(batch.Data);
|
||||||
|
nextPageToken = batch.NextPageRequest;
|
||||||
|
if (nextPageToken == null)
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adjustedQuantity = AdjustValueStep(symbol.MinTradeQuantity ?? 0, symbol.MaxTradeQuantity ?? decimal.MaxValue, symbol.QuantityStep, minNotionalAdjust ? RoundingType.Up : RoundingType.Down, adjustedQuantity);
|
/// <summary>
|
||||||
adjustedQuantity = symbol.QuantityDecimals.HasValue ? (minNotionalAdjust ? RoundUp(adjustedQuantity, symbol.QuantityDecimals.Value) : RoundDown(adjustedQuantity, symbol.QuantityDecimals.Value)) : adjustedQuantity;
|
/// Apply filters to the data set
|
||||||
|
/// </summary>
|
||||||
}
|
/// <typeparam name="T">Type</typeparam>
|
||||||
|
/// <param name="data">Data set</param>
|
||||||
/// <summary>
|
/// <param name="timeSelector">Time selector for the data</param>
|
||||||
/// Parse a decimal value from a string
|
/// <param name="startTime">Start time filter</param>
|
||||||
/// </summary>
|
/// <param name="endTime">End time filter</param>
|
||||||
public static decimal? ParseDecimal(string? value)
|
/// <param name="direction">Data direction</param>
|
||||||
{
|
public static IEnumerable<T> ApplyFilter<T>(
|
||||||
// Value is null or empty is the most common case to return null so check before trying to parse
|
IEnumerable<T> data,
|
||||||
if (string.IsNullOrEmpty(value))
|
Func<T, DateTime> timeSelector,
|
||||||
return null;
|
DateTime? startTime,
|
||||||
|
DateTime? endTime,
|
||||||
// Try parse, only fails for these reasons:
|
DataDirection direction)
|
||||||
// 1. string is null or empty
|
|
||||||
// 2. value is larger or smaller than decimal max/min
|
|
||||||
// 3. unparsable format
|
|
||||||
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
|
|
||||||
return decValue;
|
|
||||||
|
|
||||||
// Check for values which should be parsed to null
|
|
||||||
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|
|
||||||
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
{
|
||||||
|
if (direction == DataDirection.Ascending)
|
||||||
|
data = data.OrderBy(timeSelector);
|
||||||
|
else
|
||||||
|
data = data.OrderByDescending(timeSelector);
|
||||||
|
|
||||||
|
if (startTime != null)
|
||||||
|
data = data.Where(x => timeSelector(x) >= startTime.Value);
|
||||||
|
|
||||||
|
if (endTime != null)
|
||||||
|
data = data.Where(x => timeSelector(x) < endTime.Value);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue updates received from a websocket subscriptions and process them async
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The queued update type</typeparam>
|
||||||
|
/// <param name="subscribeCall">The subscribe call</param>
|
||||||
|
/// <param name="asyncHandler">The async update handler</param>
|
||||||
|
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||||
|
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||||
|
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<T>(
|
||||||
|
Func<Action<DataEvent<T>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||||
|
Func<DataEvent<T>, Task> asyncHandler,
|
||||||
|
int? maxQueuedItems = null,
|
||||||
|
QueueFullBehavior? fullBehavior = null)
|
||||||
|
{
|
||||||
|
var processor = new ProcessQueue<DataEvent<T>>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||||
|
await processor.StartAsync().ConfigureAwait(false);
|
||||||
|
var result = await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
await processor.StopAsync().ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
|
||||||
|
result.Data.SubscriptionStatusChanged += (upd) =>
|
||||||
|
{
|
||||||
|
if (upd == CryptoExchange.Net.Objects.SubscriptionStatus.Closed)
|
||||||
|
_ = processor.StopAsync(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue updates and process them async
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The queued update type</typeparam>
|
||||||
|
/// <param name="subscribeCall">The subscribe call</param>
|
||||||
|
/// <param name="asyncHandler">The async update handler</param>
|
||||||
|
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||||
|
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||||
|
/// <param name="ct">Cancellation token to stop the processing</param>
|
||||||
|
public static async Task ProcessQueuedAsync<T>(
|
||||||
|
Func<Action<T>, Task> subscribeCall,
|
||||||
|
Func<T, Task> asyncHandler,
|
||||||
|
CancellationToken ct,
|
||||||
|
int? maxQueuedItems = null,
|
||||||
|
QueueFullBehavior? fullBehavior = null)
|
||||||
|
{
|
||||||
|
var processor = new ProcessQueue<T>(asyncHandler, maxQueuedItems, fullBehavior);
|
||||||
|
await processor.StartAsync().ConfigureAwait(false);
|
||||||
|
ct.Register(async () =>
|
||||||
|
{
|
||||||
|
await processor.StopAsync().ConfigureAwait(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await subscribeCall(upd => processor.Write(upd)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue updates received from a websocket subscriptions and process them async
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TEventType">The type of the queued item</typeparam>
|
||||||
|
/// <typeparam name="TOutputType">The type of the item to pass to the processor</typeparam>
|
||||||
|
/// <param name="subscribeCall">The subscribe call</param>
|
||||||
|
/// <param name="mapper">The mapper function to go from <see>TEventType</see> to <see>TOutputType</see></param>
|
||||||
|
/// <param name="asyncHandler">The async update handler</param>
|
||||||
|
/// <param name="maxQueuedItems">The max number of updates to be queued up. When happens when the queue is full and a new write is attempted can be specified with <see>fullMode</see></param>
|
||||||
|
/// <param name="fullBehavior">What should happen if the queue contains <see>maxQueuedItems</see> pending updates. If no max is set this setting is ignored</param>
|
||||||
|
public static async Task<CallResult<UpdateSubscription>> ProcessQueuedAsync<TEventType, TOutputType>(
|
||||||
|
Func<ProcessQueue<DataEvent<TEventType>>, Task<CallResult<UpdateSubscription>>> subscribeCall,
|
||||||
|
Func<DataEvent<TEventType>, DataEvent<TOutputType>> mapper,
|
||||||
|
Func<DataEvent<TOutputType>, Task> asyncHandler,
|
||||||
|
int? maxQueuedItems = null,
|
||||||
|
QueueFullBehavior? fullBehavior = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var processor = new ProcessQueue<DataEvent<TEventType>>((update) => {
|
||||||
|
return asyncHandler.Invoke(mapper.Invoke(update));
|
||||||
|
}, maxQueuedItems, fullBehavior);
|
||||||
|
await processor.StartAsync().ConfigureAwait(false);
|
||||||
|
var result = await subscribeCall(processor).ConfigureAwait(false);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
await processor.StopAsync().ConfigureAwait(false);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
processor.Exception += result.Data._subscription.InvokeExceptionHandler;
|
||||||
|
result.Data.SubscriptionStatusChanged += (upd) =>
|
||||||
|
{
|
||||||
|
if (upd == SubscriptionStatus.Closed)
|
||||||
|
_ = processor.StopAsync(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a decimal value from a string
|
||||||
|
/// </summary>
|
||||||
|
public static decimal? ParseDecimal(string? value)
|
||||||
|
{
|
||||||
|
// Value is null or empty is the most common case to return null so check before trying to parse
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Try parse, only fails for these reasons:
|
||||||
|
// 1. string is null or empty
|
||||||
|
// 2. value is larger or smaller than decimal max/min
|
||||||
|
// 3. unparsable format
|
||||||
|
if (decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var decValue))
|
||||||
|
return decValue;
|
||||||
|
|
||||||
|
// Check for values which should be parsed to null
|
||||||
|
if (string.Equals("null", value, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals("NaN", value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infinity value should be parsed to min/max value
|
||||||
|
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return decimal.MaxValue;
|
||||||
|
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return decimal.MinValue;
|
||||||
|
|
||||||
|
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
|
||||||
|
{
|
||||||
|
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
|
||||||
|
// assume overflow
|
||||||
|
if (overflowValue < 0)
|
||||||
|
return decimal.MinValue;
|
||||||
|
else
|
||||||
|
return decimal.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown decimal format, return null
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infinity value should be parsed to min/max value
|
|
||||||
if (string.Equals("Infinity", value, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return decimal.MaxValue;
|
|
||||||
else if(string.Equals("-Infinity", value, StringComparison.OrdinalIgnoreCase))
|
|
||||||
return decimal.MinValue;
|
|
||||||
|
|
||||||
if (value!.Length > 27 && decimal.TryParse(value.Substring(0, 27), out var overflowValue))
|
|
||||||
{
|
|
||||||
// Not a valid decimal value and more than 27 chars, from which the first part can be parsed correctly.
|
|
||||||
// assume overflow
|
|
||||||
if (overflowValue < 0)
|
|
||||||
return decimal.MinValue;
|
|
||||||
else
|
|
||||||
return decimal.MaxValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unknown decimal format, return null
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,68 +1,129 @@
|
|||||||
using CryptoExchange.Net.SharedApis;
|
using CryptoExchange.Net.SharedApis;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net;
|
namespace CryptoExchange.Net
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cache for symbol parsing
|
|
||||||
/// </summary>
|
|
||||||
public static class ExchangeSymbolCache
|
|
||||||
{
|
{
|
||||||
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the cached symbol data for an exchange
|
/// Cache for symbol parsing
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
public static class ExchangeSymbolCache
|
||||||
/// <param name="updateData">Symbol data</param>
|
|
||||||
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
|
|
||||||
{
|
{
|
||||||
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
private static ConcurrentDictionary<string, ExchangeInfo> _symbolInfos = new ConcurrentDictionary<string, ExchangeInfo>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the cached symbol data for an exchange
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="updateData">Symbol data</param>
|
||||||
|
public static void UpdateSymbolInfo(string topicId, SharedSpotSymbol[] updateData)
|
||||||
{
|
{
|
||||||
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
if(!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
{
|
||||||
|
exchangeInfo = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||||
|
_symbolInfos.TryAdd(topicId, exchangeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
||||||
|
return;
|
||||||
|
|
||||||
|
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DateTime.UtcNow - exchangeInfo.UpdateTime < TimeSpan.FromMinutes(60))
|
/// <summary>
|
||||||
return;
|
/// Whether the specific topic has been cached
|
||||||
|
/// </summary>
|
||||||
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
|
/// <param name="topicId">Id</param>
|
||||||
}
|
public static bool HasCached(string topicId)
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse a symbol name to a SharedSymbol
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="topicId">Id for the provided data</param>
|
|
||||||
/// <param name="symbolName">Symbol name</param>
|
|
||||||
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
|
|
||||||
{
|
|
||||||
if (symbolName == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
|
||||||
{
|
{
|
||||||
DeliverTime = symbolInfo.DeliverTime
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
};
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
class ExchangeInfo
|
return exchangeInfo.Symbols.Count > 0;
|
||||||
{
|
}
|
||||||
public DateTime UpdateTime { get; set; }
|
|
||||||
public Dictionary<string, SharedSymbol> Symbols { get; set; }
|
|
||||||
|
|
||||||
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
|
/// <summary>
|
||||||
|
/// Whether a specific exchange(topic) support the provided symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="symbolName">The symbol name</param>
|
||||||
|
public static bool SupportsSymbol(string topicId, string symbolName)
|
||||||
{
|
{
|
||||||
UpdateTime = updateTime;
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
Symbols = symbols;
|
return false;
|
||||||
|
|
||||||
|
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether a specific exchange(topic) support the provided symbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="symbol">The symbol info</param>
|
||||||
|
public static bool SupportsSymbol(string topicId, SharedSymbol symbol)
|
||||||
|
{
|
||||||
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return exchangeInfo.Symbols.Any(x =>
|
||||||
|
x.Value.TradingMode == symbol.TradingMode
|
||||||
|
&& x.Value.BaseAsset == symbol.BaseAsset
|
||||||
|
&& x.Value.QuoteAsset == symbol.QuoteAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all symbols for a specific base asset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="baseAsset">Base asset name</param>
|
||||||
|
public static SharedSymbol[] GetSymbolsForBaseAsset(string topicId, string baseAsset)
|
||||||
|
{
|
||||||
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return exchangeInfo.Symbols
|
||||||
|
.Where(x => x.Value.BaseAsset.Equals(baseAsset, StringComparison.InvariantCultureIgnoreCase))
|
||||||
|
.Select(x => x.Value)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a symbol name to a SharedSymbol
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topicId">Id for the provided data</param>
|
||||||
|
/// <param name="symbolName">Symbol name</param>
|
||||||
|
public static SharedSymbol? ParseSymbol(string topicId, string? symbolName)
|
||||||
|
{
|
||||||
|
if (symbolName == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (!exchangeInfo.Symbols.TryGetValue(symbolName, out var symbolInfo))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return new SharedSymbol(symbolInfo.TradingMode, symbolInfo.BaseAsset, symbolInfo.QuoteAsset, symbolName)
|
||||||
|
{
|
||||||
|
DeliverTime = symbolInfo.DeliverTime
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExchangeInfo
|
||||||
|
{
|
||||||
|
public DateTime UpdateTime { get; set; }
|
||||||
|
public Dictionary<string, SharedSymbol> Symbols { get; set; }
|
||||||
|
|
||||||
|
public ExchangeInfo(DateTime updateTime, Dictionary<string, SharedSymbol> symbols)
|
||||||
|
{
|
||||||
|
UpdateTime = updateTime;
|
||||||
|
Symbols = symbols;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,520 +1,495 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO.Compression;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using CryptoExchange.Net.Objects;
|
|
||||||
using System.Globalization;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net;
|
namespace CryptoExchange.Net
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Helper methods
|
|
||||||
/// </summary>
|
|
||||||
public static class ExtensionMethods
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add a parameter
|
/// Helper methods
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="parameters"></param>
|
public static class ExtensionMethods
|
||||||
/// <param name="key"></param>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value)
|
|
||||||
{
|
{
|
||||||
parameters.Add(key, value);
|
/// <summary>
|
||||||
}
|
/// Add a parameter
|
||||||
|
/// </summary>
|
||||||
/// <summary>
|
/// <param name="parameters"></param>
|
||||||
/// Add a parameter
|
/// <param name="key"></param>
|
||||||
/// </summary>
|
/// <param name="value"></param>
|
||||||
/// <param name="parameters"></param>
|
public static void AddParameter(this Dictionary<string, object> parameters, string key, string value)
|
||||||
/// <param name="key"></param>
|
{
|
||||||
/// <param name="value"></param>
|
|
||||||
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value)
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
parameters.Add(key, value);
|
parameters.Add(key, value);
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a query string of the specified parameters
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters">The parameters to use</param>
|
|
||||||
/// <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 IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
|
||||||
{
|
|
||||||
var uriString = string.Empty;
|
|
||||||
var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
|
|
||||||
foreach (var arrayEntry in arraysParameters)
|
|
||||||
{
|
|
||||||
if (serializationType == ArrayParametersSerialization.Array)
|
|
||||||
{
|
|
||||||
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 += $"{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(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}";
|
/// <summary>
|
||||||
uriString = uriString.TrimEnd('&');
|
/// Add a parameter
|
||||||
return uriString;
|
/// </summary>
|
||||||
}
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
/// <summary>
|
/// <param name="value"></param>
|
||||||
/// Convert a dictionary to formdata string
|
public static void AddParameter(this Dictionary<string, object> parameters, string key, object value)
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
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)
|
parameters.Add(key, value);
|
||||||
continue;
|
}
|
||||||
|
|
||||||
if (kvp.Value.GetType().IsArray)
|
/// <summary>
|
||||||
|
/// Add an optional parameter. Not added if value is null
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <param name="key"></param>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
public static void AddOptionalParameter(this Dictionary<string, object> parameters, string key, object? value)
|
||||||
|
{
|
||||||
|
if (value != null)
|
||||||
|
parameters.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a query string of the specified parameters
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters">The parameters to use</param>
|
||||||
|
/// <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 IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
|
||||||
|
{
|
||||||
|
var uriString = new StringBuilder();
|
||||||
|
bool first = true;
|
||||||
|
foreach(var parameter in parameters)
|
||||||
{
|
{
|
||||||
var array = (Array)kvp.Value;
|
if (!first)
|
||||||
foreach (var value in array)
|
uriString.Append("&");
|
||||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return formData.ToString()!;
|
first = false;
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
if (parameter.Value.GetType().IsArray)
|
||||||
/// Validates an int is one of the allowed values
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">Value of the int</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
/// <param name="allowedValues">Allowed values</param>
|
|
||||||
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
|
||||||
{
|
|
||||||
if (!allowedValues.Contains(value))
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates an int is between two values
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value of the int</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
/// <param name="minValue">Min value</param>
|
|
||||||
/// <param name="maxValue">Max value</param>
|
|
||||||
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
|
||||||
{
|
|
||||||
if (value < minValue || value > maxValue)
|
|
||||||
{
|
|
||||||
throw new ArgumentException(
|
|
||||||
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates a string is not null or empty
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value of the string</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
public static void ValidateNotNull(this string value, string argumentName)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates a string is null or not empty
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value"></param>
|
|
||||||
/// <param name="argumentName"></param>
|
|
||||||
public static void ValidateNullOrNotEmpty(this string value, string argumentName)
|
|
||||||
{
|
|
||||||
if (value != null && string.IsNullOrEmpty(value))
|
|
||||||
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates an object is not null
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value of the object</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
public static void ValidateNotNull(this object value, string argumentName)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Validates a list is not null or empty
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value of the object</param>
|
|
||||||
/// <param name="argumentName">Name of the parameter</param>
|
|
||||||
public static void ValidateNotNull<T>(this IEnumerable<T> value, string argumentName)
|
|
||||||
{
|
|
||||||
if (value == null || !value.Any())
|
|
||||||
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>
|
|
||||||
/// <param name="exception"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string ToLogString(this Exception? exception)
|
|
||||||
{
|
|
||||||
var message = new StringBuilder();
|
|
||||||
var indent = 0;
|
|
||||||
while (exception != null)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < indent; i++)
|
|
||||||
message.Append(' ');
|
|
||||||
message.Append(exception.GetType().Name);
|
|
||||||
message.Append(" - ");
|
|
||||||
message.AppendLine(exception.Message);
|
|
||||||
for (var i = 0; i < indent; i++)
|
|
||||||
message.Append(' ');
|
|
||||||
message.AppendLine(exception.StackTrace);
|
|
||||||
|
|
||||||
indent += 2;
|
|
||||||
exception = exception.InnerException;
|
|
||||||
}
|
|
||||||
|
|
||||||
return message.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Append a base url with provided path
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="url"></param>
|
|
||||||
/// <param name="path"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static string AppendPath(this string url, params string[] path)
|
|
||||||
{
|
|
||||||
if (!url.EndsWith("/"))
|
|
||||||
url += "/";
|
|
||||||
|
|
||||||
foreach (var item in path)
|
|
||||||
url += item.Trim('/') + "/";
|
|
||||||
|
|
||||||
return url.TrimEnd('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a new uri with the provided parameters as query
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <param name="baseUri"></param>
|
|
||||||
/// <param name="arraySerialization"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Uri SetParameters(this Uri baseUri, IDictionary<string, object> parameters, ArrayParametersSerialization arraySerialization)
|
|
||||||
{
|
|
||||||
var uriBuilder = new UriBuilder();
|
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
|
||||||
uriBuilder.Host = baseUri.Host;
|
|
||||||
uriBuilder.Port = baseUri.Port;
|
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
{
|
|
||||||
if (parameter.Value.GetType().IsArray)
|
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
|
||||||
{
|
{
|
||||||
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
|
if (serializationType == ArrayParametersSerialization.Array)
|
||||||
|
{
|
||||||
|
bool firstArrayValue = true;
|
||||||
|
foreach (var entry in (object[])parameter.Value)
|
||||||
|
{
|
||||||
|
if (!firstArrayValue)
|
||||||
|
uriString.Append('&');
|
||||||
|
firstArrayValue = false;
|
||||||
|
|
||||||
|
uriString.Append(parameter.Key);
|
||||||
|
uriString.Append("[]=");
|
||||||
|
if (urlEncodeValues)
|
||||||
|
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||||
|
else
|
||||||
|
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (serializationType == ArrayParametersSerialization.MultipleValues)
|
||||||
|
{
|
||||||
|
bool firstArrayValue = true;
|
||||||
|
foreach (var entry in (object[])parameter.Value)
|
||||||
|
{
|
||||||
|
if (!firstArrayValue)
|
||||||
|
uriString.Append('&');
|
||||||
|
firstArrayValue = false;
|
||||||
|
uriString.Append(parameter.Key);
|
||||||
|
uriString.Append("=");
|
||||||
|
if (urlEncodeValues)
|
||||||
|
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||||
|
else
|
||||||
|
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uriString.Append('[');
|
||||||
|
var firstArrayEntry = true;
|
||||||
|
foreach (var entry in (object[])parameter.Value)
|
||||||
|
{
|
||||||
|
if (!firstArrayEntry)
|
||||||
|
uriString.Append(',');
|
||||||
|
|
||||||
|
firstArrayEntry = false;
|
||||||
|
if (urlEncodeValues)
|
||||||
|
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", entry)));
|
||||||
|
else
|
||||||
|
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", entry));
|
||||||
|
}
|
||||||
|
uriString.Append(']');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
uriString.Append(parameter.Key);
|
||||||
|
uriString.Append('=');
|
||||||
|
if (urlEncodeValues)
|
||||||
|
uriString.Append(Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value)));
|
||||||
|
else
|
||||||
|
uriString.Append(string.Format(CultureInfo.InvariantCulture, "{0}", parameter.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return uriString.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a dictionary to formdata string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parameters"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
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, string.Format(CultureInfo.InvariantCulture, "{0}", value));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var item in (object[])parameter.Value)
|
formData.Add(kvp.Key, string.Format(CultureInfo.InvariantCulture, "{0}", kvp.Value));
|
||||||
{
|
|
||||||
if (arraySerialization == ArrayParametersSerialization.Array)
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
httpValueCollection.Add(parameter.Key, item.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
return formData.ToString()!;
|
||||||
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uriBuilder.Query = httpValueCollection.ToString();
|
/// <summary>
|
||||||
return uriBuilder.Uri;
|
/// Validates an int is one of the allowed values
|
||||||
}
|
/// </summary>
|
||||||
|
/// <param name="value">Value of the int</param>
|
||||||
/// <summary>
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
/// Create a new uri with the provided parameters as query
|
/// <param name="allowedValues">Allowed values</param>
|
||||||
/// </summary>
|
public static void ValidateIntValues(this int value, string argumentName, params int[] allowedValues)
|
||||||
/// <param name="parameters"></param>
|
|
||||||
/// <param name="baseUri"></param>
|
|
||||||
/// <param name="arraySerialization"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static Uri SetParameters(this Uri baseUri, IOrderedEnumerable<KeyValuePair<string, object>> parameters, ArrayParametersSerialization arraySerialization)
|
|
||||||
{
|
|
||||||
var uriBuilder = new UriBuilder();
|
|
||||||
uriBuilder.Scheme = baseUri.Scheme;
|
|
||||||
uriBuilder.Host = baseUri.Host;
|
|
||||||
uriBuilder.Port = baseUri.Port;
|
|
||||||
uriBuilder.Path = baseUri.AbsolutePath;
|
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(string.Empty);
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
{
|
{
|
||||||
if (parameter.Value.GetType().IsArray)
|
if (!allowedValues.Contains(value))
|
||||||
{
|
{
|
||||||
if (arraySerialization == ArrayParametersSerialization.JsonArray)
|
throw new ArgumentException(
|
||||||
{
|
$"{value} not allowed for parameter {argumentName}, allowed values: {string.Join(", ", allowedValues)}", argumentName);
|
||||||
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();
|
/// <summary>
|
||||||
return uriBuilder.Uri;
|
/// Validates an int is between two values
|
||||||
}
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the int</param>
|
||||||
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
|
/// <param name="minValue">Min value</param>
|
||||||
|
/// <param name="maxValue">Max value</param>
|
||||||
|
public static void ValidateIntBetween(this int value, string argumentName, int minValue, int maxValue)
|
||||||
|
{
|
||||||
|
if (value < minValue || value > maxValue)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"{value} not allowed for parameter {argumentName}, min: {minValue}, max: {maxValue}", argumentName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add parameter to URI
|
/// Validates a string is not null or empty
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="uri"></param>
|
/// <param name="value">The value of the string</param>
|
||||||
/// <param name="name"></param>
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
/// <param name="value"></param>
|
public static void ValidateNotNull(this string value, string argumentName)
|
||||||
/// <returns></returns>
|
{
|
||||||
public static Uri AddQueryParameter(this Uri uri, string name, string value)
|
if (string.IsNullOrEmpty(value))
|
||||||
{
|
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
||||||
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
|
}
|
||||||
|
|
||||||
httpValueCollection.Remove(name);
|
/// <summary>
|
||||||
httpValueCollection.Add(name, value);
|
/// Validates a string is null or not empty
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value"></param>
|
||||||
|
/// <param name="argumentName"></param>
|
||||||
|
public static void ValidateNullOrNotEmpty(this string value, string argumentName)
|
||||||
|
{
|
||||||
|
if (value != null && string.IsNullOrEmpty(value))
|
||||||
|
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
var ub = new UriBuilder(uri);
|
/// <summary>
|
||||||
ub.Query = httpValueCollection.ToString();
|
/// Validates an object is not null
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the object</param>
|
||||||
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
|
public static void ValidateNotNull(this object value, string argumentName)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
throw new ArgumentException($"No value provided for parameter {argumentName}", argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
return ub.Uri;
|
/// <summary>
|
||||||
}
|
/// Validates a list is not null or empty
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The value of the object</param>
|
||||||
|
/// <param name="argumentName">Name of the parameter</param>
|
||||||
|
public static void ValidateNotNull<T>(this IEnumerable<T> value, string argumentName)
|
||||||
|
{
|
||||||
|
if (value == null || !value.Any())
|
||||||
|
throw new ArgumentException($"No values provided for parameter {argumentName}", argumentName);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Decompress using GzipStream
|
/// Format a string to RFC3339/ISO8601 string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="data"></param>
|
/// <param name="dateTime"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
|
public static string ToRfc3339String(this DateTime dateTime)
|
||||||
{
|
{
|
||||||
using var decompressedStream = new MemoryStream();
|
return dateTime.ToString("yyyy-MM-dd'T'HH:mm:ss.fffzzz", DateTimeFormatInfo.InvariantInfo);
|
||||||
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>
|
/// <summary>
|
||||||
/// Decompress using DeflateStream
|
/// Format an exception and inner exception to a readable string
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input"></param>
|
/// <param name="exception"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
|
public static string ToLogString(this Exception? exception)
|
||||||
{
|
{
|
||||||
var output = new MemoryStream();
|
var message = new StringBuilder();
|
||||||
|
var indent = 0;
|
||||||
|
while (exception != null)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < indent; i++)
|
||||||
|
message.Append(' ');
|
||||||
|
message.Append(exception.GetType().Name);
|
||||||
|
message.Append(" - ");
|
||||||
|
message.AppendLine(exception.Message);
|
||||||
|
for (var i = 0; i < indent; i++)
|
||||||
|
message.Append(' ');
|
||||||
|
message.AppendLine(exception.StackTrace);
|
||||||
|
|
||||||
using (var compressStream = new MemoryStream(input.ToArray()))
|
indent += 2;
|
||||||
using (var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress))
|
exception = exception.InnerException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return message.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Append a base url with provided path
|
||||||
|
/// </summary>
|
||||||
|
public static string AppendPath(this string url, params string[] path)
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(url.TrimEnd('/'));
|
||||||
|
foreach (var subPath in path)
|
||||||
|
{
|
||||||
|
sb.Append('/');
|
||||||
|
sb.Append(subPath.Trim('/'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decompress using GzipStream
|
||||||
|
/// </summary>
|
||||||
|
public static ReadOnlySpan<byte> DecompressGzip(this ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
using var decompressedStream = new MemoryStream();
|
||||||
|
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
|
||||||
|
deflateStream.CopyTo(decompressedStream);
|
||||||
|
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decompress using GzipStream
|
||||||
|
/// </summary>
|
||||||
|
public static ReadOnlySpan<byte> Decompress(this ReadOnlySpan<byte> input)
|
||||||
|
{
|
||||||
|
using var output = new MemoryStream();
|
||||||
|
using var compressStream = new MemoryStream(input.ToArray());
|
||||||
|
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress);
|
||||||
decompressor.CopyTo(output);
|
decompressor.CopyTo(output);
|
||||||
|
return new ReadOnlySpan<byte>(output.GetBuffer(), 0, (int)output.Length);
|
||||||
|
}
|
||||||
|
|
||||||
output.Position = 0;
|
/// <summary>
|
||||||
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
|
/// Whether the trading mode is linear
|
||||||
}
|
/// </summary>
|
||||||
|
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is linear
|
/// Whether the trading mode is inverse
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsLinear(this TradingMode type) => type == TradingMode.PerpetualLinear || type == TradingMode.DeliveryLinear;
|
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>
|
/// <summary>
|
||||||
/// Whether the trading mode is inverse
|
/// Whether the trading mode is delivery
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsInverse(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.DeliveryInverse;
|
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the trading mode is perpetual
|
|
||||||
/// </summary>
|
|
||||||
public static bool IsPerpetual(this TradingMode type) => type == TradingMode.PerpetualInverse || type == TradingMode.PerpetualLinear;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the trading mode is delivery
|
/// Whether the account type is a futures account
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
|
public static bool IsFuturesAccount(this SharedAccountType type) =>
|
||||||
|
type == SharedAccountType.PerpetualLinearFutures
|
||||||
|
|| type == SharedAccountType.DeliveryLinearFutures
|
||||||
|
|| type == SharedAccountType.PerpetualInverseFutures
|
||||||
|
|| type == SharedAccountType.DeliveryInverseFutures;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register rest client interfaces
|
/// Whether the account type is a margin account
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IServiceCollection RegisterSharedRestInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
public static bool IsMarginAccount(this SharedAccountType type) =>
|
||||||
{
|
type == SharedAccountType.CrossMargin
|
||||||
if (typeof(IAssetsRestClient).IsAssignableFrom(typeof(T)))
|
|| type == SharedAccountType.IsolatedMargin;
|
||||||
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(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
|
||||||
|
|
||||||
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
/// <summary>
|
||||||
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
/// Map a TradingMode value to a SharedAccountType enum value
|
||||||
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
|
/// </summary>
|
||||||
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
public static SharedAccountType ToAccountType(this TradingMode mode)
|
||||||
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
{
|
||||||
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
if (mode == TradingMode.Spot) return SharedAccountType.Spot;
|
||||||
if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
if (mode == TradingMode.PerpetualLinear) return SharedAccountType.PerpetualLinearFutures;
|
||||||
services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!);
|
if (mode == TradingMode.PerpetualInverse) return SharedAccountType.PerpetualInverseFutures;
|
||||||
if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
if (mode == TradingMode.DeliveryInverse) return SharedAccountType.DeliveryInverseFutures;
|
||||||
services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!);
|
if (mode == TradingMode.DeliveryLinear) return SharedAccountType.DeliveryLinearFutures;
|
||||||
|
|
||||||
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
throw new ArgumentException(nameof(mode), "Unmapped trading mode");
|
||||||
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)!);
|
|
||||||
if (typeof(IFuturesTpSlRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesTpSlRestClient)client(x)!);
|
|
||||||
if (typeof(IFuturesTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesTriggerOrderRestClient)client(x)!);
|
|
||||||
if (typeof(IFuturesOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IFuturesOrderClientIdRestClient)client(x)!);
|
|
||||||
|
|
||||||
return services;
|
/// <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(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IBookTickerRestClient)client(x)!);
|
||||||
|
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (ITransferRestClient)client(x)!);
|
||||||
|
|
||||||
/// <summary>
|
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
/// Register socket client interfaces
|
services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
|
||||||
/// </summary>
|
if (typeof(ISpotSymbolRestClient).IsAssignableFrom(typeof(T)))
|
||||||
public static IServiceCollection RegisterSharedSocketInterfaces<T>(this IServiceCollection services, Func<IServiceProvider, T> client)
|
services.AddTransient(x => (ISpotSymbolRestClient)client(x)!);
|
||||||
{
|
if (typeof(ISpotTickerRestClient).IsAssignableFrom(typeof(T)))
|
||||||
if (typeof(IBalanceSocketClient).IsAssignableFrom(typeof(T)))
|
services.AddTransient(x => (ISpotTickerRestClient)client(x)!);
|
||||||
services.AddTransient(x => (IBalanceSocketClient)client(x)!);
|
if (typeof(ISpotTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
if (typeof(IBookTickerSocketClient).IsAssignableFrom(typeof(T)))
|
services.AddTransient(x => (ISpotTriggerOrderRestClient)client(x)!);
|
||||||
services.AddTransient(x => (IBookTickerSocketClient)client(x)!);
|
if (typeof(ISpotOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
||||||
if (typeof(IKlineSocketClient).IsAssignableFrom(typeof(T)))
|
services.AddTransient(x => (ISpotOrderClientIdRestClient)client(x)!);
|
||||||
services.AddTransient(x => (IKlineSocketClient)client(x)!);
|
|
||||||
if (typeof(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IOrderBookSocketClient)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)))
|
if (typeof(IFundingRateRestClient).IsAssignableFrom(typeof(T)))
|
||||||
services.AddTransient(x => (ISpotOrderSocketClient)client(x)!);
|
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)!);
|
||||||
|
if (typeof(IFuturesTpSlRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesTpSlRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesTriggerOrderRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesTriggerOrderRestClient)client(x)!);
|
||||||
|
if (typeof(IFuturesOrderClientIdRestClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IFuturesOrderClientIdRestClient)client(x)!);
|
||||||
|
|
||||||
if (typeof(IFuturesOrderSocketClient).IsAssignableFrom(typeof(T)))
|
return services;
|
||||||
services.AddTransient(x => (IFuturesOrderSocketClient)client(x)!);
|
}
|
||||||
if (typeof(IPositionSocketClient).IsAssignableFrom(typeof(T)))
|
|
||||||
services.AddTransient(x => (IPositionSocketClient)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(IOrderBookSocketClient).IsAssignableFrom(typeof(T)))
|
||||||
|
services.AddTransient(x => (IOrderBookSocketClient)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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a hex encoded string to byte array
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hexString"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static byte[] HexStringToBytes(this string hexString)
|
||||||
|
{
|
||||||
|
if (hexString.StartsWith("0x"))
|
||||||
|
hexString = hexString.Substring(2);
|
||||||
|
|
||||||
|
byte[] bytes = new byte[hexString.Length / 2];
|
||||||
|
for (int i = 0; i < hexString.Length; i += 2)
|
||||||
|
{
|
||||||
|
string hexSubstring = hexString.Substring(i, 2);
|
||||||
|
bytes[i / 2] = Convert.ToByte(hexSubstring, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using CryptoExchange.Net.Authentication;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using CryptoExchange.Net.SharedApis;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base api client
|
||||||
|
/// </summary>
|
||||||
|
public interface IBaseApiClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base address
|
||||||
|
/// </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,17 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client for accessing REST API's for different exchanges
|
||||||
|
/// </summary>
|
||||||
|
public interface ICryptoRestClient
|
||||||
|
{
|
||||||
|
/// <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.Clients
|
||||||
|
{
|
||||||
|
/// <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,18 @@
|
|||||||
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base rest API client
|
||||||
|
/// </summary>
|
||||||
|
public interface IRestApiClient : IBaseApiClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The factory for creating requests. Used for unit testing
|
||||||
|
/// </summary>
|
||||||
|
IRequestFactory RequestFactory { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total amount of requests made with this API client
|
||||||
|
/// </summary>
|
||||||
|
int TotalRequestsMade { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for rest API implementations
|
||||||
|
/// </summary>
|
||||||
|
public interface IRestClient: IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The options provided for this client
|
||||||
|
/// </summary>
|
||||||
|
ExchangeOptions ClientOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The total amount of requests made with this client
|
||||||
|
/// </summary>
|
||||||
|
int TotalRequestsMade { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The exchange name
|
||||||
|
/// </summary>
|
||||||
|
string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether client is disposed
|
||||||
|
/// </summary>
|
||||||
|
bool Disposed { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
using CryptoExchange.Net.Sockets.Default.Interfaces;
|
||||||
|
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Socket API client
|
||||||
|
/// </summary>
|
||||||
|
public interface ISocketApiClient: IBaseApiClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of socket connections on the API client
|
||||||
|
/// </summary>
|
||||||
|
int CurrentConnections { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of subscriptions over all connections
|
||||||
|
/// </summary>
|
||||||
|
int CurrentSubscriptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Incoming data Kbps
|
||||||
|
/// </summary>
|
||||||
|
double IncomingKbps { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// The factory for creating sockets. Used for unit testing
|
||||||
|
/// </summary>
|
||||||
|
IWebsocketFactory SocketFactory { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// High performance websocket factory
|
||||||
|
/// </summary>
|
||||||
|
IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// Current client options
|
||||||
|
/// </summary>
|
||||||
|
SocketExchangeOptions ClientOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Current API options
|
||||||
|
/// </summary>
|
||||||
|
SocketApiOptions ApiOptions { get; }
|
||||||
|
/// <summary>
|
||||||
|
/// Log the current state of connections and subscriptions
|
||||||
|
/// </summary>
|
||||||
|
string GetSubscriptionsState(bool includeSubDetails = true);
|
||||||
|
/// <summary>
|
||||||
|
/// Reconnect all connections
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task ReconnectAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe all subscriptions
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task UnsubscribeAllAsync();
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<bool> UnsubscribeAsync(int subscriptionId);
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe an update subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using CryptoExchange.Net.Objects.Options;
|
||||||
|
using CryptoExchange.Net.Objects.Sockets;
|
||||||
|
|
||||||
|
namespace CryptoExchange.Net.Interfaces.Clients
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for socket API implementations
|
||||||
|
/// </summary>
|
||||||
|
public interface ISocketClient: IDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The exchange name
|
||||||
|
/// </summary>
|
||||||
|
string Exchange { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The options provided for this client
|
||||||
|
/// </summary>
|
||||||
|
ExchangeOptions ClientOptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Incoming kilobytes per second of data
|
||||||
|
/// </summary>
|
||||||
|
public double IncomingKbps { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of connections to the API from this client. A connection can have multiple subscriptions.
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentConnections { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current amount of subscriptions running from the client
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentSubscriptions { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether client is disposed
|
||||||
|
/// </summary>
|
||||||
|
bool Disposed { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe from a stream using the subscription id received when starting the subscription
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscriptionId">The id of the subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task UnsubscribeAsync(int subscriptionId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe from a stream
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The subscription to unsubscribe</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task UnsubscribeAsync(UpdateSubscription subscription);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe all subscriptions
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task UnsubscribeAllAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces;
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Time provider
|
|
||||||
/// </summary>
|
|
||||||
internal interface IAuthTimeProvider
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get current time
|
/// Time provider
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
internal interface IAuthTimeProvider
|
||||||
DateTime GetTime();
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get current time
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
DateTime GetTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
using CryptoExchange.Net.Authentication;
|
|
||||||
using CryptoExchange.Net.Objects.Options;
|
|
||||||
using CryptoExchange.Net.SharedApis;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Base api client
|
|
||||||
/// </summary>
|
|
||||||
public interface IBaseApiClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Base address
|
|
||||||
/// </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;
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net.Interfaces;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client for accessing REST API's for different exchanges
|
|
||||||
/// </summary>
|
|
||||||
public interface ICryptoRestClient
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Try get
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T"></typeparam>
|
|
||||||
/// <returns></returns>
|
|
||||||
T TryGet<T>(Func<T> createFunc);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
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,13 @@
|
|||||||
|
namespace CryptoExchange.Net.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Service for an exchange
|
||||||
|
/// </summary>
|
||||||
|
public interface IExchangeService
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The exchange the service is for
|
||||||
|
/// </summary>
|
||||||
|
public string ExchangeName { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user