1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-06-07 16:06:15 +00:00

Feature/body uri param split (#203)

* Added support for specifying seperate uri and body parameters
* Added support for different message and handling generic types on socket queries
* Split DataEvent.Topic into StreamId and Symbol properties
* Added support for negative time values parsing
* Added some helper methods for converting DataEvent to CallResult
* Added support for GZip/Deflate automatic decompressing in the default HttpClient
* Updated some testing methods
This commit is contained in:
Jan Korf 2024-06-11 16:23:48 +02:00 committed by GitHub
parent 8080ecccc0
commit 9fcd722991
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 221 additions and 67 deletions

View File

@ -49,11 +49,11 @@ namespace CryptoExchange.Net.Authentication
/// <param name="method">The method of the request</param> /// <param name="method">The method of the request</param>
/// <param name="auth">If the requests should be authenticated</param> /// <param name="auth">If the requests should be authenticated</param>
/// <param name="arraySerialization">Array serialization type</param> /// <param name="arraySerialization">Array serialization type</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
/// <param name="requestBodyFormat">The formatting of the request body</param> /// <param name="requestBodyFormat">The formatting of the request body</param>
/// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param> /// <param name="uriParameters">Parameters that need to be in the Uri of the request. Should include the provided parameters if they should go in the uri</param>
/// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param> /// <param name="bodyParameters">Parameters that need to be in the body of the request. Should include the provided parameters if they should go in the body</param>
/// <param name="headers">The headers that should be send with the request</param> /// <param name="headers">The headers that should be send with the request</param>
/// <param name="parameterPosition">The position where the providedParameters should go</param>
public abstract void AuthenticateRequest( public abstract void AuthenticateRequest(
RestApiClient apiClient, RestApiClient apiClient,
Uri uri, Uri uri,

View File

@ -150,23 +150,60 @@ namespace CryptoExchange.Net.Clients
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param> /// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>( protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress, string baseAddress,
RequestDefinition definition, RequestDefinition definition,
ParameterCollection? parameters, ParameterCollection? parameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class int? weight = null) where T : class
{
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
return SendAsync<T>(
baseAddress,
definition,
parameterPosition == HttpMethodParameterPosition.InUri ? parameters : null,
parameterPosition == HttpMethodParameterPosition.InBody ? parameters : null,
cancellationToken,
additionalHeaders,
weight);
}
/// <summary>
/// Send a request to the base address based on the request definition
/// </summary>
/// <typeparam name="T">Response type</typeparam>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="uriParameters">Request query parameters</param>
/// <param name="bodyParameters">Request body parameters</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request definition, for example when the weight depends on the parameters</param>
/// <returns></returns>
protected virtual async Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null) where T : class
{ {
int currentTry = 0; int currentTry = 0;
while (true) while (true)
{ {
currentTry++; currentTry++;
var prepareResult = await PrepareAsync(baseAddress, definition, parameters, cancellationToken, additionalHeaders, weight).ConfigureAwait(false); var prepareResult = await PrepareAsync(baseAddress, definition, cancellationToken, additionalHeaders, weight).ConfigureAwait(false);
if (!prepareResult) if (!prepareResult)
return new WebCallResult<T>(prepareResult.Error!); return new WebCallResult<T>(prepareResult.Error!);
var request = CreateRequest(baseAddress, definition, parameters, additionalHeaders); var request = CreateRequest(
baseAddress,
definition,
uriParameters,
bodyParameters,
additionalHeaders);
_logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]"))); _logger.RestApiSendRequest(request.RequestId, definition, request.Content, request.Uri.Query, string.Join(", ", request.GetHeaders().Select(h => h.Key + $"=[{string.Join(",", h.Value)}]")));
TotalRequestsMade++; TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false); var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
@ -187,7 +224,6 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <param name="baseAddress">Host and schema</param> /// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param> /// <param name="definition">Request definition</param>
/// <param name="parameters">Request parameters</param>
/// <param name="cancellationToken">Cancellation token</param> /// <param name="cancellationToken">Cancellation token</param>
/// <param name="additionalHeaders">Additional headers for this request</param> /// <param name="additionalHeaders">Additional headers for this request</param>
/// <param name="weight">Override the request weight for this request</param> /// <param name="weight">Override the request weight for this request</param>
@ -196,7 +232,6 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> PrepareAsync( protected virtual async Task<CallResult> PrepareAsync(
string baseAddress, string baseAddress,
RequestDefinition definition, RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken, CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null, Dictionary<string, string>? additionalHeaders = null,
int? weight = null) int? weight = null)
@ -264,25 +299,27 @@ namespace CryptoExchange.Net.Clients
/// </summary> /// </summary>
/// <param name="baseAddress">Host and schema</param> /// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param> /// <param name="definition">Request definition</param>
/// <param name="parameters">The parameters of the request</param> /// <param name="uriParameters">The query parameters of the request</param>
/// <param name="bodyParameters">The body parameters of the request</param>
/// <param name="additionalHeaders">Additional headers to send with the request</param> /// <param name="additionalHeaders">Additional headers to send with the request</param>
/// <returns></returns> /// <returns></returns>
protected virtual IRequest CreateRequest( protected virtual IRequest CreateRequest(
string baseAddress, string baseAddress,
RequestDefinition definition, RequestDefinition definition,
ParameterCollection? parameters, ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
Dictionary<string, string>? additionalHeaders) Dictionary<string, string>? additionalHeaders)
{ {
parameters ??= new ParameterCollection(); var uriParams = uriParameters == null ? new ParameterCollection() : CreateParameterDictionary(uriParameters);
var bodyParams = bodyParameters == null ? new ParameterCollection() : CreateParameterDictionary(bodyParameters);
var uri = new Uri(baseAddress.AppendPath(definition.Path)); var uri = new Uri(baseAddress.AppendPath(definition.Path));
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var arraySerialization = definition.ArraySerialization ?? ArraySerialization; var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat; var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var requestId = ExchangeHelpers.NextId(); var requestId = ExchangeHelpers.NextId();
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var headers = new Dictionary<string, string>(); var headers = new Dictionary<string, string>();
var uriParameters = parameterPosition == HttpMethodParameterPosition.InUri ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
var bodyParameters = parameterPosition == HttpMethodParameterPosition.InBody ? CreateParameterDictionary(parameters) : new Dictionary<string, object>();
if (AuthenticationProvider != null) if (AuthenticationProvider != null)
{ {
try try
@ -291,13 +328,14 @@ namespace CryptoExchange.Net.Clients
this, this,
uri, uri,
definition.Method, definition.Method,
uriParameters, uriParams,
bodyParameters, bodyParams,
headers, headers,
definition.Authenticated, definition.Authenticated,
arraySerialization, arraySerialization,
parameterPosition, parameterPosition,
bodyFormat); bodyFormat
);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -305,18 +343,8 @@ namespace CryptoExchange.Net.Clients
} }
} }
// 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 // 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); uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId); var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader; request.Accept = Constants.JsonContentHeader;
@ -343,8 +371,8 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody) if (parameterPosition == HttpMethodParameterPosition.InBody)
{ {
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader; var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Count != 0) if (bodyParams.Count != 0)
WriteParamBody(request, bodyParameters, contentType); WriteParamBody(request, bodyParams, contentType);
else else
request.SetContent(RequestBodyEmptyContent, contentType); request.SetContent(RequestBodyEmptyContent, contentType);
} }
@ -739,7 +767,8 @@ namespace CryptoExchange.Net.Clients
signed, signed,
arraySerialization, arraySerialization,
parameterPosition, parameterPosition,
bodyFormat); bodyFormat
);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -278,10 +278,11 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Send a query on a socket connection to the BaseAddress and wait for the response /// Send a query on a socket connection to the BaseAddress and wait for the response
/// </summary> /// </summary>
/// <typeparam name="T">Expected result type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">The query</param> /// <param name="query">The query</param>
/// <returns></returns> /// <returns></returns>
protected virtual Task<CallResult<T>> QueryAsync<T>(Query<T> query) protected virtual Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query)
{ {
return QueryAsync(BaseAddress, query); return QueryAsync(BaseAddress, query);
} }
@ -289,14 +290,15 @@ namespace CryptoExchange.Net.Clients
/// <summary> /// <summary>
/// Send a query on a socket connection and wait for the response /// Send a query on a socket connection and wait for the response
/// </summary> /// </summary>
/// <typeparam name="T">The expected result type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="url">The url for the request</param> /// <param name="url">The url for the request</param>
/// <param name="query">The query</param> /// <param name="query">The query</param>
/// <returns></returns> /// <returns></returns>
protected virtual async Task<CallResult<T>> QueryAsync<T>(string url, Query<T> query) protected virtual async Task<CallResult<THandlerResponse>> QueryAsync<TServerResponse, THandlerResponse>(string url, Query<TServerResponse, THandlerResponse> query)
{ {
if (_disposing) if (_disposing)
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query")); return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
SocketConnection socketConnection; SocketConnection socketConnection;
var released = false; var released = false;
@ -305,7 +307,7 @@ namespace CryptoExchange.Net.Clients
{ {
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false); var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
if (!socketResult) if (!socketResult)
return socketResult.As<T>(default); return socketResult.As<THandlerResponse>(default);
socketConnection = socketResult.Data; socketConnection = socketResult.Data;
@ -318,7 +320,7 @@ namespace CryptoExchange.Net.Clients
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false); var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
if (!connectResult) if (!connectResult)
return new CallResult<T>(connectResult.Error!); return new CallResult<THandlerResponse>(connectResult.Error!);
} }
finally finally
{ {
@ -329,10 +331,10 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity) if (socketConnection.PausedActivity)
{ {
_logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId); _logger.HasBeenPausedCantSendQueryAtThisMoment(socketConnection.SocketId);
return new CallResult<T>(new ServerError("Socket is paused")); return new CallResult<THandlerResponse>(new ServerError("Socket is paused"));
} }
return await socketConnection.SendAndWaitQueryAsync(query).ConfigureAwait(false); return await socketConnection.SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(query).ConfigureAwait(false);
} }
/// <summary> /// <summary>

View File

@ -154,6 +154,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue)) if (double.TryParse(stringValue, NumberStyles.Float, CultureInfo.InvariantCulture, out var doubleValue))
{ {
// Parse 1637745563.000 format // Parse 1637745563.000 format
if (doubleValue <= 0)
return default;
if (doubleValue < 19999999999) if (doubleValue < 19999999999)
return ConvertFromSeconds(doubleValue); return ConvertFromSeconds(doubleValue);
if (doubleValue < 19999999999999) if (doubleValue < 19999999999999)

View File

@ -68,6 +68,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}"; var info = $"Deserialize JsonException: {ex.Message}, Path: {ex.Path}, LineNumber: {ex.LineNumber}, LinePosition: {ex.BytePositionInLine}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]")); return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
} }
catch (Exception ex)
{
var info = $"Unknown exception: {ex.Message}";
return new CallResult<T>(new DeserializeError(info, OriginalDataAvailable ? GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"));
}
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@ -14,9 +14,14 @@ namespace CryptoExchange.Net.Objects.Sockets
public DateTime Timestamp { get; set; } public DateTime Timestamp { get; set; }
/// <summary> /// <summary>
/// The topic of the update, what symbol/asset etc.. /// The stream producing the update
/// </summary> /// </summary>
public string? Topic { get; set; } public string? StreamId { get; set; }
/// <summary>
/// The symbol the update is for
/// </summary>
public string? Symbol { get; set; }
/// <summary> /// <summary>
/// The original data that was received, only available when OutputOriginalData is set to true in the client options /// The original data that was received, only available when OutputOriginalData is set to true in the client options
@ -33,10 +38,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
public T Data { get; set; } public T Data { get; set; }
internal DataEvent(T data, string? topic, string? originalData, DateTime timestamp, SocketUpdateType? updateType) internal DataEvent(T data, string? streamId, string? symbol, string? originalData, DateTime timestamp, SocketUpdateType? updateType)
{ {
Data = data; Data = data;
Topic = topic; StreamId = streamId;
Symbol = symbol;
OriginalData = originalData; OriginalData = originalData;
Timestamp = timestamp; Timestamp = timestamp;
UpdateType = updateType; UpdateType = updateType;
@ -50,7 +56,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data) public DataEvent<K> As<K>(K data)
{ {
return new DataEvent<K>(data, Topic, OriginalData, Timestamp, UpdateType); return new DataEvent<K>(data, StreamId, Symbol, OriginalData, Timestamp, UpdateType);
} }
/// <summary> /// <summary>
@ -58,11 +64,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
/// <typeparam name="K">The type of the new data</typeparam> /// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param> /// <param name="data">The new data</param>
/// <param name="topic">The new topic</param> /// <param name="symbol">The new symbol</param>
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic) public DataEvent<K> As<K>(K data, string? symbol)
{ {
return new DataEvent<K>(data, topic, OriginalData, Timestamp, UpdateType); return new DataEvent<K>(data, StreamId, symbol, OriginalData, Timestamp, UpdateType);
} }
/// <summary> /// <summary>
@ -70,12 +76,73 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary> /// </summary>
/// <typeparam name="K">The type of the new data</typeparam> /// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param> /// <param name="data">The new data</param>
/// <param name="topic">The new topic</param> /// <param name="streamId">The new stream id</param>
/// <param name="symbol">The new symbol</param>
/// <param name="updateType">The type of update</param> /// <param name="updateType">The type of update</param>
/// <returns></returns> /// <returns></returns>
public DataEvent<K> As<K>(K data, string? topic, SocketUpdateType updateType) public DataEvent<K> As<K>(K data, string streamId, string? symbol, SocketUpdateType updateType)
{ {
return new DataEvent<K>(data, topic, OriginalData, Timestamp, updateType); return new DataEvent<K>(data, streamId, symbol, OriginalData, Timestamp, updateType);
}
/// <summary>
/// Specify the symbol
/// </summary>
/// <param name="symbol"></param>
/// <returns></returns>
public DataEvent<T> WithSymbol(string symbol)
{
Symbol = symbol;
return this;
}
/// <summary>
/// Specify the update type
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public DataEvent<T> WithUpdateType(SocketUpdateType type)
{
UpdateType = type;
return this;
}
/// <summary>
/// Specify the stream id
/// </summary>
/// <param name="streamId"></param>
/// <returns></returns>
public DataEvent<T> WithStreamId(string streamId)
{
StreamId = streamId;
return this;
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<T> ToCallResult()
{
return new CallResult<T>(Data, OriginalData, null);
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<K> ToCallResult<K>(K data)
{
return new CallResult<K>(data, OriginalData, null);
}
/// <summary>
/// Create a CallResult from this DataEvent
/// </summary>
/// <returns></returns>
public CallResult<K> ToCallResult<K>(Error error)
{
return new CallResult<K>(default, OriginalData, error);
} }
} }
} }

View File

@ -19,6 +19,7 @@ namespace CryptoExchange.Net.Requests
if (client == null) if (client == null)
{ {
var handler = new HttpClientHandler(); var handler = new HttpClientHandler();
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
if (proxy != null) if (proxy != null)
{ {
handler.Proxy = new WebProxy handler.Proxy = new WebProxy

View File

@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces; using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects; using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets; using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Requests;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@ -145,16 +146,17 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Query /// Query
/// </summary> /// </summary>
/// <typeparam name="TResponse">Response object type</typeparam> /// <typeparam name="TServerResponse">The type returned from the server</typeparam>
public abstract class Query<TResponse> : Query /// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam>
public abstract class Query<TServerResponse, THandlerResponse> : Query
{ {
/// <inheritdoc /> /// <inheritdoc />
public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse); public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse);
/// <summary> /// <summary>
/// The typed call result /// The typed call result
/// </summary> /// </summary>
public CallResult<TResponse>? TypedResult => (CallResult<TResponse>?)Result; public CallResult<THandlerResponse>? TypedResult => (CallResult<THandlerResponse>?)Result;
/// <summary> /// <summary>
/// ctor /// ctor
@ -171,7 +173,7 @@ namespace CryptoExchange.Net.Sockets
{ {
Completed = true; Completed = true;
Response = message.Data; Response = message.Data;
Result = HandleMessage(connection, message.As((TResponse)message.Data)); Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
_event.Set(); _event.Set();
ContinueAwaiter?.WaitOne(); ContinueAwaiter?.WaitOne();
return Result; return Result;
@ -183,7 +185,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="connection"></param> /// <param name="connection"></param>
/// <param name="message"></param> /// <param name="message"></param>
/// <returns></returns> /// <returns></returns>
public virtual CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => new CallResult<TResponse>(message.Data, message.OriginalData, null); public abstract CallResult<THandlerResponse> HandleMessage(SocketConnection connection, DataEvent<TServerResponse> message);
/// <inheritdoc /> /// <inheritdoc />
public override void Timeout() public override void Timeout()
@ -192,7 +194,7 @@ namespace CryptoExchange.Net.Sockets
return; return;
Completed = true; Completed = true;
Result = new CallResult<TResponse>(new CancellationRequestedError(null, "Query timeout", null)); Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null));
ContinueAwaiter?.Set(); ContinueAwaiter?.Set();
_event.Set(); _event.Set();
} }
@ -200,10 +202,35 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc /> /// <inheritdoc />
public override void Fail(Error error) public override void Fail(Error error)
{ {
Result = new CallResult<TResponse>(error); Result = new CallResult<THandlerResponse>(error);
Completed = true; Completed = true;
ContinueAwaiter?.Set(); ContinueAwaiter?.Set();
_event.Set(); _event.Set();
} }
} }
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TResponse">Response object type</typeparam>
public abstract class Query<TResponse> : Query<TResponse, TResponse>
{
/// <summary>
/// ctor
/// </summary>
/// <param name="request"></param>
/// <param name="authenticated"></param>
/// <param name="weight"></param>
protected Query(object request, bool authenticated, int weight = 1) : base(request, authenticated, weight)
{
}
/// <summary>
/// Handle the query response
/// </summary>
/// <param name="connection"></param>
/// <param name="message"></param>
/// <returns></returns>
public override CallResult<TResponse> HandleMessage(SocketConnection connection, DataEvent<TResponse> message) => message.ToCallResult();
}
} }

View File

@ -498,7 +498,7 @@ namespace CryptoExchange.Net.Sockets
try try
{ {
var innerSw = Stopwatch.StartNew(); var innerSw = Stopwatch.StartNew();
processor.Handle(this, new DataEvent<object>(deserialized, null, originalData, receiveTime, null)); processor.Handle(this, new DataEvent<object>(deserialized, null, null, originalData, receiveTime, null));
totalUserTime += (int)innerSw.ElapsedMilliseconds; totalUserTime += (int)innerSw.ElapsedMilliseconds;
} }
catch (Exception ex) catch (Exception ex)
@ -696,14 +696,15 @@ namespace CryptoExchange.Net.Sockets
/// <summary> /// <summary>
/// Send a query request and wait for an answer /// Send a query request and wait for an answer
/// </summary> /// </summary>
/// <typeparam name="T">Query response type</typeparam> /// <typeparam name="THandlerResponse">Expected result type</typeparam>
/// <typeparam name="TServerResponse">The type returned to the caller</typeparam>
/// <param name="query">Query to send</param> /// <param name="query">Query to send</param>
/// <param name="continueEvent">Wait event for when the socket message handler can continue</param> /// <param name="continueEvent">Wait event for when the socket message handler can continue</param>
/// <returns></returns> /// <returns></returns>
public virtual async Task<CallResult<T>> SendAndWaitQueryAsync<T>(Query<T> query, ManualResetEvent? continueEvent = null) public virtual async Task<CallResult<THandlerResponse>> SendAndWaitQueryAsync<TServerResponse, THandlerResponse>(Query<TServerResponse, THandlerResponse> query, ManualResetEvent? continueEvent = null)
{ {
await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false); await SendAndWaitIntAsync(query, continueEvent).ConfigureAwait(false);
return query.TypedResult ?? new CallResult<T>(new ServerError("Timeout")); return query.TypedResult ?? new CallResult<THandlerResponse>(new ServerError("Timeout"));
} }
private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent) private async Task SendAndWaitIntAsync(Query query, ManualResetEvent? continueEvent)

View File

@ -52,8 +52,13 @@ namespace CryptoExchange.Net.Testing.Comparers
else else
{ {
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null) if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
{
if (dictProp.Value.ToString() == "")
continue;
// Property value not correct // Property value not correct
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}"); throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
}
} }
} }
} }
@ -162,7 +167,7 @@ namespace CryptoExchange.Net.Testing.Comparers
if (dictProp.Value.Type == JTokenType.Object) if (dictProp.Value.Type == JTokenType.Object)
{ {
CheckObject(method, dictProp, dict[dictProp.Name]!, ignoreProperties); CheckPropertyValue(method, dictProp.Value, dict[dictProp.Name]!, dict[dictProp.Name].GetType(), null, null, ignoreProperties);
} }
else else
{ {
@ -180,7 +185,10 @@ namespace CryptoExchange.Net.Testing.Comparers
var enumerator = list.GetEnumerator(); var enumerator = list.GetEnumerator();
foreach (JToken jtoken in jObjs) foreach (JToken jtoken in jObjs)
{ {
enumerator.MoveNext(); var moved = enumerator.MoveNext();
if (!moved)
throw new Exception("Enumeration not moved; incorrect amount of results?");
var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true); var typeConverter = enumerator.Current.GetType().GetCustomAttributes(typeof(JsonConverterAttribute), true);
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter)) if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
// Custom converter for the type, skip // Custom converter for the type, skip
@ -260,9 +268,9 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (objectValue is DateTime time) else if (objectValue is DateTime time)
{ {
if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!)) if (time != DateTimeConverter.ParseFromString(jsonValue.Value<string>()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<string>()} vs {time}");
} }
else if (propertyType.IsEnum) else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
{ {
// TODO enum comparing // TODO enum comparing
} }
@ -278,6 +286,10 @@ namespace CryptoExchange.Net.Testing.Comparers
if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!)) if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!))
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<decimal>()} vs {time}");
} }
else if (propertyType.IsEnum || Nullable.GetUnderlyingType(propertyType)?.IsEnum == true)
{
// TODO enum comparing
}
else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue)) else if (jsonValue.Value<long>() != Convert.ToInt64(objectValue))
{ {
throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}"); throw new Exception($"{method}: {property} not equal: {jsonValue.Value<long>()} vs {Convert.ToInt64(objectValue)}");

View File

@ -109,6 +109,7 @@ namespace CryptoExchange.Net.Testing
if (lastMessage == null) if (lastMessage == null)
throw new Exception($"{name} expected to {line} to be send to server but did not receive anything"); throw new Exception($"{name} expected to {line} to be send to server but did not receive anything");
var lastMessageJson = JToken.Parse(lastMessage); var lastMessageJson = JToken.Parse(lastMessage);
var expectedJson = JToken.Parse(line.Substring(2)); var expectedJson = JToken.Parse(line.Substring(2));
foreach(var item in expectedJson) foreach(var item in expectedJson)
@ -121,6 +122,12 @@ namespace CryptoExchange.Net.Testing
overrideKey = val.ToString(); overrideKey = val.ToString();
overrideValue = lastMessageJson[prop.Name]?.Value<string>(); overrideValue = lastMessageJson[prop.Name]?.Value<string>();
} }
else if (val.ToString() == "-999")
{
// -999 value is used to replace parts or response messages
overrideKey = val.ToString();
overrideValue = lastMessageJson[prop.Name]?.Value<decimal>().ToString();
}
else if (lastMessageJson[prop.Name]?.Value<string>() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true) else if (lastMessageJson[prop.Name]?.Value<string>() != val.ToString() && ignoreProperties?.Contains(prop.Name) != true)
throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}"); throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}");
} }

View File

@ -136,8 +136,9 @@ namespace CryptoExchange.Net.Testing
headers, headers,
true, true,
client.ArraySerialization, client.ArraySerialization,
client.ParameterPositions[method], client.ParameterPositions[method],
client.RequestBodyFormat); client.RequestBodyFormat
);
var signature = getSignature(uriParams, bodyParams, headers); var signature = getSignature(uriParams, bodyParams, headers);