mirror of
https://github.com/JKorf/CryptoExchange.Net
synced 2025-06-09 00:46:19 +00:00
wip
This commit is contained in:
parent
0d3e05880a
commit
66ac2972d6
@ -1,10 +1,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@ -15,10 +21,18 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
private ApiCredentials? _apiCredentials;
|
private ApiCredentials? _apiCredentials;
|
||||||
private AuthenticationProvider? _authenticationProvider;
|
private AuthenticationProvider? _authenticationProvider;
|
||||||
protected Log _log;
|
|
||||||
protected bool _disposing;
|
|
||||||
private bool _created;
|
private bool _created;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logger
|
||||||
|
/// </summary>
|
||||||
|
protected Log _log;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If we are disposing
|
||||||
|
/// </summary>
|
||||||
|
protected bool _disposing;
|
||||||
|
|
||||||
/// <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>
|
||||||
@ -77,6 +91,24 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public ApiClientOptions Options { get; }
|
public ApiClientOptions Options { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The last used id, use NextId() to get the next id and up this
|
||||||
|
/// </summary>
|
||||||
|
protected static int lastId;
|
||||||
|
/// <summary>
|
||||||
|
/// Lock for id generating
|
||||||
|
/// </summary>
|
||||||
|
protected static object idLock = new ();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A default serializer
|
||||||
|
/// </summary>
|
||||||
|
private static readonly JsonSerializer _defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
||||||
|
Culture = CultureInfo.InvariantCulture
|
||||||
|
});
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -105,6 +137,212 @@ namespace CryptoExchange.Net
|
|||||||
_authenticationProvider = null;
|
_authenticationProvider = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">The data to parse</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<JToken> ValidateJson(string data)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(data))
|
||||||
|
{
|
||||||
|
var info = "Empty data object received";
|
||||||
|
_log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new CallResult<JToken>(JToken.Parse(data));
|
||||||
|
}
|
||||||
|
catch (JsonReaderException jre)
|
||||||
|
{
|
||||||
|
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var exceptionInfo = ex.ToLogString();
|
||||||
|
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
||||||
|
return new CallResult<JToken>(new DeserializeError(info, data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a string into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="data">The data to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
||||||
|
{
|
||||||
|
var tokenResult = ValidateJson(data);
|
||||||
|
if (!tokenResult)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
||||||
|
return new CallResult<T>(tokenResult.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a JToken into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="obj">The data to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
||||||
|
{
|
||||||
|
serializer ??= _defaultSerializer;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException jre)
|
||||||
|
{
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
||||||
|
_log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
||||||
|
_log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var exceptionInfo = ex.ToLogString();
|
||||||
|
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
||||||
|
_log.Write(LogLevel.Error, info);
|
||||||
|
return new CallResult<T>(new DeserializeError(info, obj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deserialize a stream into an object
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="stream">The stream to deserialize</param>
|
||||||
|
/// <param name="serializer">A specific serializer to use</param>
|
||||||
|
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
||||||
|
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
||||||
|
{
|
||||||
|
serializer ??= _defaultSerializer;
|
||||||
|
string? data = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
||||||
|
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||||
|
|
||||||
|
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
||||||
|
// in order to log/return the json data
|
||||||
|
if (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<T>(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<T>(serializer.Deserialize<T>(jsonReader)!);
|
||||||
|
}
|
||||||
|
catch (JsonReaderException jre)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = "[Data only available in Trace LogLevel]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_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<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
||||||
|
}
|
||||||
|
catch (JsonSerializationException jse)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = "[Data only available in Trace LogLevel]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
||||||
|
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
if (stream.CanSeek)
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
data = "[Data only available in Trace LogLevel]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var exceptionInfo = ex.ToLogString();
|
||||||
|
_log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
||||||
|
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string> ReadStreamAsync(Stream stream)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
||||||
|
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected static int NextId()
|
||||||
|
{
|
||||||
|
lock (idLock)
|
||||||
|
{
|
||||||
|
lastId += 1;
|
||||||
|
return lastId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Dispose
|
/// Dispose
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -2,14 +2,8 @@
|
|||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System.IO;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@ -30,23 +24,6 @@ namespace CryptoExchange.Net
|
|||||||
/// The log object
|
/// The log object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected internal Log log;
|
protected internal Log log;
|
||||||
/// <summary>
|
|
||||||
/// The last used id, use NextId() to get the next id and up this
|
|
||||||
/// </summary>
|
|
||||||
protected static int lastId;
|
|
||||||
/// <summary>
|
|
||||||
/// Lock for id generating
|
|
||||||
/// </summary>
|
|
||||||
protected static object idLock = new object();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A default serializer
|
|
||||||
/// </summary>
|
|
||||||
private static readonly JsonSerializer defaultSerializer = JsonSerializer.Create(new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
|
|
||||||
Culture = CultureInfo.InvariantCulture
|
|
||||||
});
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provided client options
|
/// 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}");
|
log.Write(LogLevel.Trace, $"Client configuration: {options}, CryptoExchange.Net: v{typeof(BaseClient).Assembly.GetName().Version}, {name}.Net: v{GetType().Assembly.GetName().Version}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="credentials">The credentials to set</param>
|
||||||
|
public void SetApiCredentials(ApiCredentials credentials)
|
||||||
|
{
|
||||||
|
foreach (var apiClient in ApiClients)
|
||||||
|
apiClient.SetApiCredentials(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Register an API client
|
/// Register an API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -83,206 +70,6 @@ namespace CryptoExchange.Net
|
|||||||
return apiClient;
|
return apiClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to parse the json data and return a JToken, validating the input not being empty and being valid json
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="data">The data to parse</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected CallResult<JToken> ValidateJson(string data)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(data))
|
|
||||||
{
|
|
||||||
var info = "Empty data object received";
|
|
||||||
log.Write(LogLevel.Error, info);
|
|
||||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new CallResult<JToken>(JToken.Parse(data));
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}";
|
|
||||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
var info = $"Deserialize JsonSerializationException: {jse.Message}";
|
|
||||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
var info = $"Deserialize Unknown Exception: {exceptionInfo}";
|
|
||||||
return new CallResult<JToken>(new DeserializeError(info, data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize a string into an object
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="data">The data to deserialize</param>
|
|
||||||
/// <param name="serializer">A specific serializer to use</param>
|
|
||||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected CallResult<T> Deserialize<T>(string data, JsonSerializer? serializer = null, int? requestId = null)
|
|
||||||
{
|
|
||||||
var tokenResult = ValidateJson(data);
|
|
||||||
if (!tokenResult)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Error, tokenResult.Error!.Message);
|
|
||||||
return new CallResult<T>( tokenResult.Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Deserialize<T>(tokenResult.Data, serializer, requestId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize a JToken into an object
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="obj">The data to deserialize</param>
|
|
||||||
/// <param name="serializer">A specific serializer to use</param>
|
|
||||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected CallResult<T> Deserialize<T>(JToken obj, JsonSerializer? serializer = null, int? requestId = null)
|
|
||||||
{
|
|
||||||
serializer ??= defaultSerializer;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new CallResult<T>(obj.ToObject<T>(serializer)!);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonReaderException: {jre.Message} Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}, data: {obj}";
|
|
||||||
log.Write(LogLevel.Error, info);
|
|
||||||
return new CallResult<T>(new DeserializeError(info, obj));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message} data: {obj}";
|
|
||||||
log.Write(LogLevel.Error, info);
|
|
||||||
return new CallResult<T>(new DeserializeError(info, obj));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
var info = $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {obj}";
|
|
||||||
log.Write(LogLevel.Error, info);
|
|
||||||
return new CallResult<T>(new DeserializeError(info, obj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Deserialize a stream into an object
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="stream">The stream to deserialize</param>
|
|
||||||
/// <param name="serializer">A specific serializer to use</param>
|
|
||||||
/// <param name="requestId">Id of the request the data is returned from (used for grouping logging by request)</param>
|
|
||||||
/// <param name="elapsedMilliseconds">Milliseconds response time for the request this stream is a response for</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected async Task<CallResult<T>> DeserializeAsync<T>(Stream stream, JsonSerializer? serializer = null, int? requestId = null, long? elapsedMilliseconds = null)
|
|
||||||
{
|
|
||||||
serializer ??= defaultSerializer;
|
|
||||||
string? data = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Let the reader keep the stream open so we're able to seek if needed. The calling method will close the stream.
|
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
|
||||||
|
|
||||||
// If we have to output the original json data or output the data into the logging we'll have to read to full response
|
|
||||||
// in order to log/return the json data
|
|
||||||
if (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<T>(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<T>(serializer.Deserialize<T>(jsonReader)!);
|
|
||||||
}
|
|
||||||
catch (JsonReaderException jre)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
if (stream.CanSeek)
|
|
||||||
{
|
|
||||||
// If we can seek the stream rewind it so we can retrieve the original data that was sent
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
data = "[Data only available in Trace LogLevel]";
|
|
||||||
}
|
|
||||||
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<T>(new DeserializeError($"Deserialize JsonReaderException: {jre.Message}, Path: {jre.Path}, LineNumber: {jre.LineNumber}, LinePosition: {jre.LinePosition}", data));
|
|
||||||
}
|
|
||||||
catch (JsonSerializationException jse)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
if (stream.CanSeek)
|
|
||||||
{
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
data = "[Data only available in Trace LogLevel]";
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize JsonSerializationException: {jse.Message}, data: {data}");
|
|
||||||
return new CallResult<T>(new DeserializeError($"Deserialize JsonSerializationException: {jse.Message}", data));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
if (stream.CanSeek)
|
|
||||||
{
|
|
||||||
stream.Seek(0, SeekOrigin.Begin);
|
|
||||||
data = await ReadStreamAsync(stream).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
data = "[Data only available in Trace LogLevel]";
|
|
||||||
}
|
|
||||||
|
|
||||||
var exceptionInfo = ex.ToLogString();
|
|
||||||
log.Write(LogLevel.Error, $"{(requestId != null ? $"[{requestId}] " : "")}Deserialize Unknown Exception: {exceptionInfo}, data: {data}");
|
|
||||||
return new CallResult<T>(new DeserializeError($"Deserialize Unknown Exception: {exceptionInfo}", data));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task<string> ReadStreamAsync(Stream stream)
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(stream, Encoding.UTF8, false, 512, true);
|
|
||||||
return await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Generate a new unique id. The id is staticly stored so it is guarenteed to be unique across different client instances
|
|
||||||
/// </summary>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected static int NextId()
|
|
||||||
{
|
|
||||||
lock (idLock)
|
|
||||||
{
|
|
||||||
lastId += 1;
|
|
||||||
return lastId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handle a change in the client options log config
|
/// Handle a change in the client options log config
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,19 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Requests;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@ -22,475 +11,18 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class BaseRestClient : BaseClient, IRestClient
|
public abstract class BaseRestClient : BaseClient, IRestClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// The factory for creating requests. Used for unit testing
|
|
||||||
/// </summary>
|
|
||||||
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
public int TotalRequestsMade => ApiClients.OfType<RestApiClient>().Sum(s => s.TotalRequestsMade);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Request headers to be sent with each request
|
|
||||||
/// </summary>
|
|
||||||
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Client options
|
|
||||||
/// </summary>
|
|
||||||
public new BaseRestClientOptions ClientOptions { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the API this client is for</param>
|
/// <param name="name">The name of the API this client is for</param>
|
||||||
/// <param name="options">The options for this client</param>
|
/// <param name="options">The options for this client</param>
|
||||||
protected BaseRestClient(string name, BaseRestClientOptions options) : base(name, options)
|
protected BaseRestClient(string name, ClientOptions options) : base(name, options)
|
||||||
{
|
{
|
||||||
if (options == null)
|
if (options == null)
|
||||||
throw new ArgumentNullException(nameof(options));
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
ClientOptions = options;
|
|
||||||
RequestFactory.Configure(options.RequestTimeout, options.Proxy, options.HttpClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SetApiCredentials(ApiCredentials credentials)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetApiCredentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a request to the uri and returns if it was successful
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The API client the request is for</param>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNull]
|
|
||||||
protected virtual async Task<WebCallResult> SendRequestAsync(RestApiClient apiClient,
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
JsonSerializer? deserializer = null,
|
|
||||||
Dictionary<string, string>? 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<object>(apiClient, request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
|
||||||
return result.AsDataless();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="T">The type to deserialize into</typeparam>
|
|
||||||
/// <param name="apiClient">The API client the request is for</param>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
[return: NotNull]
|
|
||||||
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
|
||||||
RestApiClient apiClient,
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
JsonSerializer? deserializer = null,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
bool ignoreRatelimit = false
|
|
||||||
) where T : class
|
|
||||||
{
|
|
||||||
var request = await PrepareRequestAsync(apiClient, uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
|
||||||
if (!request)
|
|
||||||
return new WebCallResult<T>(request.Error!);
|
|
||||||
|
|
||||||
return await GetResponseAsync<T>(apiClient, request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Prepares a request to be sent to the server
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The API client the request is for</param>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
|
||||||
/// <param name="requestWeight">Credits used for the request</param>
|
|
||||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(RestApiClient apiClient,
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
Dictionary<string, object>? parameters = null,
|
|
||||||
bool signed = false,
|
|
||||||
HttpMethodParameterPosition? parameterPosition = null,
|
|
||||||
ArrayParametersSerialization? arraySerialization = null,
|
|
||||||
int requestWeight = 1,
|
|
||||||
JsonSerializer? deserializer = null,
|
|
||||||
Dictionary<string, string>? additionalHeaders = null,
|
|
||||||
bool ignoreRatelimit = false)
|
|
||||||
{
|
|
||||||
var requestId = NextId();
|
|
||||||
|
|
||||||
if (signed)
|
|
||||||
{
|
|
||||||
var syncTask = apiClient.SyncTimeAsync();
|
|
||||||
var timeSyncInfo = apiClient.GetTimeSyncInfo();
|
|
||||||
if (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)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
|
||||||
return syncTimeResult.As<IRequest>(default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ignoreRatelimit)
|
|
||||||
{
|
|
||||||
foreach (var limiter in apiClient.RateLimiters)
|
|
||||||
{
|
|
||||||
var limitResult = await limiter.LimitRequestAsync(log, uri.AbsolutePath, method, signed, apiClient.Options.ApiCredentials?.Key, apiClient.Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
|
||||||
if (!limitResult.Success)
|
|
||||||
return new CallResult<IRequest>(limitResult.Error!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signed && apiClient.AuthenticationProvider == null)
|
|
||||||
{
|
|
||||||
log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
|
||||||
return new CallResult<IRequest>(new NoApiCredentialsError());
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
|
||||||
var paramsPosition = parameterPosition ?? apiClient.ParameterPositions[method];
|
|
||||||
var request = ConstructRequest(apiClient, uri, method, parameters, signed, paramsPosition, arraySerialization ?? apiClient.arraySerialization, requestId, additionalHeaders);
|
|
||||||
|
|
||||||
string? paramString = "";
|
|
||||||
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
|
||||||
paramString = $" with request body '{request.Content}'";
|
|
||||||
|
|
||||||
var headers = request.GetHeaders();
|
|
||||||
if (headers.Any())
|
|
||||||
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
|
||||||
|
|
||||||
apiClient.TotalRequestsMade++;
|
|
||||||
log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
|
||||||
return new CallResult<IRequest>(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Executes the request and returns the result deserialized into the type parameter class
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The client making the request</param>
|
|
||||||
/// <param name="request">The request object to execute</param>
|
|
||||||
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
|
||||||
/// <param name="cancellationToken">Cancellation token</param>
|
|
||||||
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
|
||||||
BaseApiClient apiClient,
|
|
||||||
IRequest request,
|
|
||||||
JsonSerializer? deserializer,
|
|
||||||
CancellationToken cancellationToken,
|
|
||||||
bool expectedEmptyResponse)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var sw = Stopwatch.StartNew();
|
|
||||||
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
|
||||||
sw.Stop();
|
|
||||||
var statusCode = response.StatusCode;
|
|
||||||
var headers = response.ResponseHeaders;
|
|
||||||
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
|
||||||
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
|
|
||||||
if (apiClient.manualParseError)
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(responseStream);
|
|
||||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
responseStream.Close();
|
|
||||||
response.Close();
|
|
||||||
log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(log.Level == LogLevel.Trace ? (": "+data): "")}");
|
|
||||||
|
|
||||||
if (!expectedEmptyResponse)
|
|
||||||
{
|
|
||||||
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
|
||||||
var parseResult = ValidateJson(data);
|
|
||||||
if (!parseResult.Success)
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
|
||||||
|
|
||||||
// Let the library implementation see if it is an error response, and if so parse the error
|
|
||||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
|
||||||
if (error != null)
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
|
||||||
|
|
||||||
// Not an error, so continue deserializing
|
|
||||||
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrEmpty(data))
|
|
||||||
{
|
|
||||||
var parseResult = ValidateJson(data);
|
|
||||||
if (!parseResult.Success)
|
|
||||||
// Not empty, and not json
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
|
||||||
|
|
||||||
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
|
||||||
if (error != null)
|
|
||||||
// Error response
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Empty success response; okay
|
|
||||||
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, ClientOptions.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (expectedEmptyResponse)
|
|
||||||
{
|
|
||||||
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
|
|
||||||
responseStream.Close();
|
|
||||||
response.Close();
|
|
||||||
|
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
|
||||||
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
|
||||||
responseStream.Close();
|
|
||||||
response.Close();
|
|
||||||
|
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, ClientOptions.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Http status code indicates error
|
|
||||||
using var reader = new StreamReader(responseStream);
|
|
||||||
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
|
||||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
|
||||||
responseStream.Close();
|
|
||||||
response.Close();
|
|
||||||
var parseResult = ValidateJson(data);
|
|
||||||
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
|
||||||
if(error.Code == null || error.Code == 0)
|
|
||||||
error.Code = (int)response.StatusCode;
|
|
||||||
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpRequestException requestException)
|
|
||||||
{
|
|
||||||
// Request exception, can't reach server for instance
|
|
||||||
var exceptionInfo = requestException.ToLogString();
|
|
||||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
|
||||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException canceledException)
|
|
||||||
{
|
|
||||||
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
|
||||||
{
|
|
||||||
// Cancellation token canceled by caller
|
|
||||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
|
||||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Request timed out
|
|
||||||
log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
|
||||||
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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.
|
|
||||||
/// When setting manualParseError to true 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="data">Received data</param>
|
|
||||||
/// <returns>Null if not an error, Error otherwise</returns>
|
|
||||||
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
|
|
||||||
{
|
|
||||||
return Task.FromResult<ServerError?>(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Creates a request object
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The API client the request is for</param>
|
|
||||||
/// <param name="uri">The uri to send the request to</param>
|
|
||||||
/// <param name="method">The method of the request</param>
|
|
||||||
/// <param name="parameters">The parameters of the request</param>
|
|
||||||
/// <param name="signed">Whether or not the request should be authenticated</param>
|
|
||||||
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
|
||||||
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
|
||||||
/// <param name="requestId">Unique id of a request</param>
|
|
||||||
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual IRequest ConstructRequest(
|
|
||||||
RestApiClient apiClient,
|
|
||||||
Uri uri,
|
|
||||||
HttpMethod method,
|
|
||||||
Dictionary<string, object>? parameters,
|
|
||||||
bool signed,
|
|
||||||
HttpMethodParameterPosition parameterPosition,
|
|
||||||
ArrayParametersSerialization arraySerialization,
|
|
||||||
int requestId,
|
|
||||||
Dictionary<string, string>? additionalHeaders)
|
|
||||||
{
|
|
||||||
parameters ??= new Dictionary<string, object>();
|
|
||||||
|
|
||||||
for (var i = 0; i< parameters.Count; i++)
|
|
||||||
{
|
|
||||||
var kvp = parameters.ElementAt(i);
|
|
||||||
if (kvp.Value is Func<object> delegateValue)
|
|
||||||
parameters[kvp.Key] = delegateValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
|
||||||
{
|
|
||||||
foreach (var parameter in parameters)
|
|
||||||
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers = new Dictionary<string, string>();
|
|
||||||
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
|
||||||
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
|
||||||
if (apiClient.AuthenticationProvider != null)
|
|
||||||
apiClient.AuthenticationProvider.AuthenticateRequest(
|
|
||||||
apiClient,
|
|
||||||
uri,
|
|
||||||
method,
|
|
||||||
parameters,
|
|
||||||
signed,
|
|
||||||
arraySerialization,
|
|
||||||
parameterPosition,
|
|
||||||
out uriParameters,
|
|
||||||
out bodyParameters,
|
|
||||||
out headers);
|
|
||||||
|
|
||||||
// Sanity check
|
|
||||||
foreach(var param in parameters)
|
|
||||||
{
|
|
||||||
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
|
||||||
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
|
||||||
$"should return provided parameters in either the uri or body parameters output");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
|
||||||
uri = uri.SetParameters(uriParameters, arraySerialization);
|
|
||||||
|
|
||||||
var request = RequestFactory.Create(method, uri, requestId);
|
|
||||||
request.Accept = Constants.JsonContentHeader;
|
|
||||||
|
|
||||||
foreach (var header in headers)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
|
|
||||||
if (additionalHeaders != null)
|
|
||||||
{
|
|
||||||
foreach (var header in additionalHeaders)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StandardRequestHeaders != null)
|
|
||||||
{
|
|
||||||
foreach (var header in StandardRequestHeaders)
|
|
||||||
// Only add it if it isn't overwritten
|
|
||||||
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
|
||||||
request.AddHeader(header.Key, header.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
|
||||||
{
|
|
||||||
var contentType = apiClient.requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
|
||||||
if (bodyParameters.Any())
|
|
||||||
WriteParamBody(apiClient, request, bodyParameters, contentType);
|
|
||||||
else
|
|
||||||
request.SetContent(apiClient.requestBodyEmptyContent, contentType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Writes the parameters of the request to the request object body
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="apiClient">The client making the request</param>
|
|
||||||
/// <param name="request">The request to set the parameters on</param>
|
|
||||||
/// <param name="parameters">The parameters to set</param>
|
|
||||||
/// <param name="contentType">The content type of the data</param>
|
|
||||||
protected virtual void WriteParamBody(BaseApiClient apiClient, IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
|
||||||
{
|
|
||||||
if (apiClient.requestBodyFormat == RequestBodyFormat.Json)
|
|
||||||
{
|
|
||||||
// Write the parameters as json in the body
|
|
||||||
var stringData = JsonConvert.SerializeObject(parameters);
|
|
||||||
request.SetContent(stringData, contentType);
|
|
||||||
}
|
|
||||||
else if (apiClient.requestBodyFormat == RequestBodyFormat.FormData)
|
|
||||||
{
|
|
||||||
// Write the parameters as form data in the body
|
|
||||||
var stringData = parameters.ToFormData();
|
|
||||||
request.SetContent(stringData, contentType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="error">The string the request returned</param>
|
|
||||||
/// <returns></returns>
|
|
||||||
protected virtual Error ParseErrorResponse(JToken error)
|
|
||||||
{
|
|
||||||
return new ServerError(error.ToString());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Authentication;
|
using CryptoExchange.Net.Authentication;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
using CryptoExchange.Net.Sockets;
|
using CryptoExchange.Net.Sockets;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@ -31,6 +26,8 @@ namespace CryptoExchange.Net
|
|||||||
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
public int CurrentConnections => ApiClients.OfType<SocketApiClient>().Sum(c => c.CurrentConnections);
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
public int CurrentSubscriptions => ApiClients.OfType<SocketApiClient>().Sum(s => s.CurrentSubscriptions);
|
||||||
|
/// <inheritdoc />
|
||||||
|
public double IncomingKbps => ApiClients.OfType<SocketApiClient>().Sum(s => s.IncomingKbps);
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -42,13 +39,6 @@ namespace CryptoExchange.Net
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void SetApiCredentials(ApiCredentials credentials)
|
|
||||||
{
|
|
||||||
foreach (var apiClient in ApiClients)
|
|
||||||
apiClient.SetApiCredentials(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Unsubscribe an update subscription
|
/// Unsubscribe an update subscription
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -1,11 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
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;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CryptoExchange.Net.Interfaces;
|
using CryptoExchange.Net.Interfaces;
|
||||||
using CryptoExchange.Net.Logging;
|
using CryptoExchange.Net.Logging;
|
||||||
using CryptoExchange.Net.Objects;
|
using CryptoExchange.Net.Objects;
|
||||||
|
using CryptoExchange.Net.Requests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace CryptoExchange.Net
|
namespace CryptoExchange.Net
|
||||||
{
|
{
|
||||||
@ -14,6 +22,16 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class RestApiClient: BaseApiClient
|
public abstract class RestApiClient: BaseApiClient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The factory for creating requests. Used for unit testing
|
||||||
|
/// </summary>
|
||||||
|
public IRequestFactory RequestFactory { get; set; } = new RequestFactory();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Request headers to be sent with each request
|
||||||
|
/// </summary>
|
||||||
|
protected Dictionary<string, string>? StandardRequestHeaders { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get time sync info for an API client
|
/// Get time sync info for an API client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -41,17 +59,455 @@ namespace CryptoExchange.Net
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
internal IEnumerable<IRateLimiter> RateLimiters { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options
|
||||||
|
/// </summary>
|
||||||
|
internal ClientOptions ClientOptions { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="log">Logger</param>
|
||||||
/// <param name="options">The base client options</param>
|
/// <param name="options">The base client options</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public RestApiClient(BaseRestClientOptions options, RestApiClientOptions apiOptions): base(options, apiOptions)
|
public RestApiClient(Log log, ClientOptions options, RestApiClientOptions apiOptions): base(log, apiOptions)
|
||||||
{
|
{
|
||||||
var rateLimiters = new List<IRateLimiter>();
|
var rateLimiters = new List<IRateLimiter>();
|
||||||
foreach (var rateLimiter in apiOptions.RateLimiters)
|
foreach (var rateLimiter in apiOptions.RateLimiters)
|
||||||
rateLimiters.Add(rateLimiter);
|
rateLimiters.Add(rateLimiter);
|
||||||
RateLimiters = rateLimiters;
|
RateLimiters = rateLimiters;
|
||||||
|
ClientOptions = options;
|
||||||
|
|
||||||
|
RequestFactory.Configure(apiOptions.RequestTimeout, options.Proxy, apiOptions.HttpClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a request to the uri and returns if it was successful
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[return: NotNull]
|
||||||
|
protected virtual async Task<WebCallResult> SendRequestAsync(
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
bool signed = false,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
int requestWeight = 1,
|
||||||
|
JsonSerializer? deserializer = null,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
bool ignoreRatelimit = false)
|
||||||
|
{
|
||||||
|
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||||
|
if (!request)
|
||||||
|
return new WebCallResult(request.Error!);
|
||||||
|
|
||||||
|
var result = await GetResponseAsync<object>(request.Data, deserializer, cancellationToken, true).ConfigureAwait(false);
|
||||||
|
return result.AsDataless();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a request to the uri and deserialize the response into the provided type parameter
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type to deserialize into</typeparam>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[return: NotNull]
|
||||||
|
protected virtual async Task<WebCallResult<T>> SendRequestAsync<T>(
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
bool signed = false,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
int requestWeight = 1,
|
||||||
|
JsonSerializer? deserializer = null,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
bool ignoreRatelimit = false
|
||||||
|
) where T : class
|
||||||
|
{
|
||||||
|
var request = await PrepareRequestAsync(uri, method, cancellationToken, parameters, signed, parameterPosition, arraySerialization, requestWeight, deserializer, additionalHeaders, ignoreRatelimit).ConfigureAwait(false);
|
||||||
|
if (!request)
|
||||||
|
return new WebCallResult<T>(request.Error!);
|
||||||
|
|
||||||
|
return await GetResponseAsync<T>(request.Data, deserializer, cancellationToken, false).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares a request to be sent to the server
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed, overwrites the value set in the client</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized, overwrites the value set in the client</param>
|
||||||
|
/// <param name="requestWeight">Credits used for the request</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <param name="ignoreRatelimit">Ignore rate limits for this request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<CallResult<IRequest>> PrepareRequestAsync(
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
Dictionary<string, object>? parameters = null,
|
||||||
|
bool signed = false,
|
||||||
|
HttpMethodParameterPosition? parameterPosition = null,
|
||||||
|
ArrayParametersSerialization? arraySerialization = null,
|
||||||
|
int requestWeight = 1,
|
||||||
|
JsonSerializer? deserializer = null,
|
||||||
|
Dictionary<string, string>? additionalHeaders = null,
|
||||||
|
bool ignoreRatelimit = false)
|
||||||
|
{
|
||||||
|
var requestId = NextId();
|
||||||
|
|
||||||
|
if (signed)
|
||||||
|
{
|
||||||
|
var syncTask = SyncTimeAsync();
|
||||||
|
var timeSyncInfo = GetTimeSyncInfo();
|
||||||
|
if (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)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Debug, $"[{requestId}] Failed to sync time, aborting request: " + syncTimeResult.Error);
|
||||||
|
return syncTimeResult.As<IRequest>(default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ignoreRatelimit)
|
||||||
|
{
|
||||||
|
foreach (var limiter in RateLimiters)
|
||||||
|
{
|
||||||
|
var limitResult = await limiter.LimitRequestAsync(_log, uri.AbsolutePath, method, signed, Options.ApiCredentials?.Key, Options.RateLimitingBehaviour, requestWeight, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (!limitResult.Success)
|
||||||
|
return new CallResult<IRequest>(limitResult.Error!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signed && AuthenticationProvider == null)
|
||||||
|
{
|
||||||
|
_log.Write(LogLevel.Warning, $"[{requestId}] Request {uri.AbsolutePath} failed because no ApiCredentials were provided");
|
||||||
|
return new CallResult<IRequest>(new NoApiCredentialsError());
|
||||||
|
}
|
||||||
|
|
||||||
|
_log.Write(LogLevel.Information, $"[{requestId}] Creating request for " + uri);
|
||||||
|
var paramsPosition = parameterPosition ?? ParameterPositions[method];
|
||||||
|
var request = ConstructRequest(uri, method, parameters, signed, paramsPosition, arraySerialization ?? this.arraySerialization, requestId, additionalHeaders);
|
||||||
|
|
||||||
|
string? paramString = "";
|
||||||
|
if (paramsPosition == HttpMethodParameterPosition.InBody)
|
||||||
|
paramString = $" with request body '{request.Content}'";
|
||||||
|
|
||||||
|
var headers = request.GetHeaders();
|
||||||
|
if (headers.Any())
|
||||||
|
paramString += " with headers " + string.Join(", ", headers.Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"));
|
||||||
|
|
||||||
|
TotalRequestsMade++;
|
||||||
|
_log.Write(LogLevel.Trace, $"[{requestId}] Sending {method}{(signed ? " signed" : "")} request to {request.Uri}{paramString ?? " "}{(ClientOptions.Proxy == null ? "" : $" via proxy {ClientOptions.Proxy.Host}")}");
|
||||||
|
return new CallResult<IRequest>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Executes the request and returns the result deserialized into the type parameter class
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request object to execute</param>
|
||||||
|
/// <param name="deserializer">The JsonSerializer to use for deserialization</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <param name="expectedEmptyResponse">If an empty response is expected</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual async Task<WebCallResult<T>> GetResponseAsync<T>(
|
||||||
|
IRequest request,
|
||||||
|
JsonSerializer? deserializer,
|
||||||
|
CancellationToken cancellationToken,
|
||||||
|
bool expectedEmptyResponse)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
var response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
sw.Stop();
|
||||||
|
var statusCode = response.StatusCode;
|
||||||
|
var headers = response.ResponseHeaders;
|
||||||
|
var responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
// If we have to manually parse error responses (can't rely on HttpStatusCode) we'll need to read the full
|
||||||
|
// response before being able to deserialize it into the resulting type since we don't know if it an error response or data
|
||||||
|
if (manualParseError)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(responseStream);
|
||||||
|
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
_log.Write(LogLevel.Debug, $"[{request.RequestId}] Response received in {sw.ElapsedMilliseconds}ms{(_log.Level == LogLevel.Trace ? (": " + data) : "")}");
|
||||||
|
|
||||||
|
if (!expectedEmptyResponse)
|
||||||
|
{
|
||||||
|
// Validate if it is valid json. Sometimes other data will be returned, 502 error html pages for example
|
||||||
|
var parseResult = ValidateJson(data);
|
||||||
|
if (!parseResult.Success)
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||||
|
|
||||||
|
// Let the library implementation see if it is an error response, and if so parse the error
|
||||||
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
|
if (error != null)
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||||
|
|
||||||
|
// Not an error, so continue deserializing
|
||||||
|
var deserializeResult = Deserialize<T>(parseResult.Data, deserializer, request.RequestId);
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), deserializeResult.Data, deserializeResult.Error);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(data))
|
||||||
|
{
|
||||||
|
var parseResult = ValidateJson(data);
|
||||||
|
if (!parseResult.Success)
|
||||||
|
// Not empty, and not json
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, parseResult.Error!);
|
||||||
|
|
||||||
|
var error = await TryParseErrorAsync(parseResult.Data).ConfigureAwait(false);
|
||||||
|
if (error != null)
|
||||||
|
// Error response
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty success response; okay
|
||||||
|
return new WebCallResult<T>(response.StatusCode, response.ResponseHeaders, sw.Elapsed, Options.OutputOriginalData ? data : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (expectedEmptyResponse)
|
||||||
|
{
|
||||||
|
// We expected an empty response and the request is successful and don't manually parse errors, so assume it's correct
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
|
||||||
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success status code, and we don't have to check for errors. Continue deserializing directly from the stream
|
||||||
|
var desResult = await DeserializeAsync<T>(responseStream, deserializer, request.RequestId, sw.ElapsedMilliseconds).ConfigureAwait(false);
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
|
||||||
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, Options.OutputOriginalData ? desResult.OriginalData : null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), desResult.Data, desResult.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Http status code indicates error
|
||||||
|
using var reader = new StreamReader(responseStream);
|
||||||
|
var data = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||||
|
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Error received in {sw.ElapsedMilliseconds}ms: {data}");
|
||||||
|
responseStream.Close();
|
||||||
|
response.Close();
|
||||||
|
var parseResult = ValidateJson(data);
|
||||||
|
var error = parseResult.Success ? ParseErrorResponse(parseResult.Data) : new ServerError(data)!;
|
||||||
|
if (error.Code == null || error.Code == 0)
|
||||||
|
error.Code = (int)response.StatusCode;
|
||||||
|
return new WebCallResult<T>(statusCode, headers, sw.Elapsed, data, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (HttpRequestException requestException)
|
||||||
|
{
|
||||||
|
// Request exception, can't reach server for instance
|
||||||
|
var exceptionInfo = requestException.ToLogString();
|
||||||
|
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request exception: " + exceptionInfo);
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError(exceptionInfo));
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException canceledException)
|
||||||
|
{
|
||||||
|
if (cancellationToken != default && canceledException.CancellationToken == cancellationToken)
|
||||||
|
{
|
||||||
|
// Cancellation token canceled by caller
|
||||||
|
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request canceled by cancellation token");
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new CancellationRequestedError());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Request timed out
|
||||||
|
_log.Write(LogLevel.Warning, $"[{request.RequestId}] Request timed out: " + canceledException.ToLogString());
|
||||||
|
return new WebCallResult<T>(null, null, null, null, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), default, new WebError($"[{request.RequestId}] Request timed out"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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.
|
||||||
|
/// When setting manualParseError to true 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="data">Received data</param>
|
||||||
|
/// <returns>Null if not an error, Error otherwise</returns>
|
||||||
|
protected virtual Task<ServerError?> TryParseErrorAsync(JToken data)
|
||||||
|
{
|
||||||
|
return Task.FromResult<ServerError?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a request object
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="uri">The uri to send the request to</param>
|
||||||
|
/// <param name="method">The method of the request</param>
|
||||||
|
/// <param name="parameters">The parameters of the request</param>
|
||||||
|
/// <param name="signed">Whether or not the request should be authenticated</param>
|
||||||
|
/// <param name="parameterPosition">Where the parameters should be placed</param>
|
||||||
|
/// <param name="arraySerialization">How array parameters should be serialized</param>
|
||||||
|
/// <param name="requestId">Unique id of a request</param>
|
||||||
|
/// <param name="additionalHeaders">Additional headers to send with the request</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual IRequest ConstructRequest(
|
||||||
|
Uri uri,
|
||||||
|
HttpMethod method,
|
||||||
|
Dictionary<string, object>? parameters,
|
||||||
|
bool signed,
|
||||||
|
HttpMethodParameterPosition parameterPosition,
|
||||||
|
ArrayParametersSerialization arraySerialization,
|
||||||
|
int requestId,
|
||||||
|
Dictionary<string, string>? additionalHeaders)
|
||||||
|
{
|
||||||
|
parameters ??= new Dictionary<string, object>();
|
||||||
|
|
||||||
|
for (var i = 0; i < parameters.Count; i++)
|
||||||
|
{
|
||||||
|
var kvp = parameters.ElementAt(i);
|
||||||
|
if (kvp.Value is Func<object> delegateValue)
|
||||||
|
parameters[kvp.Key] = delegateValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterPosition == HttpMethodParameterPosition.InUri)
|
||||||
|
{
|
||||||
|
foreach (var parameter in parameters)
|
||||||
|
uri = uri.AddQueryParmeter(parameter.Key, parameter.Value.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers = new Dictionary<string, string>();
|
||||||
|
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||||
|
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? new SortedDictionary<string, object>(parameters) : new SortedDictionary<string, object>();
|
||||||
|
if (AuthenticationProvider != null)
|
||||||
|
{
|
||||||
|
AuthenticationProvider.AuthenticateRequest(
|
||||||
|
this,
|
||||||
|
uri,
|
||||||
|
method,
|
||||||
|
parameters,
|
||||||
|
signed,
|
||||||
|
arraySerialization,
|
||||||
|
parameterPosition,
|
||||||
|
out uriParameters,
|
||||||
|
out bodyParameters,
|
||||||
|
out headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
foreach (var param in parameters)
|
||||||
|
{
|
||||||
|
if (!uriParameters.ContainsKey(param.Key) && !bodyParameters.ContainsKey(param.Key))
|
||||||
|
{
|
||||||
|
throw new Exception($"Missing parameter {param.Key} after authentication processing. AuthenticationProvider implementation " +
|
||||||
|
$"should return provided parameters in either the uri or body parameters output");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the auth parameters to the uri, start with a new URI to be able to sort the parameters including the auth parameters
|
||||||
|
uri = uri.SetParameters(uriParameters, arraySerialization);
|
||||||
|
|
||||||
|
var request = RequestFactory.Create(method, uri, requestId);
|
||||||
|
request.Accept = Constants.JsonContentHeader;
|
||||||
|
|
||||||
|
foreach (var header in headers)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
|
||||||
|
if (additionalHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (var header in additionalHeaders)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StandardRequestHeaders != null)
|
||||||
|
{
|
||||||
|
foreach (var header in StandardRequestHeaders)
|
||||||
|
{
|
||||||
|
// Only add it if it isn't overwritten
|
||||||
|
if (additionalHeaders?.ContainsKey(header.Key) != true)
|
||||||
|
request.AddHeader(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parameterPosition == HttpMethodParameterPosition.InBody)
|
||||||
|
{
|
||||||
|
var contentType = requestBodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
|
||||||
|
if (bodyParameters.Any())
|
||||||
|
WriteParamBody(request, bodyParameters, contentType);
|
||||||
|
else
|
||||||
|
request.SetContent(requestBodyEmptyContent, contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Writes the parameters of the request to the request object body
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The request to set the parameters on</param>
|
||||||
|
/// <param name="parameters">The parameters to set</param>
|
||||||
|
/// <param name="contentType">The content type of the data</param>
|
||||||
|
protected virtual void WriteParamBody(IRequest request, SortedDictionary<string, object> parameters, string contentType)
|
||||||
|
{
|
||||||
|
if (requestBodyFormat == RequestBodyFormat.Json)
|
||||||
|
{
|
||||||
|
// Write the parameters as json in the body
|
||||||
|
var stringData = JsonConvert.SerializeObject(parameters);
|
||||||
|
request.SetContent(stringData, contentType);
|
||||||
|
}
|
||||||
|
else if (requestBodyFormat == RequestBodyFormat.FormData)
|
||||||
|
{
|
||||||
|
// Write the parameters as form data in the body
|
||||||
|
var stringData = parameters.ToFormData();
|
||||||
|
request.SetContent(stringData, contentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse an error response from the server. Only used when server returns a status other than Success(200)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The string the request returned</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
protected virtual Error ParseErrorResponse(JToken error)
|
||||||
|
{
|
||||||
|
return new ServerError(error.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -105,15 +105,21 @@ namespace CryptoExchange.Net
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public new SocketApiClientOptions Options => (SocketApiClientOptions)base.Options;
|
public new SocketApiClientOptions Options => (SocketApiClientOptions)base.Options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Options
|
||||||
|
/// </summary>
|
||||||
|
internal ClientOptions ClientOptions { get; set; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// ctor
|
/// ctor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="log">log</param>
|
/// <param name="log">log</param>
|
||||||
|
/// <param name="options">Client options</param>
|
||||||
/// <param name="apiOptions">The Api client options</param>
|
/// <param name="apiOptions">The Api client options</param>
|
||||||
public SocketApiClient(Log log, SocketApiClientOptions apiOptions): base(log, apiOptions)
|
public SocketApiClient(Log log, ClientOptions options, SocketApiClientOptions apiOptions): base(log, apiOptions)
|
||||||
{
|
{
|
||||||
|
ClientOptions = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -131,23 +137,21 @@ namespace CryptoExchange.Net
|
|||||||
/// Connect to an url and listen for data on the BaseAddress
|
/// Connect to an url and listen for data on the BaseAddress
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of the expected data</typeparam>
|
/// <typeparam name="T">The type of the expected data</typeparam>
|
||||||
/// <param name="apiClient">The API client the subscription is for</param>
|
|
||||||
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
||||||
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||||
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
/// <param name="authenticated">If the subscription is to an authenticated endpoint</param>
|
||||||
/// <param name="dataHandler">The handler of update data</param>
|
/// <param name="dataHandler">The handler of update data</param>
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
protected virtual Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||||
{
|
{
|
||||||
return SubscribeAsync(apiClient, Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
return SubscribeAsync(Options.BaseAddress, request, identifier, authenticated, dataHandler, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connect to an url and listen for data
|
/// Connect to an url and listen for data
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">The type of the expected data</typeparam>
|
/// <typeparam name="T">The type of the expected data</typeparam>
|
||||||
/// <param name="apiClient">The API client the subscription is for</param>
|
|
||||||
/// <param name="url">The URL to connect to</param>
|
/// <param name="url">The URL to connect to</param>
|
||||||
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
/// <param name="request">The optional request object to send, will be serialized to json</param>
|
||||||
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
/// <param name="identifier">The identifier to use, necessary if no request object is sent</param>
|
||||||
@ -155,7 +159,7 @@ namespace CryptoExchange.Net
|
|||||||
/// <param name="dataHandler">The handler of update data</param>
|
/// <param name="dataHandler">The handler of update data</param>
|
||||||
/// <param name="ct">Cancellation token for closing this subscription</param>
|
/// <param name="ct">Cancellation token for closing this subscription</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(SocketApiClient apiClient, string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
protected virtual async Task<CallResult<UpdateSubscription>> SubscribeAsync<T>(string url, object? request, string? identifier, bool authenticated, Action<DataEvent<T>> dataHandler, CancellationToken ct)
|
||||||
{
|
{
|
||||||
if (_disposing)
|
if (_disposing)
|
||||||
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
return new CallResult<UpdateSubscription>(new InvalidOperationError("Client disposed, can't subscribe"));
|
||||||
@ -179,7 +183,7 @@ namespace CryptoExchange.Net
|
|||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
// Get a new or existing socket connection
|
// Get a new or existing socket connection
|
||||||
var socketResult = await GetSocketConnection(apiClient, url, authenticated).ConfigureAwait(false);
|
var socketResult = await GetSocketConnection(url, authenticated).ConfigureAwait(false);
|
||||||
if (!socketResult)
|
if (!socketResult)
|
||||||
return socketResult.As<UpdateSubscription>(null);
|
return socketResult.As<UpdateSubscription>(null);
|
||||||
|
|
||||||
@ -519,11 +523,10 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the url to connect to (defaults to BaseAddress form the client options)
|
/// Get the url to connect to (defaults to BaseAddress form the client options)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
|
||||||
/// <param name="address"></param>
|
/// <param name="address"></param>
|
||||||
/// <param name="authentication"></param>
|
/// <param name="authentication"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(SocketApiClient apiClient, string address, bool authentication)
|
protected virtual Task<CallResult<string?>> GetConnectionUrlAsync(string address, bool authentication)
|
||||||
{
|
{
|
||||||
return Task.FromResult(new CallResult<string?>(address));
|
return Task.FromResult(new CallResult<string?>(address));
|
||||||
}
|
}
|
||||||
@ -531,10 +534,9 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the url to reconnect to after losing a connection
|
/// Get the url to reconnect to after losing a connection
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient"></param>
|
|
||||||
/// <param name="connection"></param>
|
/// <param name="connection"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public virtual Task<Uri?> GetReconnectUriAsync(SocketApiClient apiClient, SocketConnection connection)
|
public virtual Task<Uri?> GetReconnectUriAsync(SocketConnection connection)
|
||||||
{
|
{
|
||||||
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
return Task.FromResult<Uri?>(connection.ConnectionUri);
|
||||||
}
|
}
|
||||||
@ -542,15 +544,14 @@ namespace CryptoExchange.Net
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
/// Gets a connection for a new subscription or query. Can be an existing if there are open position or a new one.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="apiClient">The API client the connection is for</param>
|
|
||||||
/// <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>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(SocketApiClient apiClient, string address, bool authenticated)
|
protected virtual async Task<CallResult<SocketConnection>> GetSocketConnection(string address, bool authenticated)
|
||||||
{
|
{
|
||||||
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
var socketResult = socketConnections.Where(s => (s.Value.Status == SocketConnection.SocketStatus.None || s.Value.Status == SocketConnection.SocketStatus.Connected)
|
||||||
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
&& s.Value.Tag.TrimEnd('/') == address.TrimEnd('/')
|
||||||
&& (s.Value.ApiClient.GetType() == apiClient.GetType())
|
&& (s.Value.ApiClient.GetType() == GetType())
|
||||||
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
&& (s.Value.Authenticated == authenticated || !authenticated) && s.Value.Connected).OrderBy(s => s.Value.SubscriptionCount).FirstOrDefault();
|
||||||
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
var result = socketResult.Equals(default(KeyValuePair<int, SocketConnection>)) ? null : socketResult.Value;
|
||||||
if (result != null)
|
if (result != null)
|
||||||
@ -562,7 +563,7 @@ namespace CryptoExchange.Net
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var connectionAddress = await GetConnectionUrlAsync(apiClient, address, authenticated).ConfigureAwait(false);
|
var connectionAddress = await GetConnectionUrlAsync(address, authenticated).ConfigureAwait(false);
|
||||||
if (!connectionAddress)
|
if (!connectionAddress)
|
||||||
{
|
{
|
||||||
_log.Write(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
_log.Write(LogLevel.Warning, $"Failed to determine connection url: " + connectionAddress.Error);
|
||||||
@ -574,7 +575,7 @@ namespace CryptoExchange.Net
|
|||||||
|
|
||||||
// Create new socket
|
// Create new socket
|
||||||
var socket = CreateSocket(connectionAddress.Data!);
|
var socket = CreateSocket(connectionAddress.Data!);
|
||||||
var socketConnection = new SocketConnection(_log, apiClient, socket, address);
|
var socketConnection = new SocketConnection(_log, this, socket, address);
|
||||||
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
socketConnection.UnhandledMessage += HandleUnhandledMessage;
|
||||||
foreach (var kvp in genericHandlers)
|
foreach (var kvp in genericHandlers)
|
||||||
{
|
{
|
||||||
@ -623,7 +624,7 @@ namespace CryptoExchange.Net
|
|||||||
KeepAliveInterval = KeepAliveInterval,
|
KeepAliveInterval = KeepAliveInterval,
|
||||||
ReconnectInterval = Options.ReconnectInterval,
|
ReconnectInterval = Options.ReconnectInterval,
|
||||||
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
RatelimitPerSecond = RateLimitPerSocketPerSecond,
|
||||||
Proxy = Options.Proxy,
|
Proxy = ClientOptions.Proxy,
|
||||||
Timeout = Options.SocketNoDataTimeout
|
Timeout = Options.SocketNoDataTimeout
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,20 +10,15 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
public interface IRestClient: IDisposable
|
public interface IRestClient: IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The factory for creating requests. Used for unit testing
|
/// The options provided for this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
IRequestFactory RequestFactory { get; set; }
|
ClientOptions ClientOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The total amount of requests made with this client
|
/// The total amount of requests made with this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
int TotalRequestsMade { get; }
|
int TotalRequestsMade { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The options provided for this client
|
|
||||||
/// </summary>
|
|
||||||
BaseRestClientOptions ClientOptions { get; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -14,7 +14,7 @@ namespace CryptoExchange.Net.Interfaces
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The options provided for this client
|
/// The options provided for this client
|
||||||
/// </summary>
|
/// </summary>
|
||||||
BaseSocketClientOptions ClientOptions { get; }
|
ClientOptions ClientOptions { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
/// Set the API credentials for this client. All Api clients in this client will use the new credentials, regardless of earlier set options.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user