diff --git a/CryptoExchange.Net/Clients/BaseApiClient.cs b/CryptoExchange.Net/Clients/BaseApiClient.cs
index b2b5418..57e6f17 100644
--- a/CryptoExchange.Net/Clients/BaseApiClient.cs
+++ b/CryptoExchange.Net/Clients/BaseApiClient.cs
@@ -1,10 +1,16 @@
using System;
using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
@@ -15,10 +21,18 @@ namespace CryptoExchange.Net
{
private ApiCredentials? _apiCredentials;
private AuthenticationProvider? _authenticationProvider;
- protected Log _log;
- protected bool _disposing;
private bool _created;
+ ///
+ /// Logger
+ ///
+ protected Log _log;
+
+ ///
+ /// If we are disposing
+ ///
+ protected bool _disposing;
+
///
/// The authentication provider for this API client. (null if no credentials are set)
///
@@ -77,6 +91,24 @@ namespace CryptoExchange.Net
///
public ApiClientOptions Options { get; }
+ ///
+ /// The last used id, use NextId() to get the next id and up this
+ ///
+ protected static int lastId;
+ ///
+ /// Lock for id generating
+ ///
+ protected static object idLock = new ();
+
+ ///
+ /// A default serializer
+ ///
+ private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
+ {
+ DateTimeZoneHandling = DateTimeZoneHandling.Utc,
+ Culture = CultureInfo.InvariantCulture
+ });
+
///
/// ctor
///
@@ -105,6 +137,212 @@ namespace CryptoExchange.Net
_authenticationProvider = null;
}
+ ///
+ /// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
+ ///
+ /// The data to parse
+ ///
+ protected CallResult ValidateJson(string data)
+ {
+ if (string.IsNullOrEmpty(data))
+ {
+ var info = "Empty data object received";
+ _log.Write(LogLevel.Error, info);
+ return new CallResult(new DeserializeError(info, data));
+ }
+
+ try
+ {
+ return new CallResult(JToken.Parse(data));
+ }
+ catch (JsonReaderException jre)
+ {
+ var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
+ return new CallResult(new DeserializeError(info, data));
+ }
+ catch (JsonSerializationException jse)
+ {
+ var info = $"Deserialize JsonSerializationException: {jse.Message}";
+ return new CallResult(new DeserializeError(info, data));
+ }
+ catch (Exception ex)
+ {
+ var exceptionInfo = ex.ToLogString();
+ var info = $"Deserialize Unknown Exception: {exceptionInfo}";
+ return new CallResult(new DeserializeError(info, data));
+ }
+ }
+
+ ///
+ /// Deserialize a string into an object
+ ///
+ /// The type to deserialize into
+ /// The data to deserialize
+ /// A specific serializer to use
+ /// Id of the request the data is returned from (used for grouping logging by request)
+ ///
+ protected CallResult Deserialize(string data, JsonSerializer? serializer = null, int? requestId = null)
+ {
+ var tokenResult = ValidateJson(data);
+ if (!tokenResult)
+ {
+ _log.Write(LogLevel.Error, tokenResult.Error!.Message);
+ return new CallResult(tokenResult.Error);
+ }
+
+ return Deserialize(tokenResult.Data, serializer, requestId);
+ }
+
+ ///
+ /// Deserialize a JToken into an object
+ ///
+ /// The type to deserialize into
+ /// The data to deserialize
+ /// A specific serializer to use
+ /// Id of the request the data is returned from (used for grouping logging by request)
+ ///
+ protected CallResult Deserialize(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
+ {
+ serializer ??= _defaultSerializer;
+
+ try
+ {
+ return new CallResult(obj.ToObject(serializer)!);
+ }
+ catch (JsonReaderException jre)
+ {
+ var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
+ _log.Write(LogLevel.Error, info);
+ return new CallResult(new DeserializeError(info, obj));
+ }
+ catch (JsonSerializationException jse)
+ {
+ var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
+ _log.Write(LogLevel.Error, info);
+ return new CallResult(new DeserializeError(info, obj));
+ }
+ catch (Exception ex)
+ {
+ var exceptionInfo = ex.ToLogString();
+ var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
+ _log.Write(LogLevel.Error, info);
+ return new CallResult(new DeserializeError(info, obj));
+ }
+ }
+
+ ///
+ /// Deserialize a stream into an object
+ ///
+ /// The type to deserialize into
+ /// The stream to deserialize
+ /// A specific serializer to use
+ /// Id of the request the data is returned from (used for grouping logging by request)
+ /// Milliseconds response time for the request this stream is a response for
+ ///
+ protected async Task> DeserializeAsync(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
+ {
+ serializer ??= _defaultSerializer;
+ string? data = null;
+
+ try
+ {
+ // Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
+ using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
+
+ // If we have to output the original json data or output the data into the logging we'll have to read to full response
+ // in order to log/return the json data
+ if (Options.OutputOriginalData == true || _log.Level == LogLevel.Trace)
+ {
+ data = await reader.ReadToEndAsync().ConfigureAwait(false);
+ _log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(_log.Level == LogLevel.Trace ? (": " + data) : "")}");
+ var result = Deserialize(data, serializer, requestId);
+ if (Options.OutputOriginalData == true)
+ result.OriginalData = data;
+ return result;
+ }
+
+ // If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
+ // into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
+ using var jsonReader = new JsonTextReader(reader);
+ _log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
+ return new CallResult(serializer.Deserialize(jsonReader)!);
+ }
+ catch (JsonReaderException jre)
+ {
+ if (data == null)
+ {
+ if (stream.CanSeek)
+ {
+ // If we can seek the stream rewind it so we can retrieve the original data that was sent
+ stream.Seek(0, SeekOrigin.Begin);
+ data = await ReadStreamAsync(stream).ConfigureAwait(false);
+ }
+ else
+ {
+ data = "[Data only available in Trace LogLevel]";
+ }
+ }
+ _log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
+ return new CallResult(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
+ }
+ catch (JsonSerializationException jse)
+ {
+ if (data == null)
+ {
+ if (stream.CanSeek)
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+ data = await ReadStreamAsync(stream).ConfigureAwait(false);
+ }
+ else
+ {
+ data = "[Data only available in Trace LogLevel]";
+ }
+ }
+
+ _log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
+ return new CallResult(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
+ }
+ catch (Exception ex)
+ {
+ if (data == null)
+ {
+ if (stream.CanSeek)
+ {
+ stream.Seek(0, SeekOrigin.Begin);
+ data = await ReadStreamAsync(stream).ConfigureAwait(false);
+ }
+ else
+ {
+ data = "[Data only available in Trace LogLevel]";
+ }
+ }
+
+ var exceptionInfo = ex.ToLogString();
+ _log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
+ return new CallResult(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
+ }
+ }
+
+ private static async Task ReadStreamAsync(Stream stream)
+ {
+ using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
+ return await reader.ReadToEndAsync().ConfigureAwait(false);
+ }
+
+ ///
+ /// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
+ ///
+ ///
+ protected static int NextId()
+ {
+ lock (idLock)
+ {
+ lastId += 1;
+ return lastId;
+ }
+ }
+
///
/// Dispose
///
diff --git a/CryptoExchange.Net/Clients/BaseClient.cs b/CryptoExchange.Net/Clients/BaseClient.cs
index e03e986..ec26ae0 100644
--- a/CryptoExchange.Net/Clients/BaseClient.cs
+++ b/CryptoExchange.Net/Clients/BaseClient.cs
@@ -2,14 +2,8 @@
using CryptoExchange.Net.Logging;
using CryptoExchange.Net.Objects;
using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
-using System.Globalization;
-using System.IO;
-using System.Text;
-using System.Threading.Tasks;
namespace CryptoExchange.Net
{
@@ -30,24 +24,7 @@ namespace CryptoExchange.Net
/// The log object
///
protected internal Log log;
- ///
- /// The last used id, use NextId() to get the next id and up this
- ///
- protected static int lastId;
- ///
- /// Lock for id generating
- ///
- protected static object idLock = new object();
-
- ///
- /// A default serializer
- ///
- private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
- {
- DateTimeZoneHandling = DateTimeZoneHandling.Utc,
- Culture = CultureInfo.InvariantCulture
- });
-
+
///
/// Provided client options
///
@@ -72,6 +49,16 @@ namespace CryptoExchange.Net
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
}
+ ///
+ /// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
+ ///
+ /// The credentials to set
+ public void SetApiCredentials(ApiCredentials credentials)
+ {
+ foreach (var apiClient in ApiClients)
+ apiClient.SetApiCredentials(credentials);
+ }
+
///
/// Register an API client
///
@@ -83,206 +70,6 @@ namespace CryptoExchange.Net
return apiClient;
}
- ///
- /// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
- ///
- /// The data to parse
- ///
- protected CallResult ValidateJson(string data)
- {
- if (string.IsNullOrEmpty(data))
- {
- var info = "Empty data object received";
- log.Write(LogLevel.Error, info);
- return new CallResult(new DeserializeError(info, data));
- }
-
- try
- {
- return new CallResult(JToken.Parse(data));
- }
- catch (JsonReaderException jre)
- {
- var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
- return new CallResult(new DeserializeError(info, data));
- }
- catch (JsonSerializationException jse)
- {
- var info = $"Deserialize JsonSerializationException: {jse.Message}";
- return new CallResult(new DeserializeError(info, data));
- }
- catch (Exception ex)
- {
- var exceptionInfo = ex.ToLogString();
- var info = $"Deserialize Unknown Exception: {exceptionInfo}";
- return new CallResult(new DeserializeError(info, data));
- }
- }
-
- ///
- /// Deserialize a string into an object
- ///
- /// The type to deserialize into
- /// The data to deserialize
- /// A specific serializer to use
- /// Id of the request the data is returned from (used for grouping logging by request)
- ///
- protected CallResult Deserialize(string data, JsonSerializer? serializer = null, int? requestId = null)
- {
- var tokenResult = ValidateJson(data);
- if (!tokenResult)
- {
- log.Write(LogLevel.Error, tokenResult.Error!.Message);
- return new CallResult( tokenResult.Error);
- }
-
- return Deserialize(tokenResult.Data, serializer, requestId);
- }
-
- ///
- /// Deserialize a JToken into an object
- ///
- /// The type to deserialize into
- /// The data to deserialize
- /// A specific serializer to use
- /// Id of the request the data is returned from (used for grouping logging by request)
- ///
- protected CallResult Deserialize(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
- {
- serializer ??= defaultSerializer;
-
- try
- {
- return new CallResult(obj.ToObject(serializer)!);
- }
- catch (JsonReaderException jre)
- {
- var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
- log.Write(LogLevel.Error, info);
- return new CallResult(new DeserializeError(info, obj));
- }
- catch (JsonSerializationException jse)
- {
- var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
- log.Write(LogLevel.Error, info);
- return new CallResult(new DeserializeError(info, obj));
- }
- catch (Exception ex)
- {
- var exceptionInfo = ex.ToLogString();
- var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
- log.Write(LogLevel.Error, info);
- return new CallResult(new DeserializeError(info, obj));
- }
- }
-
- ///
- /// Deserialize a stream into an object
- ///
- /// The type to deserialize into
- /// The stream to deserialize
- /// A specific serializer to use
- /// Id of the request the data is returned from (used for grouping logging by request)
- /// Milliseconds response time for the request this stream is a response for
- ///
- protected async Task> DeserializeAsync(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
- {
- serializer ??= defaultSerializer;
- string? data = null;
-
- try
- {
- // Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
- using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
-
- // If we have to output the original json data or output the data into the logging we'll have to read to full response
- // in order to log/return the json data
- if (ClientOptions.OutputOriginalData || log.Level == LogLevel.Trace)
- {
- data = await reader.ReadToEndAsync().ConfigureAwait(false);
- log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms{(log.Level == LogLevel.Trace ? (": " + data) : "")}");
- var result = Deserialize(data, serializer, requestId);
- if(ClientOptions.OutputOriginalData)
- result.OriginalData = data;
- return result;
- }
-
- // If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
- // into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
- using var jsonReader = new JsonTextReader(reader);
- log.Write(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] ": "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
- return new CallResult(serializer.Deserialize(jsonReader)!);
- }
- catch (JsonReaderException jre)
- {
- if (data == null)
- {
- if (stream.CanSeek)
- {
- // If we can seek the stream rewind it so we can retrieve the original data that was sent
- stream.Seek(0, SeekOrigin.Begin);
- data = await ReadStreamAsync(stream).ConfigureAwait(false);
- }
- else
- data = "[Data only available in Trace LogLevel]";
- }
- log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
- return new CallResult(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
- }
- catch (JsonSerializationException jse)
- {
- if (data == null)
- {
- if (stream.CanSeek)
- {
- stream.Seek(0, SeekOrigin.Begin);
- data = await ReadStreamAsync(stream).ConfigureAwait(false);
- }
- else
- data = "[Data only available in Trace LogLevel]";
- }
-
- log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
- return new CallResult(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
- }
- catch (Exception ex)
- {
- if (data == null)
- {
- if (stream.CanSeek)
- {
- stream.Seek(0, SeekOrigin.Begin);
- data = await ReadStreamAsync(stream).ConfigureAwait(false);
- }
- else
- data = "[Data only available in Trace LogLevel]";
- }
-
- var exceptionInfo = ex.ToLogString();
- log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
- return new CallResult(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
- }
- }
-
- private static async Task ReadStreamAsync(Stream stream)
- {
- using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
- return await reader.ReadToEndAsync().ConfigureAwait(false);
- }
-
- ///
- /// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
- ///
- ///
- protected static int NextId()
- {
- lock (idLock)
- {
- lastId += 1;
- return lastId;
- }
- }
-
///
/// Handle a change in the client options log config
///
diff --git a/CryptoExchange.Net/Clients/BaseRestClient.cs b/CryptoExchange.Net/Clients/BaseRestClient.cs
index c8a7629..da592c6 100644
--- a/CryptoExchange.Net/Clients/BaseRestClient.cs
+++ b/CryptoExchange.Net/Clients/BaseRestClient.cs
@@ -1,19 +1,8 @@
using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
using System.Linq;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
-using CryptoExchange.Net.Requests;
-using Microsoft.Extensions.Logging;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
@@ -22,475 +11,18 @@ namespace CryptoExchange.Net
///
public abstract class BaseRestClient : BaseClient, IRestClient
{
- ///
- /// The factory for creating requests. Used for unit testing
- ///
- public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
-
///
public int TotalRequestsMade => ApiClients.OfType().Sum(s => s.TotalRequestsMade);
- ///
- /// Request headers to be sent with each request
- ///
- protected Dictionary? StandardRequestHeaders { get; set; }
-
- ///
- /// Client options
- ///
- public new BaseRestClientOptions ClientOptions { get; }
-
///
/// ctor
///
/// The name of the API this client is for
/// The options for this client
- protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
+ protected BaseRestClient(string name, ClientOptions options) : base(name, options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
-
- ClientOptions = options;
- RequestFactory.Configure(options.RequestTimeout, options.Proxy, options.HttpClient);
- }
-
- ///
- public void SetApiCredentials(ApiCredentials credentials)
- {
- foreach (var apiClient in ApiClients)
- apiClient.SetApiCredentials(credentials);
- }
-
- ///
- /// Execute a request to the uri and returns if it was successful
- ///
- /// The API client the request is for
- /// The uri to send the request to
- /// The method of the request
- /// Cancellation token
- /// The parameters of the request
- /// Whether or not the request should be authenticated
- /// Where the parameters should be placed, overwrites the value set in the client
- /// How array parameters should be serialized, overwrites the value set in the client
- /// Credits used for the request
- /// The JsonSerializer to use for deserialization
- /// Additional headers to send with the request
- /// Ignore rate limits for this request
- ///
- [return: NotNull]
- protected virtual async Task SendRequestAsync(RestApiClient apiClient,
- Uri uri,
- HttpMethod method,
- CancellationToken cancellationToken,
- Dictionary? parameters = null,
- bool signed = false,
- HttpMethodParameterPosition? parameterPosition = null,
- ArrayParametersSerialization? arraySerialization = null,
- int requestWeight = 1,
- JsonSerializer? deserializer = null,
- Dictionary? additionalHeaders = null,
- bool ignoreRatelimit = false)
- {
- var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
- if (!request)
- return new WebCallResult(request.Error!);
-
- var result = await GetResponseAsync