1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-12-14 01:33:26 +00:00
This commit is contained in:
JKorf 2025-11-24 21:22:02 +01:00
parent 07fdee1740
commit 839ebc4ca9
18 changed files with 260 additions and 149 deletions

View File

@ -17,6 +17,7 @@ using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
@ -97,9 +98,7 @@ namespace CryptoExchange.Net.Clients
/// The message handler
/// </summary>
protected abstract IRestMessageHandler MessageHandler { get; }
private static MediaTypeWithQualityHeaderValue AcceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
/// <summary>
/// ctor
/// </summary>
@ -327,6 +326,9 @@ namespace CryptoExchange.Net.Clients
return null;
}
/// <summary>
/// Check rate limits for the request
/// </summary>
protected virtual async Task<Error?> RateLimitAsync(
string host,
int requestId,
@ -412,8 +414,7 @@ namespace CryptoExchange.Net.Clients
var uri = new Uri(baseAddress.AppendPath(definition.Path) + queryString);
var request = RequestFactory.Create(ClientOptions.HttpVersion, definition.Method, uri, requestId);
#warning Should be configurable
request.Accept = AcceptJsonContent;
request.Accept = MessageHandler.AcceptHeader;
if (requestConfiguration.Headers != null)
{
@ -473,7 +474,23 @@ namespace CryptoExchange.Net.Clients
response = await request.GetResponseAsync(cancellationToken).ConfigureAwait(false);
sw.Stop();
responseStream = await response.GetResponseStreamAsync().ConfigureAwait(false);
string? originalData = null;
var outputOriginalData = ApiOptions.OutputOriginalData ?? ClientOptions.OutputOriginalData;
if (outputOriginalData)
{
// If we want to return the original string data from the stream, but still want to process it
// we'll need to copy it as the stream isn't seekable, and thus we can only read it once
var memoryStream = new MemoryStream();
await responseStream.CopyToAsync(memoryStream).ConfigureAwait(false);
using var reader = new StreamReader(memoryStream, Encoding.UTF8,false, 4096, true);
memoryStream.Position = 0;
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
// Continue processing from the memory stream since the response stream is already read and we can't seek it
responseStream.Close();
memoryStream.Position = 0;
responseStream = memoryStream;
}
if (!response.IsSuccessStatusCode && !requestDefinition.TryParseOnNonSuccess)
{
@ -502,12 +519,12 @@ namespace CryptoExchange.Net.Clients
responseStream).ConfigureAwait(false);
}
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, error);
}
if (typeof(T) == typeof(object))
// Success status code and expected empty response, assume it's correct
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, null/*accessor.OriginalDataAvailable ? accessor.GetOriginalString() : "[Data only available when OutputOriginal = true in client options]"*/, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, 0, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, null);
// Data response received
var parsedError = await MessageHandler.CheckForErrorResponse(
@ -527,11 +544,11 @@ namespace CryptoExchange.Net.Clients
}
// Success status code, but TryParseError determined it was an error response
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, default, parsedError);
}
var (deserializeResult, deserializeError) = await MessageHandler.TryDeserializeAsync<T>(responseStream, state, cancellationToken).ConfigureAwait(false);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, null, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError);
return new WebCallResult<T>(response.StatusCode, response.HttpVersion, response.ResponseHeaders, sw.Elapsed, response.ContentLength, originalData, request.RequestId, request.Uri.ToString(), request.Content, request.Method, request.GetHeaders(), ResultDataSource.Server, deserializeResult, deserializeError);
}
catch (HttpRequestException requestException)
{

View File

@ -14,6 +14,15 @@ namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
/// </summary>
public interface IRestMessageHandler
{
/// <summary>
/// The `accept` HTTP response header for the request
/// </summary>
MediaTypeWithQualityHeaderValue AcceptHeader { get; }
/// <summary>
/// Create an object to keep state for a request
/// </summary>
/// <returns></returns>
object? CreateState();
/// <summary>

View File

@ -4,26 +4,41 @@ using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Message evaluator
/// </summary>
public class MessageEvaluator
{
/// <summary>
/// The priority of this evaluator, higher prio evaluators (with lower Priority number) will be checked for matches before lower ones
/// </summary>
public int Priority { get; set; }
/// <summary>
/// Whether to immediately match the evaluator when it is matched. Can only be used when the evaluator has a single unique field to look for
/// </summary>
public bool ForceIfFound { get; set; }
/// <summary>
/// The fields this evaluator has to look for
/// </summary>
public MessageFieldReference[] Fields { get; set; }
public Func<SearchResult, string> IdentifyMessageCallback { get; set; }
/// <summary>
/// The callback for getting the identifier string
/// </summary>
public Func<SearchResult, string>? IdentifyMessageCallback { get; set; }
/// <summary>
/// The static identifier string to return when this evaluator is matched
/// </summary>
public string? StaticIdentifier { get; set; }
public string? IdentifyMessage(SearchResult result)
internal string? IdentifyMessage(SearchResult result)
{
if (StaticIdentifier != null)
return StaticIdentifier;
return IdentifyMessageCallback(result);
return IdentifyMessageCallback!(result);
}
public bool Statisfied(SearchResult result)
internal bool Statisfied(SearchResult result)
{
foreach(var field in Fields)
{
@ -34,90 +49,4 @@ namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
return true;
}
}
public abstract class MessageFieldReference
{
public string SearchName { get; set; }
public int Depth { get; set; } = 1;
public Func<string?, bool>? Constraint { get; set; }
public MessageFieldReference(string searchName)
{
SearchName = searchName;
}
}
public class PropertyFieldReference : MessageFieldReference
{
public byte[] PropertyName { get; set; }
public bool ArrayValues { get; set; }
public PropertyFieldReference(string propertyName) : base(propertyName)
{
PropertyName = Encoding.UTF8.GetBytes(propertyName);
}
}
public class ArrayFieldReference : MessageFieldReference
{
public int ArrayIndex { get; set; }
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
{
Depth = depth;
ArrayIndex = index;
}
}
public class MessageEvalutorFieldReference
{
public bool SkipReading { get; set; }
public bool OverlappingField { get; set; }
public MessageFieldReference Field { get; set; }
public MessageEvaluator? ForceEvaluator { get; set; }
}
public class SearchResult
{
private List<SearchResultItem> _items = new List<SearchResultItem>();
public string FieldValue(string searchName)
{
foreach(var item in _items)
{
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
return item.Value;
}
throw new Exception(""); // TODO
}
public int Count => _items.Count;
public void Clear() => _items.Clear();
public bool Contains(MessageFieldReference field)
{
foreach(var item in _items)
{
if (item.Field == field)
return true;
}
return false;
}
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
{
Field = field,
Value = value
});
}
public struct SearchResultItem
{
public MessageFieldReference Field { get; set; }
public string? Value { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
internal class MessageEvalutorFieldReference
{
public bool SkipReading { get; set; }
public bool OverlappingField { get; set; }
public MessageFieldReference Field { get; set; }
public MessageEvaluator? ForceEvaluator { get; set; }
public MessageEvalutorFieldReference(MessageFieldReference field)
{
Field = field;
}
}
}

View File

@ -0,0 +1,77 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Reference to a message field
/// </summary>
public abstract class MessageFieldReference
{
/// <summary>
/// The name for this search field
/// </summary>
public string SearchName { get; set; }
/// <summary>
/// The depth at which to look for this field
/// </summary>
public int Depth { get; set; } = 1;
/// <summary>
/// Callback to check if the field value matches an expected constraint
/// </summary>
public Func<string?, bool>? Constraint { get; set; }
/// <summary>
/// ctor
/// </summary>
public MessageFieldReference(string searchName)
{
SearchName = searchName;
}
}
/// <summary>
/// Reference to a property message field
/// </summary>
public class PropertyFieldReference : MessageFieldReference
{
/// <summary>
/// The property name in the JSON
/// </summary>
public byte[] PropertyName { get; set; }
/// <summary>
/// Whether the property value is array values
/// </summary>
public bool ArrayValues { get; set; }
/// <summary>
/// ctor
/// </summary>
public PropertyFieldReference(string propertyName) : base(propertyName)
{
PropertyName = Encoding.UTF8.GetBytes(propertyName);
}
}
/// <summary>
/// Reference to an array message field
/// </summary>
public class ArrayFieldReference : MessageFieldReference
{
/// <summary>
/// The index in the array
/// </summary>
public int ArrayIndex { get; set; }
/// <summary>
/// ctor
/// </summary>
public ArrayFieldReference(string searchName, int depth, int index) : base(searchName)
{
Depth = depth;
ArrayIndex = index;
}
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// The results of a search for fields in a JSON message
/// </summary>
public class SearchResult
{
private List<SearchResultItem> _items = new List<SearchResultItem>();
/// <summary>
/// Get the value of a field
/// </summary>
public string? FieldValue(string searchName)
{
foreach (var item in _items)
{
if (item.Field.SearchName.Equals(searchName, StringComparison.Ordinal))
return item.Value;
}
throw new Exception(""); // TODO
}
/// <summary>
/// The number of found search field values
/// </summary>
public int Count => _items.Count;
/// <summary>
/// Clear the search result
/// </summary>
public void Clear() => _items.Clear();
/// <summary>
/// Whether the value for a specific field was found
/// </summary>
public bool Contains(MessageFieldReference field)
{
foreach (var item in _items)
{
if (item.Field == field)
return true;
}
return false;
}
/// <summary>
/// Write a value to the result
/// </summary>
public void Write(MessageFieldReference field, string? value) => _items.Add(new SearchResultItem
{
Field = field,
Value = value
});
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
{
/// <summary>
/// Search result value
/// </summary>
public struct SearchResultItem
{
/// <summary>
/// The field the values is for
/// </summary>
public MessageFieldReference Field { get; set; }
/// <summary>
/// The value of the field
/// </summary>
public string? Value { get; set; }
}
}

View File

@ -27,6 +27,8 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageConverters
/// </summary>
public abstract class JsonRestMessageHandler : IRestMessageHandler
{
private static MediaTypeWithQualityHeaderValue _acceptJsonContent = new MediaTypeWithQualityHeaderValue(Constants.JsonContentHeader);
/// <summary>
/// Empty rate limit error
/// </summary>
@ -37,7 +39,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageConverters
/// </summary>
public abstract JsonSerializerOptions Options { get; }
public virtual object CreateState() => new JsonDocState();
/// <inheritdoc />
public MediaTypeWithQualityHeaderValue AcceptHeader => _acceptJsonContent;
/// <inheritdoc />
public virtual object CreateState() => new JsonDocState();
/// <inheritdoc />
public virtual ValueTask<ServerRateLimitError> ParseErrorRateLimitResponse(
@ -75,6 +81,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageConverters
HttpResponseHeaders responseHeaders,
Stream responseStream) => new ValueTask<Error?>((Error?)null);
/// <summary>
/// Read the response into a JsonDocument object
/// </summary>
protected virtual async ValueTask<(Error?, JsonDocument?)> GetJsonDocument(Stream stream, object? state)
{
if (state is JsonDocState documentState && documentState.Document != null)
@ -106,7 +115,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageConverters
}
else
{
result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false);
result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
}
return (result, null);
}

View File

@ -101,12 +101,11 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
}
_searchFields.Add(new MessageEvalutorFieldReference
_searchFields.Add(new MessageEvalutorFieldReference(field)
{
SkipReading = evaluator.IdentifyMessageCallback == null && field.Constraint == null,
ForceEvaluator = !existingSameSearchField.Any() ? (evaluator.ForceIfFound ? evaluator : null) : null,
OverlappingField = overlapping.Any(),
Field = field
OverlappingField = overlapping.Any()
});
if (field.Depth > _maxSearchDepth)

View File

@ -21,10 +21,6 @@ namespace CryptoExchange.Net.Interfaces
/// </summary>
public MessageMatcher MessageMatcher { get; }
/// <summary>
/// The types the message processor deserializes to
/// </summary>
public HashSet<Type> DeserializationTypes { get; set; }
/// <summary>
/// Handle a message
/// </summary>
CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageHandlerLink matchedHandler);

View File

@ -8,7 +8,8 @@ namespace CryptoExchange.Net.Objects.Options
public class ApiOptions
{
/// <summary>
/// If true, the CallResult and DataEvent objects will also include the originally received data in the OriginalData property
/// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property.
/// Note that this comes at a performance cost
/// </summary>
public bool? OutputOriginalData { get; set; }

View File

@ -14,7 +14,8 @@ namespace CryptoExchange.Net.Objects.Options
public ApiProxy? Proxy { get; set; }
/// <summary>
/// If true, the CallResult and DataEvent objects will also include the originally received data in the OriginalData property
/// If true, the CallResult and DataEvent objects will also include the originally received string data in the OriginalData property.
/// Note that this comes at a performance cost
/// </summary>
public bool OutputOriginalData { get; set; } = false;

View File

@ -56,9 +56,9 @@ namespace CryptoExchange.Net.Objects.Sockets
}
}
/// <inheritdoc />
public class DataEvent<T> : DataEvent
{
/// <summary>
/// The received data deserialized into an object
/// </summary>
@ -120,8 +120,6 @@ namespace CryptoExchange.Net.Objects.Sockets
/// <summary>
/// Copy the DataEvent to a new data type
/// </summary>
/// <param name="exchange">The exchange the result is for</param>
/// <returns></returns>
public ExchangeEvent<K> AsExchangeEvent<K>(string exchange, K data)
{
return new ExchangeEvent<K>(exchange, this, data)

View File

@ -41,7 +41,6 @@ namespace CryptoExchange.Net.Sockets
private ClientWebSocket _socket;
private CancellationTokenSource _ctsSource;
private DateTime _lastReceivedMessagesUpdate;
private Task? _processTask;
private Task? _closeTask;
private bool _stopRequested;

View File

@ -59,22 +59,11 @@ namespace CryptoExchange.Net.Sockets
/// Response
/// </summary>
public object? Response { get; set; }
#warning check if there is a better solution for this in combination with the MessageMatcher
public HashSet<Type> DeserializationTypes { get; set; }
private MessageMatcher _matcher;
/// <summary>
/// Matcher for this subscription
/// </summary>
public MessageMatcher MessageMatcher
{
get => _matcher;
set
{
_matcher = value;
DeserializationTypes = new HashSet<Type>(MessageMatcher.HandlerLinks.Select(x => x.DeserializationType));
}
}
public MessageMatcher MessageMatcher { get; set; }
/// <summary>
/// The query request object

View File

@ -35,8 +35,6 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public bool UserSubscription { get; set; }
public HashSet<Type> DeserializationTypes { get; set; }
private SubscriptionStatus _status;
/// <summary>
/// Current subscription status
@ -74,20 +72,10 @@ namespace CryptoExchange.Net.Sockets
/// </summary>
public bool Authenticated { get; }
private MessageMatcher _matcher;
/// <summary>
/// Matcher for this subscription
/// </summary>
public MessageMatcher MessageMatcher
{
get => _matcher;
set
{
_matcher = value;
DeserializationTypes = new HashSet<Type>(MessageMatcher.HandlerLinks.Select(x => x.DeserializationType));
}
}
public MessageMatcher MessageMatcher { get; set; }
/// <summary>
/// Cancellation token registration

View File

@ -17,8 +17,6 @@ namespace CryptoExchange.Net.Testing
/// <summary>
/// Get a client instance
/// </summary>
/// <param name="loggerFactory"></param>
/// <returns></returns>
public abstract TClient GetClient(ILoggerFactory loggerFactory, bool newDeserialization);
/// <summary>
@ -59,14 +57,15 @@ namespace CryptoExchange.Net.Testing
/// Execute a REST endpoint call and check for any errors or warnings.
/// </summary>
/// <typeparam name="T">Type of response</typeparam>
/// <param name="useNewDeserialization">Whether to use the new deserialization method</param>
/// <param name="expression">The call expression</param>
/// <param name="authRequest">Whether this is an authenticated request</param>
public async Task RunAndCheckResult<T>(bool newDeserialization, Expression<Func<TClient, Task<WebCallResult<T>>>> expression, bool authRequest)
public async Task RunAndCheckResult<T>(bool useNewDeserialization, Expression<Func<TClient, Task<WebCallResult<T>>>> expression, bool authRequest)
{
if (!ShouldRun())
return;
var client = CreateClient(newDeserialization);
var client = CreateClient(useNewDeserialization);
var expressionBody = (MethodCallExpression)expression.Body;
if (authRequest && !Authenticated)

View File

@ -19,8 +19,6 @@ namespace CryptoExchange.Net.Testing
/// <summary>
/// Get a client instance
/// </summary>
/// <param name="loggerFactory"></param>
/// <returns></returns>
public abstract TClient GetClient(ILoggerFactory loggerFactory, bool newDeserialization);
/// <summary>
@ -61,6 +59,7 @@ namespace CryptoExchange.Net.Testing
/// Execute a REST endpoint call and check for any errors or warnings.
/// </summary>
/// <typeparam name="T">Type of the update</typeparam>
/// <param name="useNewDeserialization">Whether to use the new deserialization method</param>
/// <param name="expression">The call expression</param>
/// <param name="expectUpdate">Whether an update is expected</param>
/// <param name="authRequest">Whether this is an authenticated request</param>