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="auth">If the requests should be authenticated</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="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="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(
RestApiClient apiClient,
Uri uri,

View File

@ -150,23 +150,60 @@ namespace CryptoExchange.Net.Clients
/// <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>(
protected virtual Task<WebCallResult<T>> SendAsync<T>(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
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;
while (true)
{
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)
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)}]")));
TotalRequestsMade++;
var result = await GetResponseAsync<T>(request, definition.RateLimitGate, cancellationToken).ConfigureAwait(false);
@ -187,7 +224,6 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="baseAddress">Host and schema</param>
/// <param name="definition">Request definition</param>
/// <param name="parameters">Request 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</param>
@ -196,7 +232,6 @@ namespace CryptoExchange.Net.Clients
protected virtual async Task<CallResult> PrepareAsync(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
CancellationToken cancellationToken,
Dictionary<string, string>? additionalHeaders = null,
int? weight = null)
@ -264,25 +299,27 @@ namespace CryptoExchange.Net.Clients
/// </summary>
/// <param name="baseAddress">Host and schema</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>
/// <returns></returns>
protected virtual IRequest CreateRequest(
string baseAddress,
RequestDefinition definition,
ParameterCollection? parameters,
ParameterCollection? uriParameters,
ParameterCollection? bodyParameters,
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 parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
var arraySerialization = definition.ArraySerialization ?? ArraySerialization;
var bodyFormat = definition.RequestBodyFormat ?? RequestBodyFormat;
var requestId = ExchangeHelpers.NextId();
var parameterPosition = definition.ParameterPosition ?? ParameterPositions[definition.Method];
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)
{
try
@ -291,13 +328,14 @@ namespace CryptoExchange.Net.Clients
this,
uri,
definition.Method,
uriParameters,
bodyParameters,
uriParams,
bodyParams,
headers,
definition.Authenticated,
arraySerialization,
parameterPosition,
bodyFormat);
bodyFormat
);
}
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
uri = uri.SetParameters(uriParameters, arraySerialization);
uri = uri.SetParameters(uriParams, arraySerialization);
var request = RequestFactory.Create(definition.Method, uri, requestId);
request.Accept = Constants.JsonContentHeader;
@ -343,8 +371,8 @@ namespace CryptoExchange.Net.Clients
if (parameterPosition == HttpMethodParameterPosition.InBody)
{
var contentType = bodyFormat == RequestBodyFormat.Json ? Constants.JsonContentHeader : Constants.FormContentHeader;
if (bodyParameters.Count != 0)
WriteParamBody(request, bodyParameters, contentType);
if (bodyParams.Count != 0)
WriteParamBody(request, bodyParams, contentType);
else
request.SetContent(RequestBodyEmptyContent, contentType);
}
@ -739,7 +767,8 @@ namespace CryptoExchange.Net.Clients
signed,
arraySerialization,
parameterPosition,
bodyFormat);
bodyFormat
);
}
catch (Exception ex)
{

View File

@ -278,10 +278,11 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Send a query on a socket connection to the BaseAddress and wait for the response
/// </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>
/// <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);
}
@ -289,14 +290,15 @@ namespace CryptoExchange.Net.Clients
/// <summary>
/// Send a query on a socket connection and wait for the response
/// </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="query">The query</param>
/// <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)
return new CallResult<T>(new InvalidOperationError("Client disposed, can't query"));
return new CallResult<THandlerResponse>(new InvalidOperationError("Client disposed, can't query"));
SocketConnection socketConnection;
var released = false;
@ -305,7 +307,7 @@ namespace CryptoExchange.Net.Clients
{
var socketResult = await GetSocketConnection(url, query.Authenticated).ConfigureAwait(false);
if (!socketResult)
return socketResult.As<T>(default);
return socketResult.As<THandlerResponse>(default);
socketConnection = socketResult.Data;
@ -318,7 +320,7 @@ namespace CryptoExchange.Net.Clients
var connectResult = await ConnectIfNeededAsync(socketConnection, query.Authenticated).ConfigureAwait(false);
if (!connectResult)
return new CallResult<T>(connectResult.Error!);
return new CallResult<THandlerResponse>(connectResult.Error!);
}
finally
{
@ -329,10 +331,10 @@ namespace CryptoExchange.Net.Clients
if (socketConnection.PausedActivity)
{
_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>

View File

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

View File

@ -14,9 +14,14 @@ namespace CryptoExchange.Net.Objects.Sockets
public DateTime Timestamp { get; set; }
/// <summary>
/// The topic of the update, what symbol/asset etc..
/// The stream producing the update
/// </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>
/// 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>
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;
Topic = topic;
StreamId = streamId;
Symbol = symbol;
OriginalData = originalData;
Timestamp = timestamp;
UpdateType = updateType;
@ -50,7 +56,7 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <returns></returns>
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>
@ -58,11 +64,11 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <param name="data">The new data</param>
/// <param name="topic">The new topic</param>
/// <param name="symbol">The new symbol</param>
/// <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>
@ -70,12 +76,73 @@ namespace CryptoExchange.Net.Objects.Sockets
/// </summary>
/// <typeparam name="K">The type of the new data</typeparam>
/// <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>
/// <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)
{
var handler = new HttpClientHandler();
handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
if (proxy != null)
{
handler.Proxy = new WebProxy

View File

@ -1,6 +1,7 @@
using CryptoExchange.Net.Interfaces;
using CryptoExchange.Net.Objects;
using CryptoExchange.Net.Objects.Sockets;
using CryptoExchange.Net.Requests;
using System;
using System.Collections.Generic;
using System.Threading;
@ -145,16 +146,17 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Query
/// </summary>
/// <typeparam name="TResponse">Response object type</typeparam>
public abstract class Query<TResponse> : Query
/// <typeparam name="TServerResponse">The type returned from the server</typeparam>
/// <typeparam name="THandlerResponse">The type to be returned to the caller</typeparam>
public abstract class Query<TServerResponse, THandlerResponse> : Query
{
/// <inheritdoc />
public override Type? GetMessageType(IMessageAccessor message) => typeof(TResponse);
public override Type? GetMessageType(IMessageAccessor message) => typeof(TServerResponse);
/// <summary>
/// The typed call result
/// </summary>
public CallResult<TResponse>? TypedResult => (CallResult<TResponse>?)Result;
public CallResult<THandlerResponse>? TypedResult => (CallResult<THandlerResponse>?)Result;
/// <summary>
/// ctor
@ -171,7 +173,7 @@ namespace CryptoExchange.Net.Sockets
{
Completed = true;
Response = message.Data;
Result = HandleMessage(connection, message.As((TResponse)message.Data));
Result = HandleMessage(connection, message.As((TServerResponse)message.Data));
_event.Set();
ContinueAwaiter?.WaitOne();
return Result;
@ -183,7 +185,7 @@ namespace CryptoExchange.Net.Sockets
/// <param name="connection"></param>
/// <param name="message"></param>
/// <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 />
public override void Timeout()
@ -192,7 +194,7 @@ namespace CryptoExchange.Net.Sockets
return;
Completed = true;
Result = new CallResult<TResponse>(new CancellationRequestedError(null, "Query timeout", null));
Result = new CallResult<THandlerResponse>(new CancellationRequestedError(null, "Query timeout", null));
ContinueAwaiter?.Set();
_event.Set();
}
@ -200,10 +202,35 @@ namespace CryptoExchange.Net.Sockets
/// <inheritdoc />
public override void Fail(Error error)
{
Result = new CallResult<TResponse>(error);
Result = new CallResult<THandlerResponse>(error);
Completed = true;
ContinueAwaiter?.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
{
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;
}
catch (Exception ex)
@ -696,14 +696,15 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// Send a query request and wait for an answer
/// </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="continueEvent">Wait event for when the socket message handler can continue</param>
/// <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);
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)

View File

@ -52,11 +52,16 @@ namespace CryptoExchange.Net.Testing.Comparers
else
{
if (dict[dictProp.Name] == default && dictProp.Value.Type != JTokenType.Null)
{
if (dictProp.Value.ToString() == "")
continue;
// Property value not correct
throw new Exception($"{method}: Dictionary entry `{dictProp.Name}` has no value while input json has value {dictProp.Value}");
}
}
}
}
else if (jsonObject!.Type == JTokenType.Array)
{
var jObjs = (JArray)jsonObject;
@ -162,7 +167,7 @@ namespace CryptoExchange.Net.Testing.Comparers
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
{
@ -180,7 +185,10 @@ namespace CryptoExchange.Net.Testing.Comparers
var enumerator = list.GetEnumerator();
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);
if (typeConverter.Length != 0 && ((JsonConverterAttribute)typeConverter.First()).ConverterType != typeof(ArrayConverter))
// Custom converter for the type, skip
@ -260,9 +268,9 @@ namespace CryptoExchange.Net.Testing.Comparers
else if (objectValue is DateTime time)
{
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
}
@ -278,6 +286,10 @@ namespace CryptoExchange.Net.Testing.Comparers
if (time != DateTimeConverter.ParseFromDouble(jsonValue.Value<long>()!))
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))
{
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)
throw new Exception($"{name} expected to {line} to be send to server but did not receive anything");
var lastMessageJson = JToken.Parse(lastMessage);
var expectedJson = JToken.Parse(line.Substring(2));
foreach(var item in expectedJson)
@ -121,6 +122,12 @@ namespace CryptoExchange.Net.Testing
overrideKey = val.ToString();
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)
throw new Exception($"{name} Expected {prop.Name} to be {val}, but was {lastMessageJson[prop.Name]?.Value<string>()}");
}

View File

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