1
0
mirror of https://github.com/JKorf/CryptoExchange.Net.git synced 2026-08-12 17:03:10 +00:00

Compare commits

..

3 Commits

Author SHA1 Message Date
JKorf 73764970b0 . 2025-08-25 19:11:50 +02:00
JKorf 9ba29035b2 Merge branch 'master' into feature/code-analyzis 2025-08-25 17:37:46 +02:00
JKorf 4c953e2c87 wip 2025-08-24 21:58:52 +02:00
468 changed files with 24656 additions and 34631 deletions
+1 -1
View File
@@ -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: 10.0.x dotnet-version: 9.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;net10.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.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>10.0.1</PackageVersion> <PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>10.0.1</AssemblyVersion> <AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>10.0.1</FileVersion> <FileVersion>9.6.0</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="10.0.2" /> <PackageReference Include="CryptoExchange.Net" Version="9.6.0" />
<PackageReference Include="protobuf-net" Version="3.2.56" /> <PackageReference Include="protobuf-net" Version="3.2.56" />
</ItemGroup> </ItemGroup>
</Project> </Project>
-30
View File
@@ -5,36 +5,6 @@
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.Threading; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
@@ -140,17 +140,5 @@ 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,5 +1,11 @@
using NUnit.Framework; using CryptoExchange.Net.Authentication;
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
{ {
@@ -19,6 +25,20 @@ 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,8 +4,10 @@ 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.Net; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests namespace CryptoExchange.Net.UnitTests
{ {
@@ -111,8 +113,7 @@ namespace CryptoExchange.Net.UnitTests
{ {
var result = new WebCallResult<TestObjectResult>( var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK, System.Net.HttpStatusCode.OK,
HttpVersion.Version11, new KeyValuePair<string, string[]>[0],
new HttpResponseMessage().Headers,
TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1),
null, null,
"{}", "{}",
@@ -120,7 +121,7 @@ namespace CryptoExchange.Net.UnitTests
"https://test.com/api", "https://test.com/api",
null, null,
HttpMethod.Get, HttpMethod.Get,
new HttpRequestMessage().Headers, new KeyValuePair<string, string[]>[0],
ResultDataSource.Server, ResultDataSource.Server,
new TestObjectResult(), new TestObjectResult(),
null); null);
@@ -142,8 +143,7 @@ namespace CryptoExchange.Net.UnitTests
{ {
var result = new WebCallResult<TestObjectResult>( var result = new WebCallResult<TestObjectResult>(
System.Net.HttpStatusCode.OK, System.Net.HttpStatusCode.OK,
HttpVersion.Version11, new KeyValuePair<string, string[]>[0],
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 HttpRequestMessage().Headers, new KeyValuePair<string, string[]>[0],
ResultDataSource.Server, ResultDataSource.Server,
new TestObjectResult(), new TestObjectResult(),
null); null);
@@ -1,15 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1"></PackageReference> <None Include="..\CryptoExchange.Net\.editorconfig" Link=".editorconfig" />
</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.4.0"></PackageReference> <PackageReference Include="NUnit" Version="4.3.2"></PackageReference>
<PackageReference Include="NUnit3TestAdapter" Version="6.0.1"></PackageReference> <PackageReference Include="NUnit3TestAdapter" Version="5.0.0"></PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -1,5 +1,7 @@
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
@@ -30,7 +32,6 @@ 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);
@@ -1,465 +0,0 @@
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,8 +1,15 @@
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,6 +9,7 @@ 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;
@@ -81,25 +82,6 @@ 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()
{ {
@@ -1,633 +0,0 @@
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));
}
}
}
@@ -1,400 +0,0 @@
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));
}
}
}
+200 -203
View File
@@ -1,234 +1,231 @@
//using CryptoExchange.Net.Objects; using System;
//using CryptoExchange.Net.Objects.Sockets; using System.Collections.Generic;
//using CryptoExchange.Net.Sockets; using System.Text.Json;
//using CryptoExchange.Net.Testing.Implementations; using System.Threading;
//using CryptoExchange.Net.UnitTests.TestImplementations; using System.Threading.Tasks;
//using CryptoExchange.Net.UnitTests.TestImplementations.Sockets; using CryptoExchange.Net.Objects;
//using Microsoft.Extensions.Logging; using CryptoExchange.Net.Objects.Sockets;
//using Moq; using CryptoExchange.Net.Sockets;
//using NUnit.Framework; using CryptoExchange.Net.UnitTests.TestImplementations;
//using NUnit.Framework.Legacy; using CryptoExchange.Net.UnitTests.TestImplementations.Sockets;
//using System; using Microsoft.Extensions.Logging;
//using System.Collections.Generic; using Moq;
//using System.Net.Sockets; using NUnit.Framework;
//using System.Text.Json; using NUnit.Framework.Legacy;
//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( var connectResult = client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, null));
// 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(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// var rstEvent = new ManualResetEvent(false); var rstEvent = new ManualResetEvent(false);
// Dictionary<string, string> result = null; Dictionary<string, string> result = null;
// 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(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// var rstEvent = new ManualResetEvent(false); var rstEvent = new ManualResetEvent(false);
// string original = null; string original = null;
// 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(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// 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(), new TestWebsocketFactory(socket1), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket1, null);
// var sub2 = new SocketConnection(new TraceLogger(), new TestWebsocketFactory(socket2), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub2 = new SocketConnection(new TraceLogger(), client.SubClient, socket2, null);
// 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(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, ""); var sub1 = new SocketConnection(new TraceLogger(), client.SubClient, socket, null);
// // 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(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "")); client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
// // 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.IsTrue(client.SubClient.TestSubscription.Status != SubscriptionStatus.Subscribed); ClassicAssert.IsFalse(client.SubClient.TestSubscription.Confirmed);
// } }
// [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(), new TestWebsocketFactory(socket), new WebSocketParameters(new Uri("https://localhost/"), ReconnectPolicy.Disabled), client.SubClient, "")); client.SubClient.ConnectSocketSub(new SocketConnection(new TraceLogger(), client.SubClient, socket, "https://test.test"));
// // 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.Status == SubscriptionStatus.Subscribed); Assert.That(client.SubClient.TestSubscription.Confirmed);
// } }
// } }
//} }
@@ -4,7 +4,9 @@ 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
@@ -0,0 +1,50 @@
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();
}
}
}
@@ -0,0 +1,17 @@
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);
}
}
}
@@ -0,0 +1,34 @@
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);
}
}
@@ -0,0 +1,34 @@
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,17 +1,19 @@
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; using System.Text.Json.Serialization;
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;
@@ -44,19 +46,27 @@ 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)
{ {
return new CallResult<T>(JsonSerializer.Deserialize<T>(data)); var stream = new MemoryStream(Encoding.UTF8.GetBytes(data));
var accessor = CreateAccessor();
var valid = accessor.Read(stream, true).Result;
if (!valid)
return new CallResult<T>(new ServerError(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();
@@ -64,8 +74,6 @@ 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)
{ {
} }
@@ -77,14 +85,4 @@ 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,13 +13,12 @@ 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 System.Net.Http.Headers; using CryptoExchange.Net.Objects.Errors;
using CryptoExchange.Net.SharedApis;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
namespace CryptoExchange.Net.UnitTests.TestImplementations namespace CryptoExchange.Net.UnitTests.TestImplementations
{ {
@@ -50,19 +49,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(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream)); response.Setup(c => c.GetResponseStreamAsync()).Returns(Task.FromResult((Stream)responseStream));
var headers = new HttpRequestMessage().Headers; var headers = new Dictionary<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.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); request.Setup(c => c.GetHeaders()).Returns(() => headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory); var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => .Callback<HttpMethod, Uri, int>((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);
@@ -70,8 +69,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<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => .Callback<HttpMethod, Uri, int>((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);
@@ -87,16 +86,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 HttpRequestMessage().Headers); request.Setup(c => c.GetHeaders()).Returns(new KeyValuePair<string, string[]>[0]);
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<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(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<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Returns(request.Object); .Returns(request.Object);
} }
@@ -109,31 +108,29 @@ 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(It.IsAny<CancellationToken>())).Returns(Task.FromResult((Stream)responseStream)); response.Setup(c => c.GetResponseStreamAsync()).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(new HttpRequestMessage().Headers); request.Setup(c => c.GetHeaders()).Returns(headers.ToArray());
var factory = Mock.Get(Api1.RequestFactory); var factory = Mock.Get(Api1.RequestFactory);
factory.Setup(c => c.Create(It.IsAny<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Callback<HttpMethod, Uri, int>((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<Version>(), It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>())) factory.Setup(c => c.Create(It.IsAny<HttpMethod>(), It.IsAny<Uri>(), It.IsAny<int>()))
.Callback<Version, HttpMethod, Uri, int>((version, method, uri, id) => request.Setup(a => a.Uri).Returns(uri)) .Callback<HttpMethod, Uri, int>((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;
@@ -142,6 +139,7 @@ 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
@@ -159,6 +157,11 @@ 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);
@@ -166,17 +169,21 @@ 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 />
@@ -187,6 +194,18 @@ 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);
@@ -195,6 +214,10 @@ namespace CryptoExchange.Net.UnitTests.TestImplementations
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override TimeSyncInfo GetTimeSyncInfo()
{
throw new NotImplementedException();
}
} }
public class TestError public class TestError
@@ -1,32 +0,0 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Converters.SystemTextJson.MessageHandlers;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.UnitTests.TestImplementations
{
internal class TestRestMessageHandler : JsonRestMessageHandler
{
private ErrorMapping _errorMapping = new ErrorMapping([]);
public override JsonSerializerOptions Options => new JsonSerializerOptions();
public override async ValueTask<Error> ParseErrorResponse(int httpStatusCode, HttpResponseHeaders responseHeaders, Stream responseStream)
{
var result = await GetJsonDocument(responseStream).ConfigureAwait(false);
if (result.Item1 != null)
return result.Item1;
var errorData = result.Item2.Deserialize<TestError>();
return new ServerError(errorData.ErrorCode, _errorMapping.GetErrorInfo(errorData.ErrorCode.ToString(), errorData.ErrorMessage));
}
}
}
@@ -0,0 +1,140 @@
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,6 +1,10 @@
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
{ {
+183
View File
@@ -0,0 +1,183 @@
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
+3 -4
View File
@@ -1,6 +1,5 @@
[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,7 +1,7 @@
using System; using System;
namespace CryptoExchange.Net.Attributes;
namespace CryptoExchange.Net.Attributes
{
/// <summary> /// <summary>
/// Used for conversion in ArrayConverter /// Used for conversion in ArrayConverter
/// </summary> /// </summary>
@@ -9,4 +9,3 @@ namespace CryptoExchange.Net.Attributes
public class JsonConversionAttribute: Attribute public class JsonConversionAttribute: Attribute
{ {
} }
}
@@ -1,7 +1,7 @@
using System; using System;
namespace CryptoExchange.Net.Attributes;
namespace CryptoExchange.Net.Attributes
{
/// <summary> /// <summary>
/// Map a enum entry to string values /// Map a enum entry to string values
/// </summary> /// </summary>
@@ -22,4 +22,3 @@ namespace CryptoExchange.Net.Attributes
Values = maps; Values = maps;
} }
} }
}
@@ -1,9 +1,7 @@
using System; using System;
using System.IO;
using System.Threading.Tasks; namespace CryptoExchange.Net.Authentication;
namespace CryptoExchange.Net.Authentication
{
/// <summary> /// <summary>
/// Api credentials, used to sign requests accessing private endpoints /// Api credentials, used to sign requests accessing private endpoints
/// </summary> /// </summary>
@@ -47,48 +45,6 @@ namespace CryptoExchange.Net.Authentication
Pass = pass; 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> /// <summary>
/// Copy the credentials /// Copy the credentials
/// </summary> /// </summary>
@@ -98,4 +54,3 @@ namespace CryptoExchange.Net.Authentication
return new ApiCredentials(Key, Secret, Pass, CredentialType); return new ApiCredentials(Key, Secret, Pass, CredentialType);
} }
} }
}
@@ -1,5 +1,5 @@
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication;
{
/// <summary> /// <summary>
/// Credentials type /// Credentials type
/// </summary> /// </summary>
@@ -16,10 +16,5 @@
/// <summary> /// <summary>
/// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower. /// Rsa keys credentials in pem/base64 format. Only available for .NetStandard 2.1 and up, use xml format for lower.
/// </summary> /// </summary>
RsaPem, RsaPem
/// <summary>
/// Ed25519 keys credentials
/// </summary>
Ed25519
}
} }
@@ -1,21 +1,15 @@
using CryptoExchange.Net.Clients; using CryptoExchange.Net.Clients;
using CryptoExchange.Net.Converters.SystemTextJson; using CryptoExchange.Net.Converters.SystemTextJson;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
#if NET8_0_OR_GREATER
using NSec.Cryptography;
#endif
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Globalization; using System.Globalization;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication;
{
/// <summary> /// <summary>
/// Base class for authentication providers /// Base class for authentication providers
/// </summary> /// </summary>
@@ -23,11 +17,6 @@ namespace CryptoExchange.Net.Authentication
{ {
internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider(); internal IAuthTimeProvider TimeProvider { get; set; } = new AuthTimeProvider();
/// <summary>
/// The supported credential types
/// </summary>
public abstract ApiCredentialsType[] SupportedCredentialTypes { get; }
/// <summary> /// <summary>
/// Provided credentials /// Provided credentials
/// </summary> /// </summary>
@@ -38,13 +27,6 @@ namespace CryptoExchange.Net.Authentication
/// </summary> /// </summary>
protected byte[] _sBytes; protected byte[] _sBytes;
#if NET8_0_OR_GREATER
/// <summary>
/// The Ed25519 private key
/// </summary>
protected Key? Ed25519Key;
#endif
/// <summary> /// <summary>
/// Get the API key of the current credentials /// Get the API key of the current credentials
/// </summary> /// </summary>
@@ -63,35 +45,17 @@ namespace CryptoExchange.Net.Authentication
if (credentials.Key == null || credentials.Secret == null) if (credentials.Key == null || credentials.Secret == null)
throw new ArgumentException("ApiKey/Secret needed"); throw new ArgumentException("ApiKey/Secret needed");
if (!SupportedCredentialTypes.Any(x => x == credentials.CredentialType))
throw new ArgumentException($"Credential type {credentials.CredentialType} not supported");
if (credentials.CredentialType == ApiCredentialsType.Ed25519)
{
#if !NET8_0_OR_GREATER
throw new ArgumentException($"Credential type Ed25519 only supported on Net8.0 or newer");
#endif
}
_credentials = credentials; _credentials = credentials;
_sBytes = Encoding.UTF8.GetBytes(credentials.Secret); _sBytes = Encoding.UTF8.GetBytes(credentials.Secret);
} }
/// <summary> /// <summary>
/// Authenticate a REST request /// Authenticate a request
/// </summary> /// </summary>
/// <param name="apiClient">The API client sending the request</param> /// <param name="apiClient">The Api client sending the request</param>
/// <param name="requestConfig">The request configuration</param> /// <param name="requestConfig">The request configuration</param>
public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig); public abstract void ProcessRequest(RestApiClient apiClient, RestRequestConfiguration requestConfig);
/// <summary>
/// Get an authentication query for a websocket
/// </summary>
/// <param name="apiClient">The API client sending the request</param>
/// <param name="connection">The connection to authenticate</param>
/// <param name="context">Optional context required for creating the authentication query</param>
public virtual Query? GetAuthenticationQuery(SocketApiClient apiClient, SocketConnection connection, Dictionary<string, object?>? context = null) => null;
/// <summary> /// <summary>
/// SHA256 sign the data and return the bytes /// SHA256 sign the data and return the bytes
/// </summary> /// </summary>
@@ -244,7 +208,9 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected static string SignMD5(string data, SignOutputType? outputType = null) protected static string SignMD5(string data, SignOutputType? outputType = null)
{ {
#pragma warning disable CA5351
using var encryptor = MD5.Create(); using var encryptor = MD5.Create();
#pragma warning restore CA5351
var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); var resultBytes = encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
@@ -257,7 +223,9 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected static string SignMD5(byte[] data, SignOutputType? outputType = null) protected static string SignMD5(byte[] data, SignOutputType? outputType = null)
{ {
#pragma warning disable CA5351
using var encryptor = MD5.Create(); using var encryptor = MD5.Create();
#pragma warning restore CA5351
var resultBytes = encryptor.ComputeHash(data); var resultBytes = encryptor.ComputeHash(data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
@@ -269,7 +237,9 @@ namespace CryptoExchange.Net.Authentication
/// <returns></returns> /// <returns></returns>
protected static byte[] SignMD5Bytes(string data) protected static byte[] SignMD5Bytes(string data)
{ {
#pragma warning disable CA5351
using var encryptor = MD5.Create(); using var encryptor = MD5.Create();
#pragma warning restore CA5351
return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data)); return encryptor.ComputeHash(Encoding.UTF8.GetBytes(data));
} }
@@ -384,36 +354,6 @@ namespace CryptoExchange.Net.Authentication
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes); return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
} }
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(string data, SignOutputType? outputType = null)
=> SignEd25519(Encoding.ASCII.GetBytes(data), outputType);
/// <summary>
/// Ed25519 sign the data
/// </summary>
public string SignEd25519(byte[] data, SignOutputType? outputType = null)
{
#if NET8_0_OR_GREATER
if (Ed25519Key == null)
{
var key = _credentials.Secret!
.Replace("\n", "")
.Replace("-----BEGIN PRIVATE KEY-----", "")
.Replace("-----END PRIVATE KEY-----", "")
.Trim();
var keyBytes = Convert.FromBase64String(key);
Ed25519Key = Key.Import(SignatureAlgorithm.Ed25519, keyBytes, KeyBlobFormat.PkixPrivateKey);
}
var resultBytes = SignatureAlgorithm.Ed25519.Sign(Ed25519Key, data);
return outputType == SignOutputType.Base64 ? BytesToBase64String(resultBytes) : BytesToHexString(resultBytes);
#else
throw new InvalidOperationException();
#endif
}
private RSA CreateRSA() private RSA CreateRSA()
{ {
var rsa = RSA.Create(); var rsa = RSA.Create();
@@ -452,14 +392,6 @@ namespace CryptoExchange.Net.Authentication
/// <param name="buff"></param> /// <param name="buff"></param>
/// <returns></returns> /// <returns></returns>
protected static string BytesToHexString(byte[] buff) protected static string BytesToHexString(byte[] buff)
=> BytesToHexString(new ArraySegment<byte>(buff));
/// <summary>
/// Convert byte array to hex string
/// </summary>
/// <param name="buff"></param>
/// <returns></returns>
protected static string BytesToHexString(ArraySegment<byte> buff)
{ {
#if NET9_0_OR_GREATER #if NET9_0_OR_GREATER
return Convert.ToHexString(buff); return Convert.ToHexString(buff);
@@ -471,26 +403,6 @@ namespace CryptoExchange.Net.Authentication
#endif #endif
} }
/// <summary>
/// Convert a hex encoded string to byte array
/// </summary>
/// <param name="hexString"></param>
/// <returns></returns>
protected static byte[] HexToBytesString(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;
}
/// <summary> /// <summary>
/// Convert byte array to base64 string /// Convert byte array to base64 string
/// </summary> /// </summary>
@@ -504,53 +416,32 @@ namespace CryptoExchange.Net.Authentication
/// <summary> /// <summary>
/// Get current timestamp including the time sync offset from the api client /// Get current timestamp including the time sync offset from the api client
/// </summary> /// </summary>
protected DateTime GetTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true) /// <param name="apiClient"></param>
/// <returns></returns>
protected DateTime GetTimestamp(RestApiClient apiClient)
{ {
var result = TimeProvider.GetTime().Add(TimeOffsetManager.GetRestOffset(apiClient.ClientName) ?? TimeSpan.Zero)!; return TimeProvider.GetTime().Add(apiClient.GetTimeOffset() ?? TimeSpan.Zero)!;
if (includeOneSecondOffset)
result = result.AddSeconds(-1);
return result;
}
/// <summary>
/// Get current timestamp including the time sync offset from the api client
/// </summary>
protected DateTime GetTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
{
var timestamp = TimeProvider.GetTime();
if(apiClient.ApiOptions.AutoTimestamp ?? apiClient.ClientOptions.AutoTimestamp)
timestamp = timestamp.Add(-TimeOffsetManager.GetSocketOffset(apiClient.ClientName) ?? TimeSpan.Zero)!;
if (includeOneSecondOffset)
timestamp = timestamp.AddSeconds(-1);
return timestamp;
} }
/// <summary> /// <summary>
/// Get millisecond timestamp as a string including the time sync offset from the api client /// Get millisecond timestamp as a string including the time sync offset from the api client
/// </summary> /// </summary>
protected string GetMillisecondTimestamp(RestApiClient apiClient, bool includeOneSecondOffset = true) /// <param name="apiClient"></param>
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture); /// <returns></returns>
protected string GetMillisecondTimestamp(RestApiClient apiClient)
/// <summary> {
/// Get millisecond timestamp as a string including the time sync offset from the api client return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value.ToString(CultureInfo.InvariantCulture);
/// </summary> }
protected string GetMillisecondTimestamp(SocketApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value.ToString(CultureInfo.InvariantCulture);
/// <summary> /// <summary>
/// Get millisecond timestamp as a long including the time sync offset from the api client /// Get millisecond timestamp as a long including the time sync offset from the api client
/// </summary> /// </summary>
protected long GetMillisecondTimestampLong(RestApiClient apiClient, bool includeOneSecondOffset = true) /// <param name="apiClient"></param>
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value; /// <returns></returns>
protected long GetMillisecondTimestampLong(RestApiClient apiClient)
/// <summary> {
/// Get millisecond timestamp as a long including the time sync offset from the api client return DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient)).Value;
/// </summary> }
protected long GetMillisecondTimestampLong(SocketApiClient apiClient, bool includeOneSecondOffset = true)
=> DateTimeConverter.ConvertToMilliseconds(GetTimestamp(apiClient, includeOneSecondOffset)).Value;
/// <summary> /// <summary>
/// Return the serialized request body /// Return the serialized request body
@@ -563,7 +454,7 @@ namespace CryptoExchange.Net.Authentication
if (serializer is not IStringMessageSerializer stringSerializer) if (serializer is not IStringMessageSerializer stringSerializer)
throw new InvalidOperationException("Non-string message serializer can't get serialized request body"); throw new InvalidOperationException("Non-string message serializer can't get serialized request body");
if (parameters?.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value)) if (parameters.Count == 1 && parameters.TryGetValue(Constants.BodyPlaceHolderKey, out object? value))
return stringSerializer.Serialize(value); return stringSerializer.Serialize(value);
else else
return stringSerializer.Serialize(parameters); return stringSerializer.Serialize(parameters);
@@ -574,7 +465,11 @@ namespace CryptoExchange.Net.Authentication
public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials public abstract class AuthenticationProvider<TApiCredentials> : AuthenticationProvider where TApiCredentials : ApiCredentials
{ {
/// <inheritdoc /> /// <inheritdoc />
#pragma warning disable IDE1006 // Naming Styles
#pragma warning disable CA1707 // Naming Styles
protected new TApiCredentials _credentials => (TApiCredentials)base._credentials; protected new TApiCredentials _credentials => (TApiCredentials)base._credentials;
#pragma warning restore IDE1006 // Naming Styles
#pragma warning restore CA1707 // Naming Styles
/// <summary> /// <summary>
/// ctor /// ctor
@@ -584,4 +479,3 @@ namespace CryptoExchange.Net.Authentication
{ {
} }
} }
}
@@ -1,5 +1,5 @@
namespace CryptoExchange.Net.Authentication namespace CryptoExchange.Net.Authentication;
{
/// <summary> /// <summary>
/// Output string type /// Output string type
/// </summary> /// </summary>
@@ -14,4 +14,3 @@
/// </summary> /// </summary>
Base64 Base64
} }
}
+3 -9
View File
@@ -1,18 +1,13 @@
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 internal class MemoryCache
{ {
private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>(); private readonly ConcurrentDictionary<string, CacheItem> _cache = new ConcurrentDictionary<string, CacheItem>();
#if NET9_0_OR_GREATER
private readonly Lock _lock = new Lock();
#else
private readonly object _lock = new object(); private readonly object _lock = new object();
#endif
/// <summary> /// <summary>
/// Add a new cache entry. Will override an existing entry if it already exists /// Add a new cache entry. Will override an existing entry if it already exists
@@ -55,4 +50,3 @@ namespace CryptoExchange.Net.Caching
} }
} }
} }
}
+13 -25
View File
@@ -1,23 +1,18 @@
using System; using System;
using CryptoExchange.Net.Authentication; using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces;
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> /// <summary>
/// Base API for all API clients /// Base API for all API clients
/// </summary> /// </summary>
public abstract class BaseApiClient : IDisposable, IBaseApiClient public abstract class BaseApiClient : IDisposable, IBaseApiClient
{ {
/// <summary>
/// Client name
/// </summary>
protected string? _clientName;
/// <summary> /// <summary>
/// Logger /// Logger
/// </summary> /// </summary>
@@ -28,21 +23,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected bool _disposing; protected bool _disposing;
/// <summary>
/// Name of the client
/// </summary>
protected internal string ClientName
{
get
{
if (_clientName != null)
return _clientName;
_clientName = GetType().Name;
return _clientName;
}
}
/// <summary> /// <summary>
/// The authentication provider for this API client. (null if no credentials are set) /// The authentication provider for this API client. (null if no credentials are set)
/// </summary> /// </summary>
@@ -144,9 +124,17 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public virtual void Dispose() public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose(bool disposing)
{ {
_disposing = true; _disposing = true;
} }
} }
}
+19 -16
View File
@@ -1,12 +1,11 @@
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> /// <summary>
/// The base for all clients, websocket client and rest client /// The base for all clients, websocket client and rest client
/// </summary> /// </summary>
@@ -39,11 +38,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
public string Exchange { get; } public string Exchange { get; }
/// <summary>
/// Whether client is disposed
/// </summary>
public bool Disposed { get; private set; }
/// <summary> /// <summary>
/// Api clients in this client /// Api clients in this client
/// </summary> /// </summary>
@@ -54,11 +48,7 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected internal ILogger _logger; protected internal ILogger _logger;
#if NET9_0_OR_GREATER
private readonly Lock _versionLock = new Lock();
#else
private readonly object _versionLock = new object(); private readonly object _versionLock = new object();
#endif
private Version _exchangeVersion; private Version _exchangeVersion;
/// <summary> /// <summary>
@@ -89,7 +79,7 @@ namespace CryptoExchange.Net.Clients
throw new ArgumentNullException(nameof(options)); throw new ArgumentNullException(nameof(options));
ClientOptions = options; ClientOptions = options;
_logger.Log(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{CryptoExchangeLibVersion}, {Exchange}.Net: v{ExchangeLibVersion}"); _logger.Log(LogLevel.Trace, "Client configuration: {Options}, CryptoExchange.Net: v{CryptoExchangeVersion}, {Exchange}.Net: v{ExchangeVersion}", options, CryptoExchangeLibVersion, Exchange, ExchangeLibVersion);
} }
/// <summary> /// <summary>
@@ -111,6 +101,7 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions == null) if (ClientOptions == null)
throw new InvalidOperationException("Client should have called Initialize before adding API clients"); 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); ApiClients.Add(apiClient);
return apiClient; return apiClient;
} }
@@ -128,12 +119,24 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public virtual void Dispose() public void Dispose()
{ {
Disposed = true; Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose
/// </summary>
public virtual void Dispose(bool disposing)
{
if (disposing)
{
_logger.Log(LogLevel.Debug, "Disposing client");
foreach (var client in ApiClients) foreach (var client in ApiClients)
client.Dispose(); client.Dispose();
} }
} }
} }
+3 -6
View File
@@ -1,10 +1,10 @@
using System.Linq; using System.Linq;
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces;
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> /// <summary>
/// Base rest client /// Base rest client
/// </summary> /// </summary>
@@ -21,8 +21,5 @@ namespace CryptoExchange.Net.Clients
protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name) protected BaseRestClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{ {
_logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name); _logger = loggerFactory?.CreateLogger(name + ".RestClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
}
} }
} }
+7 -14
View File
@@ -1,17 +1,16 @@
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> /// <summary>
/// Base for socket client implementations /// Base for socket client implementations
/// </summary> /// </summary>
@@ -30,9 +29,6 @@ namespace CryptoExchange.Net.Clients
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions); public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
/// <inheritdoc /> /// <inheritdoc />
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps); public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
/// <inheritdoc />
public new SocketExchangeOptions ClientOptions => (SocketExchangeOptions)base.ClientOptions;
#endregion #endregion
/// <summary> /// <summary>
@@ -43,8 +39,6 @@ namespace CryptoExchange.Net.Clients
protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name) protected BaseSocketClient(ILoggerFactory? loggerFactory, string name) : base(loggerFactory, name)
{ {
_logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name); _logger = loggerFactory?.CreateLogger(name + ".SocketClient") ?? NullLoggerFactory.Instance.CreateLogger(name);
LibraryHelpers.StaticLogger = loggerFactory?.CreateLogger("CryptoExchange");
} }
/// <summary> /// <summary>
@@ -134,4 +128,3 @@ namespace CryptoExchange.Net.Clients
return result; return result;
} }
} }
}
+15 -4
View File
@@ -1,9 +1,9 @@
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> /// <summary>
/// Base crypto client /// Base crypto client
/// </summary> /// </summary>
@@ -59,9 +59,20 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Dispose /// Dispose
/// </summary> /// </summary>
public void Dispose() public void Dispose(bool disposing)
{
if (disposing)
{ {
_serviceCache.Clear(); _serviceCache.Clear();
} }
} }
/// <summary>
/// Dispose
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
} }
@@ -1,8 +1,8 @@
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces;
using System; using System;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
{
/// <inheritdoc /> /// <inheritdoc />
public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient public class CryptoRestClient : CryptoBaseClient, ICryptoRestClient
{ {
@@ -21,4 +21,3 @@ namespace CryptoExchange.Net.Clients
{ {
} }
} }
}
@@ -1,8 +1,8 @@
using CryptoExchange.Net.Interfaces.Clients; using CryptoExchange.Net.Interfaces;
using System; using System;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
{
/// <inheritdoc /> /// <inheritdoc />
public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient public class CryptoSocketClient : CryptoBaseClient, ICryptoSocketClient
{ {
@@ -21,4 +21,3 @@ namespace CryptoExchange.Net.Clients
{ {
} }
} }
}
+179 -234
View File
@@ -1,7 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CryptoExchange.Net.Caching; using CryptoExchange.Net.Caching;
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Errors;
@@ -10,20 +17,9 @@ using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Requests; using CryptoExchange.Net.Requests;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
{
/// <summary> /// <summary>
/// Base rest API client for interacting with a REST API /// Base rest API client for interacting with a REST API
/// </summary> /// </summary>
@@ -32,6 +28,12 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc /> /// <inheritdoc />
public IRequestFactory RequestFactory { get; set; } = new RequestFactory(); public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
/// <inheritdoc />
public abstract TimeSyncInfo? GetTimeSyncInfo();
/// <inheritdoc />
public abstract TimeSpan? GetTimeOffset();
/// <inheritdoc /> /// <inheritdoc />
public int TotalRequestsMade { get; set; } public int TotalRequestsMade { get; set; }
@@ -88,11 +90,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
private readonly static MemoryCache _cache = new MemoryCache(); private readonly static MemoryCache _cache = new MemoryCache();
/// <summary>
/// The message handler
/// </summary>
protected abstract IRestMessageHandler MessageHandler { get; }
/// <summary> /// <summary>
/// ctor /// ctor
/// </summary> /// </summary>
@@ -109,11 +106,15 @@ namespace CryptoExchange.Net.Clients
options, options,
apiOptions) apiOptions)
{ {
TimeOffsetManager.RegisterRestApi(ClientName); RequestFactory.Configure(options.Proxy, options.RequestTimeout, httpClient);
RequestFactory.Configure(options, httpClient);
} }
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected abstract IStreamMessageAccessor CreateAccessor();
/// <summary> /// <summary>
/// Create a serializer instance /// Create a serializer instance
/// </summary> /// </summary>
@@ -203,13 +204,6 @@ namespace CryptoExchange.Net.Clients
int? weightSingleLimiter = null, int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null) string? rateLimitKeySuffix = null)
{ {
var requestId = ExchangeHelpers.NextId();
if (definition.Authenticated && AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, definition.Path);
return new WebCallResult<T>(new NoApiCredentialsError());
}
string? cacheKey = null; string? cacheKey = null;
if (ShouldCache(definition)) if (ShouldCache(definition))
{ {
@@ -230,19 +224,11 @@ namespace CryptoExchange.Net.Clients
while (true) while (true)
{ {
currentTry++; currentTry++;
var requestId = ExchangeHelpers.NextId();
await CheckTimeSync(requestId, definition).ConfigureAwait(false); var prepareResult = await PrepareAsync(requestId, baseAddress, definition, cancellationToken, additionalHeaders, weight, weightSingleLimiter, rateLimitKeySuffix).ConfigureAwait(false);
if (!prepareResult)
var error = await RateLimitAsync( return new WebCallResult<T>(prepareResult.Error!);
baseAddress,
requestId,
definition,
weight ?? definition.Weight,
cancellationToken,
weightSingleLimiter,
rateLimitKeySuffix).ConfigureAwait(false);
if (error != null)
return new WebCallResult<T>(error);
var request = CreateRequest( var request = CreateRequest(
requestId, requestId,
@@ -251,25 +237,17 @@ namespace CryptoExchange.Net.Clients
uriParameters, uriParameters,
bodyParameters, bodyParameters,
additionalHeaders); additionalHeaders);
if (_logger.IsEnabled(LogLevel.Debug))
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); _logger.RestApiSendRequest(request.RequestId, definition, request.Content, string.IsNullOrEmpty(request.Uri.Query) ? "-" : request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++; TotalRequestsMade++;
var result = await GetResponseAsync<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
var result = await GetResponseAsync2<T>(definition, request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
if (result.Error is not CancellationRequestedError) if (result.Error is not CancellationRequestedError)
{ {
var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]"; var originalData = OutputOriginalData ? result.OriginalData : "[Data only available when OutputOriginal = true]";
if (!result) if (!result)
{
_logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception); _logger.RestApiErrorReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), result.Error?.ToString(), originalData, result.Error?.Exception);
}
else else
{
if (_logger.IsEnabled(LogLevel.Debug))
_logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData); _logger.RestApiResponseReceived(result.RequestId, result.ResponseStatusCode, (long)Math.Floor(result.ResponseTime!.Value.TotalMilliseconds), originalData);
} }
}
else else
{ {
_logger.RestApiCancellationRequested(result.RequestId); _logger.RestApiCancellationRequested(result.RequestId);
@@ -289,19 +267,54 @@ namespace CryptoExchange.Net.Clients
} }
/// <summary> /// <summary>
/// Check rate limits for the request /// Prepare before sending a request. Sync time between client and server and check rate limits
/// </summary> /// </summary>
protected virtual async ValueTask<Error?> RateLimitAsync( /// <param name="requestId">Request id</param>
string host, /// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param>
/// <param name="weightSingleLimiter">Specify the weight to apply to the individual rate limit guard for this request</param>
/// <param name="rateLimitKeySuffix">An additional optional suffix for the key selector</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
protected virtual async Task<CallResult> PrepareAsync(
int requestId, int requestId,
string baseAddress,
RequestDefinition definition, RequestDefinition definition,
int weight,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null,
int? weightSingleLimiter = null, int? weightSingleLimiter = null,
string? rateLimitKeySuffix = null) string? rateLimitKeySuffix = null)
{ {
// Time sync
if (definition.Authenticated)
{
if (AuthenticationProvider == null)
{
_logger.RestApiNoApiCredentials(requestId, definition.Path);
return new CallResult<IRequest>(new NoApiCredentialsError());
}
var syncTask = SyncTimeAsync();
var timeSyncInfo = GetTimeSyncInfo();
if (timeSyncInfo != null && timeSyncInfo.TimeSyncState.LastSyncTime == default)
{
// Initially with first request we'll need to wait for the time syncing, if it's not the first request we can just continue
var syncTimeResult = await syncTask.ConfigureAwait(false);
if (!syncTimeResult)
{
_logger.RestApiFailedToSyncTime(requestId, syncTimeResult.Error!.ToString());
return syncTimeResult.AsDataless();
}
}
}
// Rate limiting // Rate limiting
var requestWeight = weight; var requestWeight = weight ?? definition.Weight;
if (requestWeight != 0) if (requestWeight != 0)
{ {
if (definition.RateLimitGate == null) if (definition.RateLimitGate == null)
@@ -309,9 +322,9 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); var limitResult = await definition.RateLimitGate.ProcessAsync(_logger, requestId, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, requestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return limitResult.Error!; return new CallResult(limitResult.Error!);
} }
} }
@@ -324,13 +337,13 @@ namespace CryptoExchange.Net.Clients
if (ClientOptions.RateLimiterEnabled) if (ClientOptions.RateLimiterEnabled)
{ {
var singleRequestWeight = weightSingleLimiter ?? 1; var singleRequestWeight = weightSingleLimiter ?? 1;
var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, host, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false); var limitResult = await definition.RateLimitGate.ProcessSingleAsync(_logger, requestId, definition.LimitGuard, RateLimitItemType.Request, definition, baseAddress, AuthenticationProvider?._credentials.Key, singleRequestWeight, ClientOptions.RateLimitingBehaviour, rateLimitKeySuffix, cancellationToken).ConfigureAwait(false);
if (!limitResult) if (!limitResult)
return limitResult.Error!; return new CallResult(limitResult.Error!);
} }
} }
return null; return CallResult.SuccessResult;
} }
/// <summary> /// <summary>
@@ -354,9 +367,9 @@ namespace CryptoExchange.Net.Clients
var requestConfiguration = new RestRequestConfiguration( var requestConfiguration = new RestRequestConfiguration(
definition, definition,
baseAddress, baseAddress,
uriParameters == null ? null : CreateParameterDictionary(uriParameters), uriParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(uriParameters),
bodyParameters == null ? null : CreateParameterDictionary(bodyParameters), bodyParameters == null ? new Dictionary<string, object>() : CreateParameterDictionary(bodyParameters),
additionalHeaders, new Dictionary<string, string>(additionalHeaders ?? []),
definition.ArraySerialization ?? ArraySerialization, definition.ArraySerialization ?? ArraySerialization,
definition.ParameterPosition ?? ParameterPositions[definition.Method], definition.ParameterPosition ?? ParameterPositions[definition.Method],
definition.RequestBodyFormat ?? RequestBodyFormat); definition.RequestBodyFormat ?? RequestBodyFormat);
@@ -375,19 +388,15 @@ namespace CryptoExchange.Net.Clients
queryString = $"?{queryString}"; queryString = $"?{queryString}";
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString); var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId); var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = MessageHandler.AcceptHeader; request.Accept = Constants.JsonContentHeader;
if (requestConfiguration.Headers != null)
{
foreach (var header in requestConfiguration.Headers) foreach (var header in requestConfiguration.Headers)
request.AddHeader(header.Key, header.Value); request.AddHeader(header.Key, header.Value);
}
foreach (var header in StandardRequestHeaders) foreach (var header in StandardRequestHeaders)
{ {
// Only add it if it isn't overwritten // Only add it if it isn't overwritten
requestConfiguration.Headers ??= new Dictionary<string, string>();
if (!requestConfiguration.Headers.ContainsKey(header.Key)) if (!requestConfiguration.Headers.ContainsKey(header.Key))
request.AddHeader(header.Key, header.Value); request.AddHeader(header.Key, header.Value);
} }
@@ -420,7 +429,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="gate">The ratelimit gate used</param> /// <param name="gate">The ratelimit gate used</param>
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<WebCallResult<T>> GetResponseAsync2<T>( protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
RequestDefinition requestDefinition, RequestDefinition requestDefinition,
IRequest request, IRequest request,
IRateLimitGate? gate, IRateLimitGate? gate,
@@ -429,41 +438,27 @@ namespace CryptoExchange.Net.Clients
var sw = Stopwatch.StartNew(); var sw = Stopwatch.StartNew();
Stream? responseStream = null; Stream? responseStream = null;
IResponse? response = null; IResponse? response = null;
IStreamMessageAccessor? accessor = null;
try try
{ {
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false); response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop(); sw.Stop();
responseStream = await response.GetResponseStreamAsync(cancellationToken).ConfigureAwait(false); var statusCode = response.StatusCode;
string? originalData = null; var headers = response.ResponseHeaders;
var responseLength = response.ContentLength;
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData; var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
if (outputOriginalData || MessageHandler.RequiresSeekableStream || !response.IsSuccessStatusCode)
{
// Create a seekable stream from the response stream if:
// 1. We need to output the original data
// 2. The message handler requires a seekable stream
// 3. The response indicates error and we want to output (part of) the returned data
responseStream = await CopyStreamAsync(responseStream).ConfigureAwait(false);
using var reader = new StreamReader(responseStream, Encoding.UTF8, false, 4096, true);
if (outputOriginalData)
{
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
responseStream.Position = 0;
}
}
accessor = CreateAccessor();
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess) if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
{ {
// If the response status is not success it is an error by definition // Error response
var readResult = await accessor.Read(responseStream, true).ConfigureAwait(false);
Error error; Error error;
if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429) if (response.StatusCode == (HttpStatusCode)418 || response.StatusCode == (HttpStatusCode)429)
{ {
// Specifically handle rate limit errors var rateError = ParseRateLimitResponse((int)response.StatusCode, response.ResponseHeaders, accessor);
var rateError = await MessageHandler.ParseErrorRateLimitResponse(
(int)response.StatusCode,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled) if (rateError.RetryAfter != null && gate != null && ClientOptions.RateLimiterEnabled)
{ {
_logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value); _logger.RestApiRateLimitPauseUntil(request.RequestId, rateError.RetryAfter.Value);
@@ -474,34 +469,28 @@ namespace CryptoExchange.Net.Clients
} }
else else
{ {
// Handle a 'normal' error response. Can still be either a json error message or some random HTML or other string error = ParseErrorResponse((int)response.StatusCode, response.ResponseHeaders, accessor, readResult.Error?.Exception);
try
{
error = await MessageHandler.ParseErrorResponse(
(int)response.StatusCode,
response.ResponseHeaders,
responseStream).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when parsing error response: {Message}", ex.Message);
var errorResult = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, errorResult);
}
} }
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); if (error.Code == null || error.Code == 0)
error.Code = (int)response.StatusCode;
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error!);
} }
var valid = await accessor.Read(responseStream, outputOriginalData).ConfigureAwait(false);
if (typeof(T) == typeof(object)) if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct // Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null); return new WebCallResult<T>(statusCode, headers, sw.Elapsed, 0, accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]", request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
// Data response received, inspect the message and check if it is an error or not if (!valid)
var parsedError = await MessageHandler.CheckForErrorResponse( {
requestDefinition, // Invalid json
response.ResponseHeaders, return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, valid.Error);
responseStream).ConfigureAwait(false); }
// Json response received
var parsedError = TryParseError(requestDefinition, response.ResponseHeaders, accessor);
if (parsedError != null) if (parsedError != null)
{ {
if (parsedError is ServerRateLimitError rateError) if (parsedError is ServerRateLimitError rateError)
@@ -514,84 +503,52 @@ namespace CryptoExchange.Net.Clients
} }
// Success status code, but TryParseError determined it was an error response // Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError); return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
} }
if (MessageHandler.RequiresSeekableStream) var deserializeResult = accessor.Deserialize<T>();
// Reset stream read position as it might not be at the start if `CheckForErrorResponse` has read from it return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, responseLength, OutputOriginalData ? accessor.GetOriginalString() : null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult.Data, deserializeResult.Error);
responseStream.Position = 0;
// Try deserialization into the expected type
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, cancellationToken).ConfigureAwait(false);
if (deserializeError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError); ;
try
{
// Check the deserialized response to see if it's an error or not
var responseError = MessageHandler.CheckDeserializedResponse(response.ResponseHeaders, deserializeResult);
if (responseError != null)
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, responseError);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception when checking deserialized response: {Message}", ex.Message);
var error = new ServerError(ErrorInfo.Unknown with { Message = ex.Message });
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, error);
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, null);
} }
catch (HttpRequestException requestException) catch (HttpRequestException requestException)
{ {
// Request exception, can't reach server for instance // Request exception, can't reach server for instance
var error = new WebError(requestException.Message, requestException); var error = new WebError(requestException.Message, requestException);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
} }
catch (OperationCanceledException canceledException) catch (OperationCanceledException canceledException)
{ {
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken) if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
{ {
// Cancellation token canceled by caller // Cancellation token canceled by caller
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException)); return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, new CancellationRequestedError(canceledException));
} }
else else
{ {
// Request timed out // Request timed out
var error = new WebError($"Request timed out", exception: canceledException); var error = new WebError($"Request timed out", exception: canceledException);
error.ErrorType = ErrorType.Timeout; error.ErrorType = ErrorType.Timeout;
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error); return new WebCallResult<T>(null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
} }
} }
catch (ArgumentException argumentException)
{
if (argumentException.Message.StartsWith("Only HTTP/"))
{
// Unsupported HTTP version error .net framework
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + argumentException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
catch (NotSupportedException notSupportedException)
{
if (notSupportedException.Message.StartsWith("Request version value must be one of"))
{
// Unsupported HTTP version error dotnet code
var error = ArgumentError.Invalid(nameof(RestExchangeOptions.HttpVersion), $"Invalid HTTP version {request.HttpVersion}: " + notSupportedException.Message);
return new WebCallResult<T>(null, null, null, sw.Elapsed, null, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
throw;
}
finally finally
{ {
accessor?.Clear();
responseStream?.Close(); responseStream?.Close();
response?.Close(); response?.Close();
} }
} }
/// <summary>
/// Can be used to parse an error even though response status indicates success. Some apis always return 200 OK, even though there is an error.
/// This method will be called for each response to be able to check if the response is an error or not.
/// If the response is an error this method should return the parsed error, else it should return null
/// </summary>
/// <param name="requestDefinition">Request definition</param>
/// <param name="accessor">Data accessor</param>
/// <param name="responseHeaders">The response headers</param>
/// <returns>Null if not an error, Error otherwise</returns>
protected virtual Error? TryParseError(RequestDefinition requestDefinition, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor) => null;
/// <summary> /// <summary>
/// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever. /// Can be used to indicate that a request should be retried. Defaults to false. Make sure to retry a max number of times (based on the the tries parameter) or the request will retry forever.
/// Note that this is always called; even when the request might be successful /// Note that this is always called; even when the request might be successful
@@ -601,7 +558,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="callResult">The result of the call</param> /// <param name="callResult">The result of the call</param>
/// <param name="tries">The current try number</param> /// <param name="tries">The current try number</param>
/// <returns>True if call should retry, false if the call should return</returns> /// <returns>True if call should retry, false if the call should return</returns>
protected virtual async ValueTask<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries) protected virtual async Task<bool> ShouldRetryRequestAsync<T>(IRateLimitGate? gate, WebCallResult<T> callResult, int tries)
{ {
if (tries >= 2) if (tries >= 2)
// Only retry once // Only retry once
@@ -656,6 +613,43 @@ namespace CryptoExchange.Net.Clients
} }
} }
/// <summary>
/// Parse an error response from the server. Only used when server returns a status other than Success(200) or ratelimit error (429 or 418)
/// </summary>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="accessor">Data accessor</param>
/// <param name="exception">Exception</param>
/// <returns></returns>
protected virtual Error ParseErrorResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor, Exception? exception)
{
return new ServerError(ErrorInfo.Unknown, exception);
}
/// <summary>
/// Parse a rate limit error response from the server. Only used when server returns http status 429 or 418
/// </summary>
/// <param name="httpStatusCode">The response status code</param>
/// <param name="responseHeaders">The response headers</param>
/// <param name="accessor">Data accessor</param>
/// <returns></returns>
protected virtual ServerRateLimitError ParseRateLimitResponse(int httpStatusCode, KeyValuePair<string, string[]>[] responseHeaders, IMessageAccessor accessor)
{
// Handle retry after header
var retryAfterHeader = responseHeaders.SingleOrDefault(r => r.Key.Equals("Retry-After", StringComparison.InvariantCultureIgnoreCase));
if (!(retryAfterHeader.Value.Length > 0))
return new ServerRateLimitError();
var value = retryAfterHeader.Value.First();
if (int.TryParse(value, out var seconds))
return new ServerRateLimitError() { RetryAfter = DateTime.UtcNow.AddSeconds(seconds) };
if (DateTime.TryParse(value, out var datetime))
return new ServerRateLimitError() { RetryAfter = datetime };
return new ServerRateLimitError();
}
/// <summary> /// <summary>
/// Create the parameter IDictionary /// Create the parameter IDictionary
/// </summary> /// </summary>
@@ -680,56 +674,29 @@ namespace CryptoExchange.Net.Clients
{ {
base.SetOptions(options); base.SetOptions(options);
RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout, ClientOptions.HttpKeepAliveInterval); RequestFactory.UpdateSettings(options.Proxy, options.RequestTimeout ?? ClientOptions.RequestTimeout);
} }
private async ValueTask CheckTimeSync(int requestId, RequestDefinition definition) internal async Task<WebCallResult<bool>> SyncTimeAsync()
{ {
if (!definition.Authenticated) var timeSyncParams = GetTimeSyncInfo();
return; if (timeSyncParams == null)
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
var lastUpdateTime = TimeOffsetManager.GetRestLastUpdateTime(ClientName); if (await timeSyncParams.TimeSyncState.Semaphore.WaitAsync(0).ConfigureAwait(false))
var syncTask = CheckTimeOffsetAsync();
if (lastUpdateTime == null)
{ {
// Initially with first request we'll need to wait for the time syncing before making the actual request. if (!timeSyncParams.SyncTime || DateTime.UtcNow - timeSyncParams.TimeSyncState.LastSyncTime < timeSyncParams.RecalculationInterval)
// If it's not the first request we can just continue and let it complete in the background {
await syncTask.ConfigureAwait(false); timeSyncParams.TimeSyncState.Semaphore.Release();
return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
} }
return;
}
internal async ValueTask CheckTimeOffsetAsync()
{
if (!(ApiOptions.AutoTimestamp ?? ClientOptions.AutoTimestamp))
// Time syncing not enabled
return;
await TimeOffsetManager.EnterAsync(ClientName).ConfigureAwait(false);
try
{
var lastUpdateTime = TimeOffsetManager.GetRestLastUpdateTime(ClientName);
if (DateTime.UtcNow - lastUpdateTime < (ApiOptions.TimestampRecalculationInterval ?? ClientOptions.TimestampRecalculationInterval))
// Time syncing was recently done
return;
var localTime = DateTime.UtcNow; var localTime = DateTime.UtcNow;
WebCallResult<DateTime> result; var result = await GetServerTimestampAsync().ConfigureAwait(false);
try
{
result = await GetServerTimestampAsync().ConfigureAwait(false);
}
catch (NotImplementedException)
{
throw new ArgumentException("AutoTimestamp is not available for this API");
}
if (!result) if (!result)
{ {
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail"); timeSyncParams.TimeSyncState.Semaphore.Release();
return; return result.As(false);
} }
if (TotalRequestsMade == 1) if (TotalRequestsMade == 1)
@@ -739,44 +706,22 @@ namespace CryptoExchange.Net.Clients
result = await GetServerTimestampAsync().ConfigureAwait(false); result = await GetServerTimestampAsync().ConfigureAwait(false);
if (!result) if (!result)
{ {
_logger.LogWarning("Failed to determine time offset between client and server, timestamping might fail"); timeSyncParams.TimeSyncState.Semaphore.Release();
return; return result.As(false);
} }
} }
// Estimate the offset as the round trip time / 2 // Calculate time offset between local and server
var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2); var offset = result.Data - localTime.AddMilliseconds(result.ResponseTime!.Value.TotalMilliseconds / 2);
if (offset.TotalMilliseconds > 0 && offset.TotalMilliseconds < 500) timeSyncParams.UpdateTimeOffset(offset);
{ timeSyncParams.TimeSyncState.Semaphore.Release();
_logger.LogInformation("{ClientName} Time offset within limits ({Offset}ms), set offset to 0ms", ClientName, Math.Round(offset.TotalMilliseconds));
offset = TimeSpan.Zero;
}
else
{
_logger.LogInformation("{ClientName} Time offset set to {Offset}ms", ClientName, Math.Round(offset.TotalMilliseconds));
} }
TimeOffsetManager.UpdateRestOffset(ClientName, offset.TotalMilliseconds); return new WebCallResult<bool>(null, null, null, null, null, null, null, null, null, null, ResultDataSource.Server, true, null);
}
finally
{
TimeOffsetManager.Release(ClientName);
}
}
private async Task<Stream> CopyStreamAsync(Stream responseStream)
{
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
responseStream.Close();
memoryStream.Position = 0;
return memoryStream;
} }
private bool ShouldCache(RequestDefinition definition) private bool ShouldCache(RequestDefinition definition)
=> ClientOptions.CachingEnabled => ClientOptions.CachingEnabled
&& definition.Method == HttpMethod.Get && definition.Method == HttpMethod.Get
&& !definition.PreventCaching; && !definition.PreventCaching;
}
} }
+133 -297
View File
@@ -1,6 +1,4 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Interfaces.Clients;
using CryptoExchange.Net.Logging.Extensions; using CryptoExchange.Net.Logging.Extensions;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Errors; using CryptoExchange.Net.Objects.Errors;
@@ -9,11 +7,6 @@ using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.RateLimiting; using CryptoExchange.Net.RateLimiting;
using CryptoExchange.Net.RateLimiting.Interfaces; using CryptoExchange.Net.RateLimiting.Interfaces;
using CryptoExchange.Net.Sockets; using CryptoExchange.Net.Sockets;
using CryptoExchange.Net.Sockets.Default;
using CryptoExchange.Net.Sockets.Default.Interfaces;
using CryptoExchange.Net.Sockets.HighPerf;
using CryptoExchange.Net.Sockets.HighPerf.Interfaces;
using CryptoExchange.Net.Sockets.Interfaces;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@@ -24,30 +17,21 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CryptoExchange.Net.Clients namespace CryptoExchange.Net.Clients;
{
/// <summary> /// <summary>
/// Base socket API client for interaction with a websocket API /// Base socket API client for interaction with a websocket API
/// </summary> /// </summary>
public abstract class SocketApiClient : BaseApiClient, ISocketApiClient public abstract class SocketApiClient : BaseApiClient, ISocketApiClient
{ {
#region Fields #region Fields
/// <inheritdoc/> /// <inheritdoc/>
public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory(); public IWebsocketFactory SocketFactory { get; set; } = new WebsocketFactory();
/// <inheritdoc/>
public IHighPerfConnectionFactory? HighPerfConnectionFactory { get; set; }
/// <summary> /// <summary>
/// List of socket connections currently connecting/connected /// List of socket connections currently connecting/connected
/// </summary> /// </summary>
protected internal ConcurrentDictionary<int, SocketConnection> _socketConnections = new(); protected internal ConcurrentDictionary<int, SocketConnection> socketConnections = new();
/// <summary>
/// List of HighPerf socket connections currently connecting/connected
/// </summary>
protected internal ConcurrentDictionary<int, HighPerfSocketConnection> _highPerfSocketConnections = new();
/// <summary> /// <summary>
/// Semaphore used while creating sockets /// Semaphore used while creating sockets
@@ -99,30 +83,35 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
protected bool AllowTopicsOnTheSameConnection { get; set; } = true; protected bool AllowTopicsOnTheSameConnection { get; set; } = true;
/// <summary>
/// Whether to continue processing and forward unparsable messages to handlers
/// </summary>
protected internal bool ProcessUnparsableMessages { get; set; }
/// <inheritdoc /> /// <inheritdoc />
public double IncomingKbps public double IncomingKbps
{ {
get get
{ {
if (_socketConnections.IsEmpty) if (socketConnections.IsEmpty)
return 0; return 0;
return _socketConnections.Sum(s => s.Value.IncomingKbps); return socketConnections.Sum(s => s.Value.IncomingKbps);
} }
} }
/// <inheritdoc /> /// <inheritdoc />
public int CurrentConnections => _socketConnections.Count; public int CurrentConnections => socketConnections.Count;
/// <inheritdoc /> /// <inheritdoc />
public int CurrentSubscriptions public int CurrentSubscriptions
{ {
get get
{ {
if (_socketConnections.IsEmpty) if (socketConnections.IsEmpty)
return 0; return 0;
return _socketConnections.Sum(s => s.Value.UserSubscriptionCount); return socketConnections.Sum(s => s.Value.UserSubscriptionCount);
} }
} }
@@ -132,15 +121,6 @@ namespace CryptoExchange.Net.Clients
/// <inheritdoc /> /// <inheritdoc />
public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions; public new SocketApiOptions ApiOptions => (SocketApiOptions)base.ApiOptions;
/// <summary>
/// The max number of individual subscriptions on a single connection
/// </summary>
public int? MaxIndividualSubscriptionsPerConnection { get; set; }
/// <summary>
/// Whether or not to enforce that sequence number updates are always (lastSequenceNumber + 1)
/// </summary>
public bool EnforceSequenceNumbers { get; set; }
#endregion #endregion
/// <summary> /// <summary>
@@ -160,6 +140,12 @@ namespace CryptoExchange.Net.Clients
{ {
} }
/// <summary>
/// Create a message accessor instance
/// </summary>
/// <returns></returns>
protected internal abstract IByteMessageAccessor CreateAccessor(WebSocketMessageType messageType);
/// <summary> /// <summary>
/// Create a serializer instance /// Create a serializer instance
/// </summary> /// </summary>
@@ -176,24 +162,6 @@ namespace CryptoExchange.Net.Clients
DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth }); DedicatedConnectionConfigs.Add(new DedicatedConnectionConfig() { SocketAddress = url, Authenticated = auth });
} }
/// <summary>
/// Update the timestamp offset between client and server based on the timestamp
/// </summary>
/// <param name="timestamp">Timestamp received from the server</param>
public virtual void UpdateTimeOffset(DateTime timestamp)
{
if (timestamp == default)
return;
TimeOffsetManager.UpdateSocketOffset(ClientName, (DateTime.UtcNow - timestamp).TotalMilliseconds);
}
/// <summary>
/// Get the time offset between client and server
/// </summary>
/// <returns></returns>
public virtual TimeSpan? GetTimeOffset() => TimeOffsetManager.GetSocketOffset(ClientName);
/// <summary> /// <summary>
/// Add a query to periodically send on each connection /// Add a query to periodically send on each connection
/// </summary> /// </summary>
@@ -201,7 +169,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="interval"></param> /// <param name="interval"></param>
/// <param name="queryDelegate"></param> /// <param name="queryDelegate"></param>
/// <param name="callback"></param> /// <param name="callback"></param>
protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<ISocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback) protected virtual void RegisterPeriodicQuery(string identifier, TimeSpan interval, Func<SocketConnection, Query> queryDelegate, Action<SocketConnection, CallResult>? callback)
{ {
PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration PeriodicTaskRegistrations.Add(new PeriodicTaskRegistration
{ {
@@ -241,9 +209,6 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new NoApiCredentialsError()); return new CallResult<UpdateSubscription>(new NoApiCredentialsError());
} }
if (subscription.IndividualSubscriptionCount > MaxIndividualSubscriptionsPerConnection)
return new CallResult<UpdateSubscription>(ArgumentError.Invalid("subscriptions", $"Max number of subscriptions in a single call is {MaxIndividualSubscriptionsPerConnection}"));
SocketConnection socketConnection; SocketConnection socketConnection;
var released = false; var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time. // Wait for a semaphore here, so we only connect 1 socket at a time.
@@ -262,7 +227,7 @@ namespace CryptoExchange.Net.Clients
while (true) while (true)
{ {
// Get a new or existing socket connection // Get a new or existing socket connection
var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, ct, subscription.Topic, subscription.IndividualSubscriptionCount).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, subscription.Authenticated, false, subscription.Topic).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.As<UpdateSubscription>(null); return socketResult.As<UpdateSubscription>(null);
@@ -304,108 +269,45 @@ namespace CryptoExchange.Net.Clients
return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused"))); return new CallResult<UpdateSubscription>(new ServerError(new ErrorInfo(ErrorType.WebsocketPaused, "Socket is paused")));
} }
var subscribeResult = await socketConnection.TrySubscribeAsync(subscription, true, ct).ConfigureAwait(false); var waitEvent = new AsyncResetEvent(false);
if (!subscribeResult) var subQuery = subscription.CreateSubscriptionQuery(socketConnection);
return new CallResult<UpdateSubscription>(subscribeResult.Error!); if (subQuery != null)
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
}
/// <summary>
/// Connect to an url and listen for data
/// </summary>
/// <param name="url">The URL to connect to</param>
/// <param name="subscription">The subscription</param>
/// <param name="connectionFactory">The factory for creating a socket connection</param>
/// <param name="ct">Cancellation token for closing this subscription</param>
/// <returns></returns>
protected virtual async Task<CallResult<HighPerfUpdateSubscription>> SubscribeHighPerfAsync<TUpdateType>(
string url,
HighPerfSubscription<TUpdateType> subscription,
IHighPerfConnectionFactory connectionFactory,
CancellationToken ct)
{
if (_disposing)
return new CallResult<HighPerfUpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
HighPerfSocketConnection<TUpdateType> socketConnection;
var released = false;
// Wait for a semaphore here, so we only connect 1 socket at a time.
// This is necessary for being able to see if connections can be combined
try
{
await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException tce)
{
return new CallResult<HighPerfUpdateSubscription>(new CancellationRequestedError(tce));
}
try
{
while (true)
{
// Get a new or existing socket connection
var socketResult = await GetHighPerfSocketConnection<TUpdateType>(url, connectionFactory, ct).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<HighPerfUpdateSubscription>(null);
socketConnection = socketResult.Data;
// Add a subscription on the socket connection
var success = socketConnection.AddSubscription(subscription);
if (!success)
{
_logger.FailedToAddSubscriptionRetryOnDifferentConnection(socketConnection.SocketId);
continue;
}
if (ClientOptions.SocketSubscriptionsCombineTarget == 1)
{
// Only 1 subscription per connection, so no need to wait for connection since a new subscription will create a new connection anyway
semaphoreSlim.Release();
released = true;
}
var needsConnecting = !socketConnection.Connected;
var connectResult = await ConnectIfNeededAsync(socketConnection, false, ct).ConfigureAwait(false);
if (!connectResult)
return new CallResult<HighPerfUpdateSubscription>(connectResult.Error!);
break;
}
}
finally
{
if (!released)
semaphoreSlim.Release();
}
var subRequest = subscription.CreateSubscriptionQuery(socketConnection);
if (subRequest != null)
{ {
// Send the request and wait for answer // Send the request and wait for answer
var sendResult = await socketConnection.SendAsync(subRequest).ConfigureAwait(false); var subResult = await socketConnection.SendAndWaitQueryAsync(subQuery, waitEvent, ct).ConfigureAwait(false);
if (!sendResult) if (!subResult)
{ {
await socketConnection.CloseAsync().ConfigureAwait(false); waitEvent?.Set();
return new CallResult<HighPerfUpdateSubscription>(sendResult.Error!); var isTimeout = subResult.Error is CancellationRequestedError;
if (isTimeout && subscription.Confirmed)
{
// No response received, but the subscription did receive updates. We'll assume success
}
else
{
_logger.FailedToSubscribe(socketConnection.SocketId, subResult.Error?.ToString());
// If this was a timeout we still need to send an unsubscribe to prevent messages coming in later
await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
return new CallResult<UpdateSubscription>(subResult.Error!);
} }
} }
subscription.HandleSubQueryResponse(subQuery.Response!);
}
subscription.Confirmed = true;
if (ct != default) if (ct != default)
{ {
subscription.CancellationTokenRegistration = ct.Register(async () => subscription.CancellationTokenRegistration = ct.Register(async () =>
{ {
_logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id); _logger.CancellationTokenSetClosingSubscription(socketConnection.SocketId, subscription.Id);
await socketConnection.CloseAsync().ConfigureAwait(false); await socketConnection.CloseAsync(subscription).ConfigureAwait(false);
}, false); }, false);
} }
waitEvent?.Set();
_logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id); _logger.SubscriptionCompletedSuccessfully(socketConnection.SocketId, subscription.Id);
return new CallResult<HighPerfUpdateSubscription>(new HighPerfUpdateSubscription(socketConnection, subscription)); return new CallResult<UpdateSubscription>(new UpdateSubscription(socketConnection, subscription));
} }
/// <summary> /// <summary>
@@ -438,10 +340,16 @@ namespace CryptoExchange.Net.Clients
SocketConnection socketConnection; SocketConnection socketConnection;
var released = false; var released = false;
await semaphoreSlim.WaitAsync().ConfigureAwait(false);
try try
{ {
var socketResult = await GetSocketConnection(url, query.Authenticated, true, ct).ConfigureAwait(false); await semaphoreSlim.WaitAsync(ct).ConfigureAwait(false);
}
catch (OperationCanceledException) { }
try
{
var socketResult = await GetSocketConnection(url, query.Authenticated, true).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.As<THandlerResponse>(default); return socketResult.As<THandlerResponse>(default);
@@ -473,7 +381,7 @@ namespace CryptoExchange.Net.Clients
if (ct.IsCancellationRequested) if (ct.IsCancellationRequested)
return new CallResult<THandlerResponse>(new CancellationRequestedError()); return new CallResult<THandlerResponse>(new CancellationRequestedError());
return await socketConnection.SendAndWaitQueryAsync(query, ct).ConfigureAwait(false); return await socketConnection.SendAndWaitQueryAsync(query, null, ct).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -483,7 +391,7 @@ namespace CryptoExchange.Net.Clients
/// <param name="authenticated">Whether the socket should authenticated</param> /// <param name="authenticated">Whether the socket should authenticated</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult> ConnectIfNeededAsync(ISocketConnection socket, bool authenticated, CancellationToken ct) protected virtual async Task<CallResult> ConnectIfNeededAsync(SocketConnection socket, bool authenticated, CancellationToken ct)
{ {
if (socket.Connected) if (socket.Connected)
return CallResult.SuccessResult; return CallResult.SuccessResult;
@@ -493,15 +401,18 @@ namespace CryptoExchange.Net.Clients
return connectResult; return connectResult;
if (ClientOptions.DelayAfterConnect != TimeSpan.Zero) if (ClientOptions.DelayAfterConnect != TimeSpan.Zero)
await Task.Delay(ClientOptions.DelayAfterConnect).ConfigureAwait(false); {
try
{
await Task.Delay(ClientOptions.DelayAfterConnect, ct).ConfigureAwait(false);
}
catch (OperationCanceledException) { }
}
if (!authenticated || socket.Authenticated) if (!authenticated || socket.Authenticated)
return CallResult.SuccessResult; return CallResult.SuccessResult;
if (socket is not SocketConnection sc) var result = await AuthenticateSocketAsync(socket).ConfigureAwait(false);
throw new InvalidOperationException("HighPerfSocketConnection not supported for authentication");
var result = await AuthenticateSocketAsync(sc).ConfigureAwait(false);
if (!result) if (!result)
await socket.CloseAsync().ConfigureAwait(false); await socket.CloseAsync().ConfigureAwait(false);
@@ -545,8 +456,7 @@ namespace CryptoExchange.Net.Clients
/// Should return the request which can be used to authenticate a socket connection /// Should return the request which can be used to authenticate a socket connection
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => protected internal virtual Task<Query?> GetAuthenticationRequestAsync(SocketConnection connection) => throw new NotImplementedException();
Task.FromResult(AuthenticationProvider!.GetAuthenticationQuery(this, connection));
/// <summary> /// <summary>
/// Adds a system subscription. Used for example to reply to ping requests /// Adds a system subscription. Used for example to reply to ping requests
@@ -555,7 +465,7 @@ namespace CryptoExchange.Net.Clients
protected void AddSystemSubscription(SystemSubscription systemSubscription) protected void AddSystemSubscription(SystemSubscription systemSubscription)
{ {
systemSubscriptions.Add(systemSubscription); systemSubscriptions.Add(systemSubscription);
foreach (var connection in _socketConnections.Values) foreach (var connection in socketConnections.Values)
connection.AddSubscription(systemSubscription); connection.AddSubscription(systemSubscription);
} }
@@ -575,7 +485,7 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <param name="connection"></param> /// <param name="connection"></param>
/// <returns></returns> /// <returns></returns>
protected internal virtual Task<Uri?> GetReconnectUriAsync(ISocketConnection connection) protected internal virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
{ {
return Task.FromResult<Uri?>(connection.ConnectionUri); return Task.FromResult<Uri?>(connection.ConnectionUri);
} }
@@ -596,94 +506,36 @@ namespace CryptoExchange.Net.Clients
/// <param name="address">The address the socket is for</param> /// <param name="address">The address the socket is for</param>
/// <param name="authenticated">Whether the socket should be authenticated</param> /// <param name="authenticated">Whether the socket should be authenticated</param>
/// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param> /// <param name="dedicatedRequestConnection">Whether a dedicated request connection should be returned</param>
/// <param name="ct">Cancellation token</param>
/// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param> /// <param name="topic">The subscription topic, can be provided when multiple of the same topics are not allowed on a connection</param>
/// <param name="individualSubscriptionCount">The number of individual subscriptions in this subscribe request</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection( protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated, bool dedicatedRequestConnection, string? topic = null)
string address,
bool authenticated,
bool dedicatedRequestConnection,
CancellationToken ct,
string? topic = null,
int individualSubscriptionCount = 1)
{ {
var socketQuery = _socketConnections.Where(s => s.Value.Tag.TrimEnd('/') == address.TrimEnd('/') var socketQuery = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
&& s.Value.ApiClient.GetType() == GetType() && s.Value.ApiClient.GetType() == GetType()
&& (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))) && (s.Value.Authenticated == authenticated || !authenticated)
.Select(x => x.Value) && (AllowTopicsOnTheSameConnection || !s.Value.Topics.Contains(topic))
.ToList(); && s.Value.Connected);
// If all current socket connections are reconnecting or resubscribing wait for that to finish as we can probably use the existing connection SocketConnection connection;
var delayStart = DateTime.UtcNow;
var delayed = false;
while (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
if (DateTime.UtcNow - delayStart > TimeSpan.FromSeconds(10))
{
if (socketQuery.Count >= 1 && socketQuery.All(x => x.Status == SocketStatus.Reconnecting || x.Status == SocketStatus.Resubscribing))
{
// If after this time we still trying to reconnect/reprocess there is some issue in the connection
_logger.TimeoutWaitingForReconnectingSocket();
return new CallResult<SocketConnection>(new CantConnectError());
}
break;
}
delayed = true;
try { await Task.Delay(50, ct).ConfigureAwait(false); } catch (Exception) { }
if (ct.IsCancellationRequested)
return new CallResult<SocketConnection>(new CancellationRequestedError());
}
if (delayed)
_logger.WaitedForReconnectingSocket((long)(DateTime.UtcNow - delayStart).TotalMilliseconds);
socketQuery = socketQuery.Where(s => (s.Status == SocketStatus.None || s.Status == SocketStatus.Connected)
&& (s.Authenticated == authenticated || !authenticated)
&& s.Connected).ToList();
SocketConnection? connection;
if (!dedicatedRequestConnection) if (!dedicatedRequestConnection)
{ {
connection = socketQuery.Where(s => !s.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.UserSubscriptionCount).FirstOrDefault(); connection = socketQuery.Where(s => !s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).OrderBy(s => s.Value.UserSubscriptionCount).FirstOrDefault().Value;
} }
else else
{ {
connection = socketQuery.Where(s => s.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault(); connection = socketQuery.Where(s => s.Value.DedicatedRequestConnection.IsDedicatedRequestConnection).FirstOrDefault().Value;
if (connection != null && !connection.DedicatedRequestConnection.Authenticated) if (connection != null && !connection.DedicatedRequestConnection.Authenticated)
// Mark dedicated request connection as authenticated if the request is authenticated // Mark dedicated request connection as authenticated if the request is authenticated
connection.DedicatedRequestConnection.Authenticated = authenticated; connection.DedicatedRequestConnection.Authenticated = authenticated;
if (connection == null)
// Fall back to an existing connection if there is no dedicated request connection available
connection = socketQuery.OrderBy(s => s.UserSubscriptionCount).FirstOrDefault();
} }
bool maxConnectionsReached = _socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections);
if (connection != null) if (connection != null)
{ {
bool lessThanBatchSubCombineTarget = connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget; if (connection.UserSubscriptionCount < ClientOptions.SocketSubscriptionsCombineTarget || (socketConnections.Count >= (ApiOptions.MaxSocketConnections ?? ClientOptions.MaxSocketConnections) && socketConnections.All(s => s.Value.UserSubscriptionCount >= ClientOptions.SocketSubscriptionsCombineTarget)))
bool lessThanIndividualSubCombineTarget = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount) < ClientOptions.SocketIndividualSubscriptionCombineTarget;
if ((lessThanBatchSubCombineTarget && lessThanIndividualSubCombineTarget)
|| maxConnectionsReached)
{
// Use existing socket if it has less than target connections OR it has the least connections and we can't make new // Use existing socket if it has less than target connections OR it has the least connections and we can't make new
// If there is a max subscriptions per connection limit also only use existing if the new subscription doesn't go over the limit
if (MaxIndividualSubscriptionsPerConnection == null)
return new CallResult<SocketConnection>(connection);
var currentCount = connection.Subscriptions.Sum(x => x.IndividualSubscriptionCount);
if (currentCount + individualSubscriptionCount <= MaxIndividualSubscriptionsPerConnection)
return new CallResult<SocketConnection>(connection); return new CallResult<SocketConnection>(connection);
} }
}
if (maxConnectionsReached)
return new CallResult<SocketConnection>(new InvalidOperationError("Max amount of socket connections reached"));
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false); var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
if (!connectionAddress) if (!connectionAddress)
@@ -695,8 +547,10 @@ namespace CryptoExchange.Net.Clients
if (connectionAddress.Data != address) if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!); _logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection // Create new socket
var socketConnection = new SocketConnection(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address); var socket = CreateSocket(connectionAddress.Data!);
var socketConnection = new SocketConnection(_logger, this, socket, address);
socketConnection.UnhandledMessage += HandleUnhandledMessage;
socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync; socketConnection.ConnectRateLimitedAsync += HandleConnectRateLimitedAsync;
if (dedicatedRequestConnection) if (dedicatedRequestConnection)
{ {
@@ -716,44 +570,13 @@ namespace CryptoExchange.Net.Clients
return new CallResult<SocketConnection>(socketConnection); return new CallResult<SocketConnection>(socketConnection);
} }
/// <summary>
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
/// </summary>
/// <param name="address">The address the socket is for</param>
/// <param name="connectionFactory">The factory for creating a socket connection</param>
/// <param name="ct">Cancellation token</param>
/// <returns></returns>
protected virtual async Task<CallResult<HighPerfSocketConnection<TUpdateType>>> GetHighPerfSocketConnection<TUpdateType>(
string address,
IHighPerfConnectionFactory connectionFactory,
CancellationToken ct)
{
var connectionAddress = await GetConnectionUrlAsync(address, false).ConfigureAwait(false);
if (!connectionAddress)
{
_logger.FailedToDetermineConnectionUrl(connectionAddress.Error?.ToString());
return connectionAddress.As<HighPerfSocketConnection<TUpdateType>>(null);
}
if (connectionAddress.Data != address)
_logger.ConnectionAddressSetTo(connectionAddress.Data!);
// Create new socket connection
var socketConnection = connectionFactory.CreateHighPerfConnection<TUpdateType>(_logger, SocketFactory, GetWebSocketParameters(connectionAddress.Data!), this, address);
foreach (var ptg in PeriodicTaskRegistrations)
socketConnection.QueryPeriodic(ptg.Identifier, ptg.Interval, (con) => ptg.QueryDelegate(con).Request);
return new CallResult<HighPerfSocketConnection<TUpdateType>>(socketConnection);
}
/// <summary> /// <summary>
/// Process an unhandled message /// Process an unhandled message
/// </summary> /// </summary>
/// <param name="connection">The socket connection</param> /// <param name="message">The message that wasn't processed</param>
/// <param name="typeIdentifier">The type as identified</param> protected virtual void HandleUnhandledMessage(IMessageAccessor message)
/// <param name="data">The data</param> {
protected internal virtual bool HandleUnhandledMessage(SocketConnection connection, string typeIdentifier, ReadOnlySpan<byte> data) => false; }
/// <summary> /// <summary>
/// Process connect rate limited /// Process connect rate limited
@@ -775,15 +598,12 @@ namespace CryptoExchange.Net.Clients
/// <param name="socketConnection">The socket to connect</param> /// <param name="socketConnection">The socket to connect</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult> ConnectSocketAsync(ISocketConnection socketConnection, CancellationToken ct) protected virtual async Task<CallResult> ConnectSocketAsync(SocketConnection socketConnection, CancellationToken ct)
{ {
var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false); var connectResult = await socketConnection.ConnectAsync(ct).ConfigureAwait(false);
if (connectResult) if (connectResult)
{ {
if (socketConnection is SocketConnection sc) socketConnections.TryAdd(socketConnection.SocketId, socketConnection);
_socketConnections.TryAdd(socketConnection.SocketId, sc);
else if (socketConnection is HighPerfSocketConnection hsc)
_highPerfSocketConnections.TryAdd(socketConnection.SocketId, hsc);
return connectResult; return connectResult;
} }
@@ -809,6 +629,18 @@ namespace CryptoExchange.Net.Clients
ReceiveBufferSize = ClientOptions.ReceiveBufferSize, ReceiveBufferSize = ClientOptions.ReceiveBufferSize,
}; };
/// <summary>
/// Create a socket for an address
/// </summary>
/// <param name="address">The address the socket should connect to</param>
/// <returns></returns>
protected virtual IWebsocket CreateSocket(string address)
{
var socket = SocketFactory.CreateWebsocket(_logger, GetWebSocketParameters(address));
_logger.SocketCreatedForAddress(socket.Id, address);
return socket;
}
/// <summary> /// <summary>
/// Unsubscribe an update subscription /// Unsubscribe an update subscription
/// </summary> /// </summary>
@@ -818,7 +650,7 @@ namespace CryptoExchange.Net.Clients
{ {
Subscription? subscription = null; Subscription? subscription = null;
SocketConnection? connection = null; SocketConnection? connection = null;
foreach (var socket in _socketConnections.Values.ToList()) foreach (var socket in socketConnections.Values.ToList())
{ {
subscription = socket.GetSubscription(subscriptionId); subscription = socket.GetSubscription(subscriptionId);
if (subscription != null) if (subscription != null)
@@ -856,24 +688,20 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
public virtual async Task UnsubscribeAllAsync() public virtual async Task UnsubscribeAllAsync()
{ {
var sum = _socketConnections.Sum(s => s.Value.UserSubscriptionCount) + _highPerfSocketConnections.Sum(s => s.Value.UserSubscriptionCount); var sum = socketConnections.Sum(s => s.Value.UserSubscriptionCount);
if (sum == 0) if (sum == 0)
return; return;
_logger.UnsubscribingAll(sum); _logger.UnsubscribingAll(socketConnections.Sum(s => s.Value.UserSubscriptionCount));
var tasks = new List<Task>(); var tasks = new List<Task>();
{
var socketList = _socketConnections.Values; var socketList = socketConnections.Values;
foreach (var connection in socketList) foreach (var connection in socketList)
{ {
foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription)) foreach(var subscription in connection.Subscriptions.Where(x => x.UserSubscription))
tasks.Add(connection.CloseAsync(subscription)); tasks.Add(connection.CloseAsync(subscription));
} }
}
var highPerfSocketList = _highPerfSocketConnections.Values;
foreach (var connection in highPerfSocketList)
tasks.Add(connection.CloseAsync());
await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(tasks.ToArray()).ConfigureAwait(false);
} }
@@ -884,10 +712,10 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
public virtual async Task ReconnectAsync() public virtual async Task ReconnectAsync()
{ {
_logger.ReconnectingAllConnections(_socketConnections.Count); _logger.ReconnectingAllConnections(socketConnections.Count);
var tasks = new List<Task>(); var tasks = new List<Task>();
{ {
var socketList = _socketConnections.Values; var socketList = socketConnections.Values;
foreach (var sub in socketList) foreach (var sub in socketList)
tasks.Add(sub.TriggerReconnectAsync()); tasks.Add(sub.TriggerReconnectAsync());
} }
@@ -900,7 +728,7 @@ namespace CryptoExchange.Net.Clients
{ {
foreach (var item in DedicatedConnectionConfigs) foreach (var item in DedicatedConnectionConfigs)
{ {
var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true, CancellationToken.None).ConfigureAwait(false); var socketResult = await GetSocketConnection(item.SocketAddress, item.Authenticated, true).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.AsDataless(); return socketResult.AsDataless();
@@ -919,7 +747,7 @@ namespace CryptoExchange.Net.Clients
base.SetOptions(options); base.SetOptions(options);
if ((!previousProxyIsSet && options.Proxy == null) if ((!previousProxyIsSet && options.Proxy == null)
|| _socketConnections.IsEmpty) || socketConnections.IsEmpty)
{ {
return; return;
} }
@@ -927,7 +755,7 @@ namespace CryptoExchange.Net.Clients
_logger.LogInformation("Reconnecting websockets to apply proxy"); _logger.LogInformation("Reconnecting websockets to apply proxy");
// Update proxy, also triggers reconnect // Update proxy, also triggers reconnect
foreach (var connection in _socketConnections) foreach (var connection in socketConnections)
_ = connection.Value.UpdateProxy(options.Proxy); _ = connection.Value.UpdateProxy(options.Proxy);
} }
@@ -946,15 +774,15 @@ namespace CryptoExchange.Net.Clients
/// <returns></returns> /// <returns></returns>
public SocketApiClientState GetState(bool includeSubDetails = true) public SocketApiClientState GetState(bool includeSubDetails = true)
{ {
var connectionStates = new List<SocketConnectionState>(); var connectionStates = new List<SocketConnection.SocketConnectionState>();
foreach (var socketIdAndConnection in _socketConnections) foreach (var socketIdAndConnection in socketConnections)
{ {
SocketConnection connection = socketIdAndConnection.Value; SocketConnection connection = socketIdAndConnection.Value;
SocketConnectionState connectionState = connection.GetState(includeSubDetails); SocketConnection.SocketConnectionState connectionState = connection.GetState(includeSubDetails);
connectionStates.Add(connectionState); connectionStates.Add(connectionState);
} }
return new SocketApiClientState(_socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates); return new SocketApiClientState(socketConnections.Count, CurrentSubscriptions, IncomingKbps, connectionStates);
} }
/// <summary> /// <summary>
@@ -968,7 +796,7 @@ namespace CryptoExchange.Net.Clients
int Connections, int Connections,
int Subscriptions, int Subscriptions,
double DownloadSpeed, double DownloadSpeed,
List<SocketConnectionState> ConnectionStates) List<SocketConnection.SocketConnectionState> ConnectionStates)
{ {
/// <summary> /// <summary>
/// Print the state of the client /// Print the state of the client
@@ -997,8 +825,9 @@ namespace CryptoExchange.Net.Clients
cs.SubscriptionStates.ForEach(subState => cs.SubscriptionStates.ForEach(subState =>
{ {
sb.AppendLine($"\t\t\tId: {subState.Id}"); sb.AppendLine($"\t\t\tId: {subState.Id}");
sb.AppendLine($"\t\t\tStatus: {subState.Status}"); sb.AppendLine($"\t\t\tConfirmed: {subState.Confirmed}");
sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}"); sb.AppendLine($"\t\t\tInvocations: {subState.Invocations}");
sb.AppendLine($"\t\t\tIdentifiers: [{subState.ListenMatcher.ToString()}]");
}); });
} }
}); });
@@ -1010,12 +839,15 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Dispose the client /// Dispose the client
/// </summary> /// </summary>
public override void Dispose() public override void Dispose(bool disposing)
{ {
if (disposing)
return;
_disposing = true; _disposing = true;
var tasks = new List<Task>(); var tasks = new List<Task>();
{ {
var socketList = _socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected); var socketList = socketConnections.Values.Where(x => x.UserSubscriptionCount > 0 || x.Connected);
if (socketList.Any()) if (socketList.Any())
_logger.DisposingSocketClient(); _logger.DisposingSocketClient();
@@ -1026,18 +858,22 @@ namespace CryptoExchange.Net.Clients
} }
semaphoreSlim?.Dispose(); semaphoreSlim?.Dispose();
base.Dispose(); base.Dispose(disposing);
} }
/// <summary>
/// Get the listener identifier for the message
/// </summary>
/// <param name="messageAccessor"></param>
/// <returns></returns>
public abstract string? GetListenerIdentifier(IMessageAccessor messageAccessor);
/// <summary> /// <summary>
/// Preprocess a stream message /// Preprocess a stream message
/// </summary> /// </summary>
public virtual ReadOnlySpan<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlySpan<byte> data) => data; /// <param name="connection"></param>
/// <param name="type"></param>
/// <summary> /// <param name="data"></param>
/// Create a new message converter instance
/// </summary>
/// <returns></returns> /// <returns></returns>
public abstract ISocketMessageHandler CreateMessageConverter(WebSocketMessageType messageType); public virtual ReadOnlyMemory<byte> PreprocessStreamMessage(SocketConnection connection, WebSocketMessageType type, ReadOnlyMemory<byte> data) => data;
}
} }
@@ -1,7 +1,7 @@
using System; using System;
namespace CryptoExchange.Net.Converters;
namespace CryptoExchange.Net.Converters
{
/// <summary> /// <summary>
/// Mark property as an index in the array /// Mark property as an index in the array
/// </summary> /// </summary>
@@ -22,4 +22,3 @@ namespace CryptoExchange.Net.Converters
Index = index; Index = index;
} }
} }
}
@@ -1,9 +1,9 @@
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> /// <summary>
/// Caching for JsonSerializerContext instances /// Caching for JsonSerializerContext instances
/// </summary> /// </summary>
@@ -26,4 +26,3 @@ namespace CryptoExchange.Net.Converters
return instance; return instance;
} }
} }
}
@@ -1,64 +0,0 @@
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);
}
}
@@ -1,27 +0,0 @@
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);
}
}
@@ -1,46 +0,0 @@
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;
}
}
}
@@ -1,15 +0,0 @@
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;
}
}
}
@@ -1,152 +0,0 @@
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;
}
}
}
@@ -1,60 +0,0 @@
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
});
}
}
@@ -1,17 +0,0 @@
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; }
}
}
@@ -0,0 +1,48 @@
namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Node accessor
/// </summary>
public readonly struct NodeAccessor
{
/// <summary>
/// Index
/// </summary>
public int? Index { get; }
/// <summary>
/// Property name
/// </summary>
public string? Property { get; }
/// <summary>
/// Type (0 = int, 1 = string, 2 = prop name)
/// </summary>
public int Type { get; }
private NodeAccessor(int? index, string? property, int type)
{
Index = index;
Property = property;
Type = type;
}
/// <summary>
/// Create an int node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor Int(int value) { return new NodeAccessor(value, null, 0); }
/// <summary>
/// Create a string node accessor
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static NodeAccessor String(string value) { return new NodeAccessor(null, value, 1); }
/// <summary>
/// Create a property name node accessor
/// </summary>
/// <returns></returns>
public static NodeAccessor PropertyName() { return new NodeAccessor(null, null, 2); }
}
@@ -0,0 +1,49 @@
using System.Collections;
using System.Collections.Generic;
namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Message access definition
/// </summary>
public readonly struct MessagePath : IEnumerable<NodeAccessor>
{
private readonly List<NodeAccessor> _path;
internal void Add(NodeAccessor node)
{
_path.Add(node);
}
/// <summary>
/// ctor
/// </summary>
public MessagePath()
{
_path = new List<NodeAccessor>();
}
/// <summary>
/// Create a new message path
/// </summary>
/// <returns></returns>
public static MessagePath Get()
{
return new MessagePath();
}
/// <summary>
/// IEnumerable implementation
/// </summary>
/// <returns></returns>
public IEnumerator<NodeAccessor> GetEnumerator()
{
for (var i = 0; i < _path.Count; i++)
yield return _path[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
@@ -0,0 +1,42 @@
namespace CryptoExchange.Net.Converters.MessageParsing;
/// <summary>
/// Message path extension methods
/// </summary>
public static class MessagePathExtension
{
/// <summary>
/// Add a string node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="propName"></param>
/// <returns></returns>
public static MessagePath Property(this MessagePath path, string propName)
{
path.Add(NodeAccessor.String(propName));
return path;
}
/// <summary>
/// Add a property name node accessor
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static MessagePath PropertyName(this MessagePath path)
{
path.Add(NodeAccessor.PropertyName());
return path;
}
/// <summary>
/// Add a int node accessor
/// </summary>
/// <param name="path"></param>
/// <param name="index"></param>
/// <returns></returns>
public static MessagePath Index(this MessagePath path, int index)
{
path.Add(NodeAccessor.Int(index));
return path;
}
}
@@ -0,0 +1,20 @@
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,16 +1,17 @@
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;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Text.Json;
using System.Collections.Generic;
#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> /// <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 /// 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 /// with [ArrayProperty(x)] where x is the index of the property in the array
@@ -21,7 +22,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public class ArrayConverter<T> : JsonConverter<T> where T : new() public class ArrayConverter<T> : JsonConverter<T> where T : new()
#endif #endif
{ {
private static SortedDictionary<int, List<ArrayPropertyInfo>>? _typePropertyInfo; private static readonly Lazy<List<ArrayPropertyInfo>> _typePropertyInfo = new Lazy<List<ArrayPropertyInfo>>(CacheTypeAttributes, LazyThreadSafetyMode.PublicationOnly);
/// <inheritdoc /> /// <inheritdoc />
#if NET5_0_OR_GREATER #if NET5_0_OR_GREATER
@@ -36,17 +37,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return; return;
} }
if (_typePropertyInfo == null)
_typePropertyInfo = CacheTypeAttributes();
writer.WriteStartArray(); writer.WriteStartArray();
var ordered = _typePropertyInfo.Value.Where(x => x.ArrayProperty != null).OrderBy(p => p.ArrayProperty.Index);
var last = -1; var last = -1;
foreach (var indexProps in _typePropertyInfo) foreach (var prop in ordered)
{
foreach (var prop in indexProps.Value)
{ {
if (prop.ArrayProperty.Index == last) if (prop.ArrayProperty.Index == last)
// Don't write the same index twice
continue; continue;
while (prop.ArrayProperty.Index != last + 1) while (prop.ArrayProperty.Index != last + 1)
@@ -90,7 +87,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
JsonSerializer.Serialize(writer, objValue, typeOptions ?? options); JsonSerializer.Serialize(writer, objValue, typeOptions ?? options);
} }
} }
}
writer.WriteEndArray(); writer.WriteEndArray();
} }
@@ -115,11 +111,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif #endif
{ {
if (reader.TokenType != JsonTokenType.StartArray) if (reader.TokenType != JsonTokenType.StartArray)
throw new CeDeserializationException("Not an array"); throw new Exception("Not an array");
if (_typePropertyInfo == null)
_typePropertyInfo = CacheTypeAttributes();
int index = 0; int index = 0;
while (reader.Read()) while (reader.Read())
@@ -127,7 +119,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (reader.TokenType == JsonTokenType.EndArray) if (reader.TokenType == JsonTokenType.EndArray)
break; break;
if(!_typePropertyInfo.TryGetValue(index, out var indexAttributes)) var indexAttributes = _typePropertyInfo.Value.Where(a => a.ArrayProperty.Index == index);
if (!indexAttributes.Any())
{ {
index++; index++;
continue; continue;
@@ -167,7 +160,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
JsonTokenType.String => reader.GetString(), JsonTokenType.String => reader.GetString(),
JsonTokenType.Number => reader.GetDecimal(), JsonTokenType.Number => reader.GetDecimal(),
JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options), JsonTokenType.StartObject => JsonSerializer.Deserialize(ref reader, attribute.TargetType, options),
_ => throw new CeDeserializationException($"Array deserialization of type {reader.TokenType} not supported"), _ => throw new NotImplementedException($"Array deserialization of type {reader.TokenType} not supported"),
}; };
} }
@@ -199,12 +192,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#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 SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes() private static List<ArrayPropertyInfo> CacheTypeAttributes()
#else #else
private static SortedDictionary<int, List<ArrayPropertyInfo>> CacheTypeAttributes() private static List<ArrayPropertyInfo> CacheTypeAttributes()
#endif #endif
{ {
var result = new SortedDictionary<int, List<ArrayPropertyInfo>>(); var attributes = new List<ArrayPropertyInfo>();
var properties = typeof(T).GetProperties(); var properties = typeof(T).GetProperties();
foreach (var property in properties) foreach (var property in properties)
{ {
@@ -214,13 +207,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; var targetType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType; var converterType = property.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType ?? targetType.GetCustomAttribute<JsonConverterAttribute>()?.ConverterType;
if (!result.TryGetValue(att.Index, out var indexList)) attributes.Add(new ArrayPropertyInfo
{
indexList = new List<ArrayPropertyInfo>();
result[att.Index] = indexList;
}
indexList.Add(new ArrayPropertyInfo
{ {
ArrayProperty = att, ArrayProperty = att,
PropertyInfo = property, PropertyInfo = property,
@@ -230,7 +217,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}); });
} }
return result; return attributes;
} }
private class ArrayPropertyInfo private class ArrayPropertyInfo
@@ -240,7 +227,6 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public JsonConverter? JsonConverter { get; set; } public JsonConverter? JsonConverter { get; set; }
public bool DefaultDeserialization { get; set; } public bool DefaultDeserialization { get; set; }
public Type TargetType { get; set; } = null!; public Type TargetType { get; set; } = null!;
public JsonSerializerOptions? JsonSerializerOptions { get; set; } = null; public JsonSerializerOptions? JsonSerializerOptions { get; set; }
}
} }
} }
@@ -1,10 +1,10 @@
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> /// <summary>
/// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue) /// Decimal converter that handles overflowing decimal values (by setting it to decimal.MaxValue)
/// </summary> /// </summary>
@@ -43,4 +43,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteNumberValue(value); writer.WriteNumberValue(value);
} }
} }
}
@@ -1,11 +1,11 @@
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> /// <summary>
/// Bool converter /// Bool converter
/// </summary> /// </summary>
@@ -20,35 +20,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
return typeToConvert == typeof(bool) ? new BoolConverterInner() : new BoolConverterInnerNullable(); return typeToConvert == typeof(bool) ? new BoolConverterInner<bool>() : new BoolConverterInner<bool?>();
} }
private class BoolConverterInnerNullable : JsonConverter<bool?> private class BoolConverterInner<T> : JsonConverter<T>
{ {
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadBool(ref reader, typeToConvert, options); => (T)((object?)ReadBool(ref reader, typeToConvert, options) ?? default(T))!;
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options) public static bool? ReadBool(ref Utf8JsonReader reader, Type typeToConvert, 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;
@@ -67,7 +47,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
if (typeToConvert == typeof(bool)) if (typeToConvert == typeof(bool))
LibraryHelpers.StaticLogger?.LogWarning("Received null or empty bool value, but property type is not a nullable bool. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Received null bool value, but property type is not a nullable bool");
return default; return default;
} }
@@ -91,5 +71,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
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,11 +1,13 @@
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>
@@ -32,4 +34,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x)))); writer.WriteStringValue(string.Join(",", value.Select(x => EnumConverter.GetString(x))));
} }
} }
}
@@ -1,30 +0,0 @@
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,12 +1,12 @@
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> /// <summary>
/// Date time converter /// Date time converter
/// </summary> /// </summary>
@@ -14,8 +14,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000; private const long _ticksPerSecond = TimeSpan.TicksPerMillisecond * 1000;
private const decimal _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000m; private const double _ticksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000d;
private const decimal _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000m / 1000; private const double _ticksPerNanosecond = TimeSpan.TicksPerMillisecond / 1000d / 1000;
/// <inheritdoc /> /// <inheritdoc />
public override bool CanConvert(Type typeToConvert) public override bool CanConvert(Type typeToConvert)
@@ -26,73 +26,43 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{ {
return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner() : new NullableDateTimeConverterInner(); return typeToConvert == typeof(DateTime) ? new DateTimeConverterInner<DateTime>() : new DateTimeConverterInner<DateTime?>();
} }
private class NullableDateTimeConverterInner : JsonConverter<DateTime?> private class DateTimeConverterInner<T> : JsonConverter<T>
{ {
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ReadDateTime(ref reader, typeToConvert, options); => (T)((object?)ReadDateTime(ref reader, typeToConvert, options) ?? default(T))!;
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))
LibraryHelpers.StaticLogger?.LogWarning("DateTime value of null, but property is not nullable. Resolver: {Resolver}", options.TypeInfoResolver?.GetType()?.Name); Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | DateTime value of null, but property is not nullable");
return default; return default;
} }
if (reader.TokenType is JsonTokenType.Number) if (reader.TokenType is JsonTokenType.Number)
{ {
var decValue = reader.GetDecimal(); var longValue = reader.GetDouble();
if (decValue == 0 || decValue < 0) if (longValue == 0 || longValue < 0)
return default; return default;
return ParseFromDecimal(decValue); return ParseFromDouble(longValue);
} }
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!.Equals("-1", StringComparison.Ordinal) || stringValue == "-1"
|| stringValue!.Equals("0001-01-01T00:00:00Z", StringComparison.OrdinalIgnoreCase) || stringValue == "0001-01-01T00:00:00Z"
|| decimal.TryParse(stringValue, out var decVal) && decVal == 0) || double.TryParse(stringValue, out var doubleVal) && doubleVal == 0)
{ {
return default; return default;
} }
return ParseFromString(stringValue!, options.TypeInfoResolver?.GetType()?.Name); return ParseFromString(stringValue!);
} }
else else
{ {
@@ -100,33 +70,48 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
} }
/// <summary> public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
/// 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 < 19999999999) if (value == null)
return ConvertFromSeconds(value); {
if (value < 19999999999999) writer.WriteNullValue();
return ConvertFromMilliseconds(value); }
if (value < 19999999999999999) else
return ConvertFromMicroseconds(value); {
var dtValue = (DateTime)(object)value;
if (dtValue == default)
writer.WriteStringValue(default(DateTime));
else
writer.WriteNumberValue((long)Math.Round((dtValue - new DateTime(1970, 1, 1)).TotalMilliseconds));
}
}
}
return ConvertFromNanoseconds(value); /// <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> /// <summary>
/// Parse a string value to datetime /// Parse a string value to datetime
/// </summary> /// </summary>
public static DateTime ParseFromString(string stringValue, string? resolverName) /// <param name="stringValue"></param>
/// <returns></returns>
public static DateTime ParseFromString(string stringValue)
{ {
if (stringValue!.Length == 12 && stringValue.StartsWith("202", StringComparison.OrdinalIgnoreCase)) if (stringValue!.Length == 12 && stringValue.StartsWith("202"))
{ {
// Parse 202303261200 format // Parse 202303261200 format
if (!int.TryParse(stringValue.Substring(0, 4), out var year) if (!int.TryParse(stringValue.Substring(0, 4), out var year)
@@ -135,7 +120,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(8, 2), out var hour) || !int.TryParse(stringValue.Substring(8, 2), out var hour)
|| !int.TryParse(stringValue.Substring(10, 2), out var minute)) || !int.TryParse(stringValue.Substring(10, 2), out var minute))
{ {
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName); Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default; return default;
} }
return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc); return new DateTime(year, month, day, hour, minute, 0, DateTimeKind.Utc);
@@ -148,7 +133,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(4, 2), out var month) || !int.TryParse(stringValue.Substring(4, 2), out var month)
|| !int.TryParse(stringValue.Substring(6, 2), out var day)) || !int.TryParse(stringValue.Substring(6, 2), out var day))
{ {
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName); Trace.WriteLine($"{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default; return default;
} }
return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc); return new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Utc);
@@ -161,25 +146,25 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(stringValue.Substring(2, 2), out var month) || !int.TryParse(stringValue.Substring(2, 2), out var month)
|| !int.TryParse(stringValue.Substring(4, 2), out var day)) || !int.TryParse(stringValue.Substring(4, 2), out var day))
{ {
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName); Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default; return default;
} }
return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc); return new DateTime(year + 2000, month, day, 0, 0, 0, DateTimeKind.Utc);
} }
if (decimal.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var decimalValue)) if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{ {
// Parse 1637745563.000 format // Parse 1637745563.000 format
if (decimalValue <= 0) if (doubleValue <= 0)
return default; return default;
if (decimalValue < 19999999999) if (doubleValue < 19999999999)
return ConvertFromSeconds(decimalValue); return ConvertFromSeconds(doubleValue);
if (decimalValue < 19999999999999) if (doubleValue < 19999999999999)
return ConvertFromMilliseconds(decimalValue); return ConvertFromMilliseconds((long)doubleValue);
if (decimalValue < 19999999999999999) if (doubleValue < 19999999999999999)
return ConvertFromMicroseconds(decimalValue); return ConvertFromMicroseconds((long)doubleValue);
return ConvertFromNanoseconds(decimalValue); return ConvertFromNanoseconds((long)doubleValue);
} }
if (stringValue.Length == 10) if (stringValue.Length == 10)
@@ -190,7 +175,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
|| !int.TryParse(values[1], out var month) || !int.TryParse(values[1], out var month)
|| !int.TryParse(values[2], out var day)) || !int.TryParse(values[2], out var day))
{ {
LibraryHelpers.StaticLogger?.LogWarning("Unknown DateTime format: {Value}. Resolver: {Resolver}", stringValue, resolverName); Trace.WriteLine("{DateTime.Now:yyyy/MM/dd HH:mm:ss:fff} | Warning | Unknown DateTime format: " + stringValue);
return default; return default;
} }
@@ -203,71 +188,54 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <summary> /// <summary>
/// Convert a seconds since epoch (01-01-1970) value to DateTime /// Convert a seconds since epoch (01-01-1970) value to DateTime
/// </summary> /// </summary>
public static DateTime ConvertFromSeconds(decimal seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond)); /// <param name="seconds"></param>
/// <summary> /// <returns></returns>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime public static DateTime ConvertFromSeconds(double seconds) => _epoch.AddTicks((long)Math.Round(seconds * _ticksPerSecond));
/// </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> /// <summary>
/// Convert a milliseconds since epoch (01-01-1970) value to DateTime /// Convert a milliseconds since epoch (01-01-1970) value to DateTime
/// </summary> /// </summary>
public static DateTime ConvertFromMilliseconds(decimal milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond)); /// <param name="milliseconds"></param>
/// <summary> /// <returns></returns>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime public static DateTime ConvertFromMilliseconds(double milliseconds) => _epoch.AddTicks((long)Math.Round(milliseconds * TimeSpan.TicksPerMillisecond));
/// </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> /// <summary>
/// Convert a microseconds since epoch (01-01-1970) value to DateTime /// Convert a microseconds since epoch (01-01-1970) value to DateTime
/// </summary> /// </summary>
public static DateTime ConvertFromMicroseconds(decimal microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond)); /// <param name="microseconds"></param>
/// <returns></returns>
public static DateTime ConvertFromMicroseconds(double microseconds) => _epoch.AddTicks((long)Math.Round(microseconds * _ticksPerMicrosecond));
/// <summary> /// <summary>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime /// Convert a nanoseconds since epoch (01-01-1970) value to DateTime
/// </summary> /// </summary>
public static DateTime ConvertFromMicroseconds(double microseconds) => ConvertFromMicroseconds((decimal)microseconds); /// <param name="nanoseconds"></param>
/// <summary> /// <returns></returns>
/// Convert a nanoseconds since epoch (01-01-1970) value to DateTime public static DateTime ConvertFromNanoseconds(double nanoseconds) => _epoch.AddTicks((long)Math.Round(nanoseconds * _ticksPerNanosecond));
/// </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> /// <summary>
/// Convert a DateTime value to seconds since epoch (01-01-1970) value /// Convert a DateTime value to seconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")] [return: NotNullIfNotNull("time")]
public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds); public static long? ConvertToSeconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalSeconds);
/// <summary> /// <summary>
/// Convert a DateTime value to milliseconds since epoch (01-01-1970) value /// Convert a DateTime value to milliseconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")] [return: NotNullIfNotNull("time")]
public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds); public static long? ConvertToMilliseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).TotalMilliseconds);
/// <summary> /// <summary>
/// Convert a DateTime value to microseconds since epoch (01-01-1970) value /// Convert a DateTime value to microseconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")] [return: NotNullIfNotNull("time")]
public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond); public static long? ConvertToMicroseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerMicrosecond);
/// <summary> /// <summary>
/// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value /// Convert a DateTime value to nanoseconds since epoch (01-01-1970) value
/// </summary> /// </summary>
/// <param name="time"></param>
/// <returns></returns>
[return: NotNullIfNotNull("time")] [return: NotNullIfNotNull("time")]
public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond); public static long? ConvertToNanoseconds(DateTime? time) => time == null ? null : (long)Math.Round((time.Value - _epoch).Ticks / _ticksPerNanosecond);
} }
}
@@ -1,9 +1,9 @@
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> /// <summary>
/// Decimal converter /// Decimal converter
/// </summary> /// </summary>
@@ -41,4 +41,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteNumberValue(value.Value); writer.WriteNumberValue(value.Value);
} }
} }
}
@@ -1,10 +1,10 @@
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> /// <summary>
/// Converter for serializing decimal values as string /// Converter for serializing decimal values as string
/// </summary> /// </summary>
@@ -20,4 +20,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
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,19 +1,16 @@
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> /// <summary>
/// Static EnumConverter methods /// Static EnumConverter methods
/// </summary> /// </summary>
@@ -67,26 +64,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif #endif
: JsonConverter<T>, INullableConverterFactory where T : struct, Enum : JsonConverter<T>, INullableConverterFactory where T : struct, Enum
{ {
class EnumMapping private static List<KeyValuePair<T, string>>? _mapping;
{ private NullableEnumConverter? _nullableEnumConverter;
public T Value { get; set; }
public string StringValue { get; set; }
public EnumMapping(T value, string stringValue)
{
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>(); private static ConcurrentBag<string> _unknownValuesWarned = new ConcurrentBag<string>();
@@ -100,7 +79,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
return _enumConverter.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString); return EnumConverter<T>.ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
} }
public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options)
@@ -119,13 +98,20 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <inheritdoc /> /// <inheritdoc />
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{ {
var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString); var t = ReadNullable(ref reader, typeToConvert, options, out var isEmptyString, out var warn);
if (t == null) if (t == null)
{ {
if (isEmptyString && !_unknownValuesWarned.Contains(null)) if (warn)
{
if (isEmptyString)
{ {
// We received an empty string and have no mapping for it, and the property isn't nullable // 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"); 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
{
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");
}
} }
return new T(); // return default value return new T(); // return default value
@@ -136,12 +122,13 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
} }
private T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString) private static T? ReadNullable(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options, out bool isEmptyString, out bool warn)
{ {
isEmptyString = false; isEmptyString = false;
warn = false;
var enumType = typeof(T); var enumType = typeof(T);
if (_mappingToEnum == null) if (_mapping == null)
CreateMapping(); _mapping = AddMapping();
var stringValue = reader.TokenType switch var stringValue = reader.TokenType switch
{ {
@@ -153,10 +140,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
_ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType) _ => throw new Exception("Invalid token type for enum deserialization: " + reader.TokenType)
}; };
if (stringValue is null) if (string.IsNullOrEmpty(stringValue))
return null; return null;
if (!GetValue(enumType, stringValue, out var result)) if (!GetValue(enumType, stringValue!, out var result))
{ {
if (string.IsNullOrWhiteSpace(stringValue)) if (string.IsNullOrWhiteSpace(stringValue))
{ {
@@ -167,8 +154,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
// We received an enum value but weren't able to parse it. // We received an enum value but weren't able to parse it.
if (!_unknownValuesWarned.Contains(stringValue)) if (!_unknownValuesWarned.Contains(stringValue))
{ {
warn = true;
_unknownValuesWarned.Add(stringValue!); _unknownValuesWarned.Add(stringValue!);
LibraryHelpers.StaticLogger?.LogWarning($"Cannot map enum value. EnumType: {enumType.FullName}, Value: {stringValue}, Known values: [{string.Join(", ", _mappingToEnum!.Select(m => $"{m.StringValue}: {m.Value}"))}]. If you think {stringValue} should added please open an issue on the Github repo"); 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");
} }
} }
@@ -187,35 +175,16 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private static bool GetValue(Type objectType, string value, out T? result) private static bool GetValue(Type objectType, string value, out T? result)
{ {
if (_mappingToEnum != null) if (_mapping != null)
{ {
EnumMapping? mapping = null; // Check for exact match first, then if not found fallback to a case insensitive match
// Try match on full equals var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
foreach (var item in _mappingToEnum) if (mapping.Equals(default(KeyValuePair<T, string>)))
{ mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
// If not found, try matching ignoring case if (!mapping.Equals(default(KeyValuePair<T, string>)))
if (mapping == null)
{ {
foreach (var item in _mappingToEnum) result = mapping.Key;
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
{
result = mapping.Value;
return true; return true;
} }
} }
@@ -235,23 +204,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return false; return false;
} }
if (String.IsNullOrEmpty(value))
{
// An empty/null value will always fail when parsing, so just return here
result = default;
return false;
}
try try
{ {
// If no explicit mapping is found try to parse string // If no explicit mapping is found try to parse string
result = (T)Enum.Parse(objectType, value, true); result = (T)Enum.Parse(objectType, value, true);
if (!Enum.IsDefined(objectType, result))
{
result = default;
return false;
}
return true; return true;
} }
catch (Exception) catch (Exception)
@@ -261,11 +217,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
} }
} }
private static void CreateMapping() private static List<KeyValuePair<T, string>> AddMapping()
{ {
var mappingToEnum = new List<EnumMapping>(); var mapping = new List<KeyValuePair<T, string>>();
var mappingToString = new Dictionary<T, string>();
var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); var enumType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
var enumMembers = enumType.GetFields(); var enumMembers = enumType.GetFields();
foreach (var member in enumMembers) foreach (var member in enumMembers)
@@ -274,22 +228,12 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
foreach (MapAttribute attribute in maps) foreach (MapAttribute attribute in maps)
{ {
foreach (var value in attribute.Values) foreach (var value in attribute.Values)
{ mapping.Add(new KeyValuePair<T, string>((T)Enum.Parse(enumType, member.Name), value));
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 _mapping = mapping;
_mappingToEnum = mappingToEnum.ToFrozenSet(); return mapping;
_mappingToString = mappingToString.ToFrozenDictionary();
#else
_mappingToEnum = mappingToEnum;
_mappingToString = mappingToString;
#endif
} }
/// <summary> /// <summary>
@@ -300,10 +244,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
[return: NotNullIfNotNull("enumValue")] [return: NotNullIfNotNull("enumValue")]
public static string? GetString(T? enumValue) public static string? GetString(T? enumValue)
{ {
if (_mappingToString == null) if (_mapping == null)
CreateMapping(); _mapping = AddMapping();
return enumValue == null ? null : (_mappingToString!.TryGetValue(enumValue.Value, out var str) ? str : enumValue.ToString()); return enumValue == null ? null : (_mapping.FirstOrDefault(v => v.Key.Equals(enumValue)).Value ?? enumValue.ToString());
} }
/// <summary> /// <summary>
@@ -314,35 +258,15 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
public static T? ParseString(string value) public static T? ParseString(string value)
{ {
var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); var type = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
if (_mappingToEnum == null) if (_mapping == null)
CreateMapping(); _mapping = AddMapping();
EnumMapping? mapping = null; var mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCulture));
// Try match on full equals if (mapping.Equals(default(KeyValuePair<T, string>)))
foreach(var item in _mappingToEnum!) mapping = _mapping.FirstOrDefault(kv => kv.Value.Equals(value, StringComparison.InvariantCultureIgnoreCase));
{
if (item.StringValue.Equals(value, StringComparison.Ordinal))
{
mapping = item;
break;
}
}
// If not found, try matching ignoring case if (!mapping.Equals(default(KeyValuePair<T, string>)))
if (mapping == null) return mapping.Key;
{
foreach (var item in _mappingToEnum)
{
if (item.StringValue.Equals(value, StringComparison.OrdinalIgnoreCase))
{
mapping = item;
break;
}
}
}
if (mapping != null)
return mapping.Value;
try try
{ {
@@ -362,4 +286,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return _nullableEnumConverter; return _nullableEnumConverter;
} }
} }
}
@@ -1,9 +1,9 @@
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> /// <summary>
/// Converter for serializing enum values as int /// Converter for serializing enum values as int
/// </summary> /// </summary>
@@ -19,4 +19,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
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,9 +1,8 @@
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace CryptoExchange.Net.Converters.SystemTextJson;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
internal interface INullableConverterFactory internal interface INullableConverterFactory
{ {
JsonConverter CreateNullableConverter(); JsonConverter CreateNullableConverter();
} }
}
@@ -1,38 +0,0 @@
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,10 +1,10 @@
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> /// <summary>
/// Int converter /// Int converter
/// </summary> /// </summary>
@@ -37,4 +37,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteNumberValue(value.Value); writer.WriteNumberValue(value.Value);
} }
} }
}
@@ -1,10 +1,10 @@
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> /// <summary>
/// Int converter /// Int converter
/// </summary> /// </summary>
@@ -37,4 +37,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteNumberValue(value.Value); writer.WriteNumberValue(value.Value);
} }
} }
}
@@ -1,131 +0,0 @@
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;
}
}
@@ -1,362 +0,0 @@
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
}
}
}
@@ -1,63 +0,0 @@
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,10 +1,10 @@
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 internal class NullableEnumConverterFactory : JsonConverterFactory
{ {
private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver; private readonly IJsonTypeInfoResolver _jsonTypeInfoResolver;
@@ -38,4 +38,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return nullConverterFactory.CreateNullableConverter(); return nullConverterFactory.CreateNullableConverter();
} }
} }
}
@@ -1,9 +1,9 @@
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> /// <summary>
/// Read string or number as string /// Read string or number as string
/// </summary> /// </summary>
@@ -39,4 +39,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteStringValue(value); writer.WriteStringValue(value);
} }
} }
}
@@ -1,58 +0,0 @@
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,10 +1,12 @@
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> /// <summary>
/// Converter for values which contain a nested json value /// Converter for values which contain a nested json value
/// </summary> /// </summary>
@@ -24,7 +26,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
return default; return default;
return (T?)JsonDocument.Parse(value!).Deserialize(typeof(T), options); return JsonDocument.Parse(value!).Deserialize<T>(options);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -40,4 +42,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteStringValue(JsonSerializer.Serialize(value, options)); writer.WriteStringValue(JsonSerializer.Serialize(value, options));
} }
} }
}
@@ -1,10 +1,10 @@
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> /// <summary>
/// Replace a value on a string property /// Replace a value on a string property
/// </summary> /// </summary>
@@ -19,7 +19,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
{ {
_replacementSets = replaceSets.Select(x => _replacementSets = replaceSets.Select(x =>
{ {
var split = x.Split(new string[] { "->" }, StringSplitOptions.None); var split = x.Split(["->"], StringSplitOptions.None);
if (split.Length != 2) if (split.Length != 2)
throw new ArgumentException("Invalid replacement config"); throw new ArgumentException("Invalid replacement config");
return (split[0], split[1]); return (split[0], split[1]);
@@ -38,4 +38,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <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,7 +1,7 @@
using System; using System;
namespace CryptoExchange.Net.Converters.SystemTextJson;
namespace CryptoExchange.Net.Converters.SystemTextJson
{
/// <summary> /// <summary>
/// Attribute to mark a model as json serializable. Used for AOT compilation. /// Attribute to mark a model as json serializable. Used for AOT compilation.
/// </summary> /// </summary>
@@ -18,4 +18,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// <param name="type"></param> /// <param name="type"></param>
public SerializationModelAttribute(Type type) { } public SerializationModelAttribute(Type type) { }
} }
}
@@ -1,9 +1,9 @@
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> /// <summary>
/// Serializer options /// Serializer options
/// </summary> /// </summary>
@@ -44,4 +44,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
return options; return options;
} }
} }
}
@@ -1,10 +1,10 @@
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 SharedQuantityConverter : SharedQuantityReferenceConverter<SharedQuantity> { }
internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { } internal class SharedOrderQuantityConverter : SharedQuantityReferenceConverter<SharedOrderQuantity> { }
@@ -55,4 +55,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteEndArray(); writer.WriteEndArray();
} }
} }
}
@@ -1,10 +1,10 @@
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> internal class SharedSymbolConverter : JsonConverter<SharedSymbol>
{ {
public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) public override SharedSymbol? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
@@ -41,4 +41,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
writer.WriteEndArray(); writer.WriteEndArray();
} }
} }
}
@@ -0,0 +1,376 @@
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,9 +1,11 @@
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 /> /// <inheritdoc />
public class SystemTextJsonMessageSerializer : IStringMessageSerializer public class SystemTextJsonMessageSerializer : IStringMessageSerializer
{ {
@@ -24,4 +26,3 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
#endif #endif
public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options); public string Serialize<T>(T message) => JsonSerializer.Serialize(message, _options);
} }
}
+20 -12
View File
@@ -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;net10.0</TargetFrameworks> <TargetFrameworks>netstandard2.0;netstandard2.1;net8.0;net9.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>10.7.0</PackageVersion> <PackageVersion>9.6.0</PackageVersion>
<AssemblyVersion>10.7.0</AssemblyVersion> <AssemblyVersion>9.6.0</AssemblyVersion>
<FileVersion>10.7.0</FileVersion> <FileVersion>9.6.0</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,10 +20,11 @@
<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>latest</LangVersion> <LangVersion>12.0</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>
@@ -40,24 +41,31 @@
<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="10.0.101"> <PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="9.0.0">
<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="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />
<PackageReference Include="System.Text.Json" Version="10.0.1" /> <PackageReference Include="System.Text.Json" Version="9.0.6" />
<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="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.6" />
<PackageReference Include="System.Threading.Channels" Version="10.0.1" /> </ItemGroup>
<ItemGroup>
<EditorConfigFiles Remove="C:\Projects\CryptoExchange.Net\CryptoExchange.Net\.editorconfig" />
</ItemGroup> </ItemGroup>
</Project> </Project>
@@ -1,24 +0,0 @@
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)
{
}
}
}
+16 -161
View File
@@ -1,17 +1,17 @@
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> /// <summary>
/// General helpers functions /// General helpers functions
/// </summary> /// </summary>
@@ -91,6 +91,8 @@ namespace CryptoExchange.Net
else value += (step.Value - offset); else value += (step.Value - offset);
} }
value = RoundDown(value, 8);
return value.Normalize(); return value.Normalize();
} }
@@ -243,7 +245,8 @@ namespace CryptoExchange.Net
/// <summary> /// <summary>
/// Generate a long value /// Generate a long value
/// </summary> /// </summary>
/// <param name="maxLength">Max number of digits</param> /// <param name="maxLength">Max character length</param>
/// <returns></returns>
public static long RandomLong(int maxLength) public static long RandomLong(int maxLength)
{ {
#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
@@ -259,25 +262,6 @@ namespace CryptoExchange.Net
return value; 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> /// <summary>
/// Generate a random string of specified length /// Generate a random string of specified length
/// </summary> /// </summary>
@@ -305,17 +289,17 @@ namespace CryptoExchange.Net
/// <summary> /// <summary>
/// Execute multiple requests to retrieve multiple pages of the result set /// Execute multiple requests to retrieve multiple pages of the result set
/// </summary> /// </summary>
/// <typeparam name="T">Type of the client</typeparam> /// <typeparam name="TResult">Type of the client</typeparam>
/// <typeparam name="U">Type of the request</typeparam> /// <typeparam name="TRequest">Type of the request</typeparam>
/// <param name="paginatedFunc">The func to execute with each request</param> /// <param name="paginatedFunc">The func to execute with each request</param>
/// <param name="request">The request parameters</param> /// <param name="request">The request parameters</param>
/// <param name="ct">Cancellation token</param> /// <param name="ct">Cancellation token</param>
/// <returns></returns> /// <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) 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<T>(); var result = new List<TResult>();
ExchangeWebResult<T[]> batch; ExchangeWebResult<TResult[]> batch;
PageRequest? nextPageToken = null; INextPageToken? nextPageToken = null;
while (true) while (true)
{ {
batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false); batch = await paginatedFunc(request, nextPageToken, ct).ConfigureAwait(false);
@@ -324,42 +308,12 @@ namespace CryptoExchange.Net
break; break;
result.AddRange(batch.Data); result.AddRange(batch.Data);
nextPageToken = batch.NextPageRequest; nextPageToken = batch.NextPageToken;
if (nextPageToken == null) if (nextPageToken == null)
break; break;
} }
} }
/// <summary>
/// Apply filters to the data set
/// </summary>
/// <typeparam name="T">Type</typeparam>
/// <param name="data">Data set</param>
/// <param name="timeSelector">Time selector for the data</param>
/// <param name="startTime">Start time filter</param>
/// <param name="endTime">End time filter</param>
/// <param name="direction">Data direction</param>
public static IEnumerable<T> ApplyFilter<T>(
IEnumerable<T> data,
Func<T, DateTime> timeSelector,
DateTime? startTime,
DateTime? endTime,
DataDirection direction)
{
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> /// <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 rules (price and quantity step size and decimals precision, min/max quantity) from the symbol to the quantity and price
/// </summary> /// </summary>
@@ -391,104 +345,6 @@ namespace CryptoExchange.Net
} }
/// <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> /// <summary>
/// Parse a decimal value from a string /// Parse a decimal value from a string
/// </summary> /// </summary>
@@ -532,4 +388,3 @@ namespace CryptoExchange.Net
return null; return null;
} }
} }
}
+3 -64
View File
@@ -1,11 +1,11 @@
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> /// <summary>
/// Cache for symbol parsing /// Cache for symbol parsing
/// </summary> /// </summary>
@@ -32,66 +32,6 @@ namespace CryptoExchange.Net
_symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol)); _symbolInfos[topicId] = new ExchangeInfo(DateTime.UtcNow, updateData.ToDictionary(x => x.Name, x => x.SharedSymbol));
} }
/// <summary>
/// Whether the specific topic has been cached
/// </summary>
/// <param name="topicId">Id</param>
public static bool HasCached(string topicId)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
return false;
return exchangeInfo.Symbols.Count > 0;
}
/// <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)
{
if (!_symbolInfos.TryGetValue(topicId, out var exchangeInfo))
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> /// <summary>
/// Parse a symbol name to a SharedSymbol /// Parse a symbol name to a SharedSymbol
/// </summary> /// </summary>
@@ -126,4 +66,3 @@ namespace CryptoExchange.Net
} }
} }
} }
}
+161 -136
View File
@@ -1,18 +1,18 @@
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.Globalization;
using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.IO;
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> /// <summary>
/// Helper methods /// Helper methods
/// </summary> /// </summary>
@@ -61,80 +61,30 @@ namespace CryptoExchange.Net
/// <returns></returns> /// <returns></returns>
public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType) public static string CreateParamString(this IDictionary<string, object> parameters, bool urlEncodeValues, ArrayParametersSerialization serializationType)
{ {
var uriString = new StringBuilder(); var uriString = string.Empty;
bool first = true; var arraysParameters = parameters.Where(p => p.Value.GetType().IsArray).ToList();
foreach(var parameter in parameters) foreach (var arrayEntry in arraysParameters)
{
if (!first)
uriString.Append("&");
first = false;
if (parameter.Value.GetType().IsArray)
{ {
if (serializationType == ArrayParametersSerialization.Array) if (serializationType == ArrayParametersSerialization.Array)
{ {
bool firstArrayValue = true; uriString += $"{string.Join("&", ((object[])(urlEncodeValues ? Uri.EscapeDataString(arrayEntry.Value.ToString()!) : arrayEntry.Value)).Select(v => $"{arrayEntry.Key}[]={string.Format(CultureInfo.InvariantCulture, "{0}", v)}"))}&";
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) else if (serializationType == ArrayParametersSerialization.MultipleValues)
{ {
bool firstArrayValue = true; var array = (Array)arrayEntry.Value;
foreach (var entry in (object[])parameter.Value) uriString += string.Join("&", array.OfType<object>().Select(a => $"{arrayEntry.Key}={Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", a))}"));
{ uriString += "&";
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 else
{ {
uriString.Append('['); var array = (Array)arrayEntry.Value;
var firstArrayEntry = true; uriString += $"{arrayEntry.Key}=[{string.Join(",", array.OfType<object>().Select(a => string.Format(CultureInfo.InvariantCulture, "{0}", a)))}]&";
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(); uriString += $"{string.Join("&", parameters.Where(p => !p.Value.GetType().IsArray).Select(s => $"{s.Key}={(urlEncodeValues ? Uri.EscapeDataString(string.Format(CultureInfo.InvariantCulture, "{0}", s.Value)) : string.Format(CultureInfo.InvariantCulture, "{0}", s.Value))}"))}";
uriString = uriString.TrimEnd('&');
return uriString;
} }
/// <summary> /// <summary>
@@ -280,39 +230,167 @@ namespace CryptoExchange.Net
/// <summary> /// <summary>
/// Append a base url with provided path /// Append a base url with provided path
/// </summary> /// </summary>
/// <param name="url"></param>
/// <param name="path"></param>
/// <returns></returns>
public static string AppendPath(this string url, params string[] path) public static string AppendPath(this string url, params string[] path)
{ {
var sb = new StringBuilder(url.TrimEnd('/')); if (!url.EndsWith("/"))
foreach (var subPath in path) url += "/";
{
sb.Append('/'); foreach (var item in path)
sb.Append(subPath.Trim('/')); url += item.Trim('/') + "/";
return url.TrimEnd('/');
} }
return sb.ToString(); /// <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)}]");
}
else
{
foreach (var item in (object[])parameter.Value)
{
if (arraySerialization == ArrayParametersSerialization.Array)
{
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, item.ToString());
}
}
}
}
else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <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, 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 (arraySerialization == ArrayParametersSerialization.JsonArray)
{
httpValueCollection.Add(parameter.Key, $"[{string.Join(",", (object[])parameter.Value)}]");
}
else
{
foreach (var item in (object[])parameter.Value)
{
if (arraySerialization == ArrayParametersSerialization.Array)
{
httpValueCollection.Add(parameter.Key + "[]", item.ToString());
}
else
{
httpValueCollection.Add(parameter.Key, item.ToString());
}
}
}
}
else
{
httpValueCollection.Add(parameter.Key, parameter.Value.ToString());
}
}
uriBuilder.Query = httpValueCollection.ToString();
return uriBuilder.Uri;
}
/// <summary>
/// Add parameter to URI
/// </summary>
/// <param name="uri"></param>
/// <param name="name"></param>
/// <param name="value"></param>
/// <returns></returns>
public static Uri AddQueryParameter(this Uri uri, string name, string value)
{
var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);
httpValueCollection.Remove(name);
httpValueCollection.Add(name, value);
var ub = new UriBuilder(uri);
ub.Query = httpValueCollection.ToString();
return ub.Uri;
} }
/// <summary> /// <summary>
/// Decompress using GzipStream /// Decompress using GzipStream
/// </summary> /// </summary>
public static ReadOnlySpan<byte> DecompressGzip(this ReadOnlySpan<byte> data) /// <param name="data"></param>
/// <returns></returns>
public static ReadOnlyMemory<byte> DecompressGzip(this ReadOnlyMemory<byte> data)
{ {
using var decompressedStream = new MemoryStream(); using var decompressedStream = new MemoryStream();
using var dataStream = MemoryMarshal.TryGetArray(data, out var arraySegment)
? new MemoryStream(arraySegment.Array!, arraySegment.Offset, arraySegment.Count)
: new MemoryStream(data.ToArray());
using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress); using var deflateStream = new GZipStream(new MemoryStream(data.ToArray()), CompressionMode.Decompress);
deflateStream.CopyTo(decompressedStream); deflateStream.CopyTo(decompressedStream);
return new ReadOnlySpan<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length); return new ReadOnlyMemory<byte>(decompressedStream.GetBuffer(), 0, (int)decompressedStream.Length);
} }
/// <summary> /// <summary>
/// Decompress using GzipStream /// Decompress using DeflateStream
/// </summary> /// </summary>
public static ReadOnlySpan<byte> Decompress(this ReadOnlySpan<byte> input) /// <param name="input"></param>
/// <returns></returns>
public static ReadOnlyMemory<byte> Decompress(this ReadOnlyMemory<byte> input)
{ {
using var output = new MemoryStream(); var output = new MemoryStream();
using var compressStream = new MemoryStream(input.ToArray());
using var decompressor = new DeflateStream(compressStream, CompressionMode.Decompress); 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;
return new ReadOnlyMemory<byte>(output.GetBuffer(), 0, (int)output.Length);
} }
/// <summary> /// <summary>
@@ -335,36 +413,6 @@ namespace CryptoExchange.Net
/// </summary> /// </summary>
public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear; public static bool IsDelivery(this TradingMode type) => type == TradingMode.DeliveryInverse || type == TradingMode.DeliveryLinear;
/// <summary>
/// Whether the account type is a futures account
/// </summary>
public static bool IsFuturesAccount(this SharedAccountType type) =>
type == SharedAccountType.PerpetualLinearFutures
|| type == SharedAccountType.DeliveryLinearFutures
|| type == SharedAccountType.PerpetualInverseFutures
|| type == SharedAccountType.DeliveryInverseFutures;
/// <summary>
/// Whether the account type is a margin account
/// </summary>
public static bool IsMarginAccount(this SharedAccountType type) =>
type == SharedAccountType.CrossMargin
|| type == SharedAccountType.IsolatedMargin;
/// <summary>
/// Map a TradingMode value to a SharedAccountType enum value
/// </summary>
public static SharedAccountType ToAccountType(this TradingMode mode)
{
if (mode == TradingMode.Spot) return SharedAccountType.Spot;
if (mode == TradingMode.PerpetualLinear) return SharedAccountType.PerpetualLinearFutures;
if (mode == TradingMode.PerpetualInverse) return SharedAccountType.PerpetualInverseFutures;
if (mode == TradingMode.DeliveryInverse) return SharedAccountType.DeliveryInverseFutures;
if (mode == TradingMode.DeliveryLinear) return SharedAccountType.DeliveryLinearFutures;
throw new ArgumentException(nameof(mode), "Unmapped trading mode");
}
/// <summary> /// <summary>
/// Register rest client interfaces /// Register rest client interfaces
/// </summary> /// </summary>
@@ -394,8 +442,6 @@ namespace CryptoExchange.Net
services.AddTransient(x => (IFeeRestClient)client(x)!); services.AddTransient(x => (IFeeRestClient)client(x)!);
if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T))) if (typeof(IBookTickerRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (IBookTickerRestClient)client(x)!); services.AddTransient(x => (IBookTickerRestClient)client(x)!);
if (typeof(ITransferRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ITransferRestClient)client(x)!);
if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T))) if (typeof(ISpotOrderRestClient).IsAssignableFrom(typeof(T)))
services.AddTransient(x => (ISpotOrderRestClient)client(x)!); services.AddTransient(x => (ISpotOrderRestClient)client(x)!);
@@ -470,26 +516,5 @@ namespace CryptoExchange.Net
return services; 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;
}
}
} }
@@ -1,47 +0,0 @@
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;
}
}
@@ -1,17 +0,0 @@
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);
}
}
@@ -1,17 +0,0 @@
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);
}
}
@@ -1,18 +0,0 @@
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; }
}
}
@@ -1,31 +0,0 @@
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; }
}
}
@@ -1,76 +0,0 @@
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();
}
}
@@ -1,63 +0,0 @@
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,7 +1,7 @@
using System; using System;
namespace CryptoExchange.Net.Interfaces;
namespace CryptoExchange.Net.Interfaces
{
/// <summary> /// <summary>
/// Time provider /// Time provider
/// </summary> /// </summary>
@@ -13,4 +13,3 @@ namespace CryptoExchange.Net.Interfaces
/// <returns></returns> /// <returns></returns>
DateTime GetTime(); DateTime GetTime();
} }
}
@@ -0,0 +1,46 @@
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;
}
@@ -0,0 +1,16 @@
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);
}
@@ -0,0 +1,16 @@
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);
}
@@ -1,13 +0,0 @@
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