using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using CryptoExchange.Net.Authentication;
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Options;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace CryptoExchange.Net
{
///
/// Base API for all API clients
///
public abstract class BaseApiClient : IDisposable, IBaseApiClient
{
///
/// Logger
///
protected ILogger _logger;
///
/// If we are disposing
///
protected bool _disposing;
///
/// The authentication provider for this API client. (null if no credentials are set)
///
public AuthenticationProvider? AuthenticationProvider { get; private set; }
///
/// Where to put the parameters for requests with different Http methods
///
public Dictionary ParameterPositions { get; set; } = new Dictionary
{
{ HttpMethod.Get, HttpMethodParameterPosition.InUri },
{ HttpMethod.Post, HttpMethodParameterPosition.InBody },
{ HttpMethod.Delete, HttpMethodParameterPosition.InBody },
{ HttpMethod.Put, HttpMethodParameterPosition.InBody }
};
///
/// Request body content type
///
public RequestBodyFormat requestBodyFormat = RequestBodyFormat.Json;
///
/// Whether or not we need to manually parse an error instead of relying on the http status code
///
public bool manualParseError = false;
///
/// How to serialize array parameters when making requests
///
public ArrayParametersSerialization arraySerialization = ArrayParametersSerialization.Array;
///
/// What request body should be set when no data is send (only used in combination with postParametersPosition.InBody)
///
public string requestBodyEmptyContent = "{}";
///
/// The environment this client communicates to
///
public string BaseAddress { get; }
///
/// Output the original string data along with the deserialized object
///
public bool OutputOriginalData { get; }
///
/// A default serializer
///
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
{
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
Culture = CultureInfo.InvariantCulture
});
///
/// Api options
///
public ApiOptions ApiOptions { get; }
///
/// Client Options
///
public ExchangeOptions ClientOptions { get; }
///
/// ctor
///
/// Logger
/// Should data from this client include the orginal data in the call result
/// Base address for this API client
/// Api credentials
/// Client options
/// Api options
protected BaseApiClient(ILogger logger, bool outputOriginalData, ApiCredentials? apiCredentials, string baseAddress, ExchangeOptions clientOptions, ApiOptions apiOptions)
{
_logger = logger;
ClientOptions = clientOptions;
ApiOptions = apiOptions;
OutputOriginalData = outputOriginalData;
BaseAddress = baseAddress;
if (apiCredentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(apiCredentials.Copy());
}
}
///
/// Create an AuthenticationProvider implementation instance based on the provided credentials
///
///
///
protected abstract AuthenticationProvider CreateAuthenticationProvider(ApiCredentials credentials);
///
public void SetApiCredentials(T credentials) where T : ApiCredentials
{
if (credentials != null)
{
AuthenticationProvider?.Dispose();
AuthenticationProvider = CreateAuthenticationProvider(credentials.Copy());
}
}
///
/// 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";
_logger.Log(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)
{
_logger.Log(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}";
_logger.Log(LogLevel.Error, info);
return new CallResult(new DeserializeError(info, obj));
}
catch (JsonSerializationException jse)
{
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
_logger.Log(LogLevel.Error, info);
return new CallResult(new DeserializeError(info, obj));
}
catch (Exception ex)
{
var exceptionInfo = ex.ToLogString();
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
_logger.Log(LogLevel.Error, info);
return new CallResult(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 (OutputOriginalData == true)
{
data = await reader.ReadToEndAsync().ConfigureAwait(false);
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms: " + data);
var result = Deserialize(data, serializer, requestId);
result.OriginalData = data;
return result;
}
// If we don't have to keep track of the original json data we can use the JsonTextReader to deserialize the stream directly
// into the desired object, which has increased performance over first reading the string value into memory and deserializing from that
using var jsonReader = new JsonTextReader(reader);
_logger.Log(LogLevel.Debug, $"{(requestId != null ? $"[{requestId}] " : "")}Response received{(elapsedMilliseconds != null ? $" in {elapsedMilliseconds}" : " ")}ms");
return new CallResult(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]";
}
}
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {data}");
return new CallResult(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
}
catch (JsonSerializationException jse)
{
if (data == null)
{
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
{
data = "[Data only available in Trace LogLevel]";
}
}
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
return new CallResult(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
}
catch (Exception ex)
{
if (data == null)
{
if (stream.CanSeek)
{
stream.Seek(0, SeekOrigin.Begin);
data = await ReadStreamAsync(stream).ConfigureAwait(false);
}
else
{
data = "[Data only available in Trace LogLevel]";
}
}
var exceptionInfo = ex.ToLogString();
_logger.Log(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
return new CallResult(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);
}
///
/// Dispose
///
public virtual void Dispose()
{
_disposing = true;
AuthenticationProvider?.Dispose();
}
}
}