1
0
mirror of https://github.com/JKorf/CryptoExchange.Net synced 2025-12-13 17:20:26 +00:00
This commit is contained in:
JKorf 2025-12-07 10:43:44 +01:00
parent 99be703f1f
commit 42736f3003
14 changed files with 169 additions and 100 deletions

View File

@ -223,7 +223,6 @@ namespace CryptoExchange.Net.Clients
string? cacheKey = null;
if (ShouldCache(definition))
{
#warning caching should be static per api client type
cacheKey = baseAddress + definition + uriParameters?.ToFormData();
_logger.CheckingCache(cacheKey);
var cachedValue = _cache.Get(cacheKey, ClientOptions.CachingMaxAge);
@ -487,15 +486,14 @@ namespace CryptoExchange.Net.Clients
// 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);
using var reader = new StreamReader(memoryStream, Encoding.UTF8, false, 4096, true);
if (outputOriginalData)
{
memoryStream.Position = 0;
originalData = await reader.ReadToEndAsync().ConfigureAwait(false);
if (_logger.IsEnabled(LogLevel.Trace))
#warning TODO extension
_logger.LogTrace("[Req {RequestId}] Received response: {Data}", request.RequestId, originalData);
_logger.RestApiReceivedResponse(request.RequestId, originalData);
}
// Continue processing from the memory stream since the response stream is already read and we can't seek it

View File

@ -9,11 +9,13 @@ namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
public interface ISocketMessageHandler
{
/// <summary>
/// Get an identifier for the message which can be used to link it to a listener
/// Get an identifier for the message which can be used to determine the type of the message
/// </summary>
//string? GetMessageIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
string? GetTypeIdentifier(ReadOnlySpan<byte> data, WebSocketMessageType? webSocketMessageType);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
string? GetTopicFilter(object deserializedObject);
/// <summary>

View File

@ -20,7 +20,7 @@ namespace CryptoExchange.Net.Converters.MessageParsing.DynamicConverters
/// <summary>
/// The fields this evaluator has to look for
/// </summary>
public MessageFieldReference[] Fields { get; set; }
public MessageFieldReference[] Fields { get; set; } = [];
/// <summary>
/// The callback for getting the identifier string
/// </summary>

View File

@ -6,6 +6,7 @@ using CryptoExchange.Net.Objects.Options;
using CryptoExchange.Net.Requests;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
@ -90,11 +91,19 @@ namespace CryptoExchange.Net.Converters.SystemTextJson.MessageConverters
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public async ValueTask<(T? Result, Error? Error)> TryDeserializeAsync<T>(Stream responseStream, CancellationToken cancellationToken)
{
try
{
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
var result = await JsonSerializer.DeserializeAsync<T>(responseStream, Options)!.ConfigureAwait(false)!;
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
return (result, null);
}
catch (JsonException ex)

View File

@ -1,6 +1,10 @@
using CryptoExchange.Net.Converters.MessageParsing.DynamicConverters;
using System;
#if !NETSTANDARD
using System.Collections.Frozen;
#endif
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
@ -31,10 +35,14 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
private int _maxSearchDepth;
private MessageEvaluator? _topEvaluator;
private List<MessageEvalutorFieldReference>? _searchFields;
private Dictionary<Type, Func<object, string?>> _mapping;
private Dictionary<Type, Func<object, string?>>? _baseTypeMapping;
private Dictionary<Type, Func<object, string?>>? _mapping;
/// <summary>
/// Add a mapping of a specific object of a type to a specific topic
/// </summary>
/// <typeparam name="T">Type to get topic for</typeparam>
/// <param name="mapping">The topic retrieve delegate</param>
protected void AddTopicMapping<T>(Func<T, string?> mapping)
{
_mapping ??= new Dictionary<Type, Func<object, string?>>();
@ -126,6 +134,7 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
_initialized = true;
}
/// <inheritdoc />
public virtual string? GetTopicFilter(object deserializedObject)
{
if (_mapping == null)
@ -260,20 +269,28 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
else
{
if (reader.TokenType == JsonTokenType.Number)
value = reader.GetDecimal().ToString();
else if (reader.TokenType == JsonTokenType.String)
value = reader.GetString()!;
else if (reader.TokenType == JsonTokenType.True
|| reader.TokenType == JsonTokenType.False)
value = reader.GetBoolean().ToString()!;
else if (reader.TokenType == JsonTokenType.Null)
value = null;
else if (reader.TokenType == JsonTokenType.StartObject
|| reader.TokenType == JsonTokenType.StartArray)
value = null;
else
continue;
switch (reader.TokenType)
{
case JsonTokenType.Number:
value = reader.GetDecimal().ToString();
break;
case JsonTokenType.String:
value = reader.GetString()!;
break;
case JsonTokenType.True:
case JsonTokenType.False:
value = reader.GetBoolean().ToString()!;
break;
case JsonTokenType.Null:
value = null;
break;
case JsonTokenType.StartObject:
case JsonTokenType.StartArray:
value = null;
break;
default:
continue;
}
}
}
@ -321,6 +338,10 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
}
/// <inheritdoc />
#if NET5_0_OR_GREATER
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL3050:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
[UnconditionalSuppressMessage("AssemblyLoadTrimming", "IL2026:RequiresUnreferencedCode", Justification = "JsonSerializerOptions provided here has TypeInfoResolver set")]
#endif
public virtual object Deserialize(ReadOnlySpan<byte> data, Type type)
{
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code

View File

@ -29,6 +29,9 @@ namespace CryptoExchange.Net.Converters.SystemTextJson
/// </summary>
protected abstract string? GetTypeIdentifier(JsonDocument document);
/// <summary>
/// Get optional topic filter, for example a symbol name
/// </summary>
public virtual string? GetTopicFilter(object deserializedObject) => null;
/// <inheritdoc />

View File

@ -28,6 +28,9 @@ namespace CryptoExchange.Net.Interfaces
/// Handle a message
/// </summary>
CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageHandlerLink matchedHandler);
/// <summary>
/// Handle a message
/// </summary>
CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object result, MessageRoute route);
/// <summary>
/// Deserialize a message into object of type

View File

@ -22,6 +22,7 @@ namespace CryptoExchange.Net.Logging.Extensions
private static readonly Action<ILogger, string, Exception?> _restApiCacheHit;
private static readonly Action<ILogger, string, Exception?> _restApiCacheNotHit;
private static readonly Action<ILogger, int?, Exception?> _restApiCancellationRequested;
private static readonly Action<ILogger, int?, string?, Exception?> _restApiReceivedResponse;
static RestApiClientLoggingExtensions()
{
@ -90,6 +91,11 @@ namespace CryptoExchange.Net.Logging.Extensions
new EventId(4012, "RestApiCancellationRequested"),
"[Req {RequestId}] Request cancelled by user");
_restApiReceivedResponse = LoggerMessage.Define<int?, string?>(
LogLevel.Trace,
new EventId(4013, "RestApiReceivedResponse"),
"[Req {RequestId}] Received response: {Data}");
}
public static void RestApiErrorReceived(this ILogger logger, int? requestId, HttpStatusCode? responseStatusCode, long responseTime, string? error, string? originalData, Exception? exception)
@ -155,5 +161,10 @@ namespace CryptoExchange.Net.Logging.Extensions
{
_restApiCancellationRequested(logger, requestId, null);
}
public static void RestApiReceivedResponse(this ILogger logger, int requestId, string? originalData)
{
_restApiReceivedResponse(logger, requestId, originalData, null);
}
}
}

View File

@ -214,13 +214,9 @@ namespace CryptoExchange.Net.Sockets
if (_ctsSource.IsCancellationRequested || !_processing)
return false;
#warning todo logging overloads without id
_logger.SocketAddingBytesToSendBuffer(Id, 0, data);
try
{
await _socket!.SendAsync(new ArraySegment<byte>(data, 0, data.Length), type, true, _ctsSource.Token).ConfigureAwait(false);
_logger.SocketSentBytes(Id, 0, data.Length);
return true;
}
catch (OperationCanceledException)

View File

@ -10,14 +10,17 @@ using System.Threading.Tasks;
namespace CryptoExchange.Net.Sockets
{
/// <summary>
/// Message router
/// </summary>
public class MessageRouter
{
/// <summary>
///
/// The routes registered for this router
/// </summary>
public MessageRoute[] Routes { get; }
// <summary>
/// <summary>
/// ctor
/// </summary>
private MessageRouter(params MessageRoute[] routes)
@ -26,7 +29,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// Create message router without specific message handler
/// </summary>
public static MessageRouter CreateWithoutHandler<T>(string typeIdentifier)
{
@ -34,15 +37,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageRouter CreateWithoutTopicFilter<T>(IEnumerable<string> values, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
return new MessageRouter(values.Select(x => new MessageRoute<T>(x, (string?)null, handler)).ToArray());
}
/// <summary>
/// Create message matcher
/// Create message router without specific message handler
/// </summary>
public static MessageRouter CreateWithoutHandler<T>(string typeIdentifier, string topicFilter)
{
@ -50,7 +45,15 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// Create message router without topic filter
/// </summary>
public static MessageRouter CreateWithoutTopicFilter<T>(IEnumerable<string> values, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
return new MessageRouter(values.Select(x => new MessageRoute<T>(x, (string?)null, handler)).ToArray());
}
/// <summary>
/// Create message router without topic filter
/// </summary>
public static MessageRouter CreateWithoutTopicFilter<T>(string typeIdentifier, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
@ -58,7 +61,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// Create message router with topic filter
/// </summary>
public static MessageRouter CreateWithTopicFilter<T>(string typeIdentifier, string topicFilter, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
@ -66,7 +69,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// Create message router with topic filter
/// </summary>
public static MessageRouter CreateWithTopicFilter<T>(IEnumerable<string> typeIdentifiers, string topicFilter, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
@ -78,7 +81,34 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// Create message router with topic filter
/// </summary>
public static MessageRouter CreateWithTopicFilters<T>(string typeIdentifier, IEnumerable<string> topicFilters, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
var routes = new List<MessageRoute>();
foreach (var filter in topicFilters)
routes.Add(new MessageRoute<T>(typeIdentifier, filter, handler));
return new MessageRouter(routes.ToArray());
}
/// <summary>
/// Create message router with topic filter
/// </summary>
public static MessageRouter CreateWithTopicFilters<T>(IEnumerable<string> typeIdentifiers, IEnumerable<string> topicFilters, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
var routes = new List<MessageRoute>();
foreach (var type in typeIdentifiers)
{
foreach (var filter in topicFilters)
routes.Add(new MessageRoute<T>(type, filter, handler));
}
return new MessageRouter(routes.ToArray());
}
/// <summary>
/// Create message router with optional topic filter
/// </summary>
public static MessageRouter CreateWithOptionalTopicFilter<T>(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
@ -86,7 +116,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// Create message router with optional topic filter
/// </summary>
public static MessageRouter CreateWithOptionalTopicFilters<T>(string typeIdentifier, IEnumerable<string>? topicFilters, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
@ -105,7 +135,7 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// Create message router with optional topic filter
/// </summary>
public static MessageRouter CreateWithOptionalTopicFilters<T>(IEnumerable<string> typeIdentifiers, IEnumerable<string>? topicFilters, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
@ -127,55 +157,40 @@ namespace CryptoExchange.Net.Sockets
}
/// <summary>
/// Create message matcher
/// Create message matcher with specific routes
/// </summary>
public static MessageRouter CreateWithTopicFilters<T>(string typeIdentifier, IEnumerable<string> topicFilters, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
public static MessageRouter Create(params MessageRoute[] routes)
{
var routes = new List<MessageRoute>();
foreach (var filter in topicFilters)
routes.Add(new MessageRoute<T>(typeIdentifier, filter, handler));
return new MessageRouter(routes.ToArray());
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageRouter CreateWithTopicFilters<T>(IEnumerable<string> typeIdentifiers, IEnumerable<string> topicFilters, Func<SocketConnection, DateTime, string?, T, CallResult> handler)
{
var routes = new List<MessageRoute>();
foreach(var type in typeIdentifiers)
{
foreach (var filter in topicFilters)
routes.Add(new MessageRoute<T>(type, filter, handler));
}
return new MessageRouter(routes.ToArray());
}
/// <summary>
/// Create message matcher
/// </summary>
public static MessageRouter Create(params MessageRoute[] linkers)
{
return new MessageRouter(linkers);
return new MessageRouter(routes);
}
/// <summary>
/// Whether this matcher contains a specific link
/// </summary>
public bool ContainsCheck(MessageRoute link) => Routes.Any(x => x.TypeIdentifier == link.TypeIdentifier && x.TopicFilter == link.TopicFilter);
public bool ContainsCheck(MessageRoute route) => Routes.Any(x => x.TypeIdentifier == route.TypeIdentifier && x.TopicFilter == route.TopicFilter);
}
/// <summary>
/// Message route
/// </summary>
public abstract class MessageRoute
{
/// <summary>
/// Type identifier
/// </summary>
public string TypeIdentifier { get; set; }
/// <summary>
/// Optional topic filter
/// </summary>
public string? TopicFilter { get; set; }
/// <summary>
/// Deserialization type
/// </summary>
public abstract Type DeserializationType { get; }
/// <summary>
/// ctor
/// </summary>
public MessageRoute(string typeIdentifier, string? topicFilter)
{
TypeIdentifier = typeIdentifier;
@ -188,6 +203,9 @@ namespace CryptoExchange.Net.Sockets
public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data);
}
/// <summary>
/// Message route
/// </summary>
public class MessageRoute<TMessage> : MessageRoute
{
private Func<SocketConnection, DateTime, string?, TMessage, CallResult> _handler;
@ -204,16 +222,25 @@ namespace CryptoExchange.Net.Sockets
_handler = handler;
}
/// <summary>
/// Create route without topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithoutTopicFilter(string typeIdentifier, Func<SocketConnection, DateTime, string?, TMessage, CallResult> handler)
{
return new MessageRoute<TMessage>(typeIdentifier, null, handler);
}
/// <summary>
/// Create route with optional topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithOptionalTopicFilter(string typeIdentifier, string? topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult> handler)
{
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler);
}
/// <summary>
/// Create route with topic filter
/// </summary>
public static MessageRoute<TMessage> CreateWithTopicFilter(string typeIdentifier, string topicFilter, Func<SocketConnection, DateTime, string?, TMessage, CallResult> handler)
{
return new MessageRoute<TMessage>(typeIdentifier, topicFilter, handler);

View File

@ -108,7 +108,9 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// ctor
/// </summary>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public Query(object request, bool authenticated, int weight = 1)
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
{
_event = new AsyncResetEvent(false, false);
@ -163,6 +165,10 @@ namespace CryptoExchange.Net.Sockets
/// Handle a response message
/// </summary>
public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageHandlerLink check);
/// <summary>
/// Handle a response message
/// </summary>
public abstract CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route);
}
@ -192,6 +198,7 @@ namespace CryptoExchange.Net.Sockets
{
}
/// <inheritdoc />
public override CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object message, MessageRoute route)
{
if (!PreCheckMessage(connection, message))

View File

@ -535,25 +535,7 @@ namespace CryptoExchange.Net.Sockets
_logger.ReceivedData(SocketId, originalData);
}
// Current:
// 1. Get message identifier
// 2. Look for matching handlers and grab the type
// 3. Deserialize
// 4. Dispatch
// Listen id: kline-ethusdt-1m
// Update:
// 1. Get message type identifier
// 2. Look for matching handlers and grab the type
// 3. Deserialize
// 4. Get message topic filter from deserialized type
// 5. Dispatch to filtered
// Type id: kline
// Topic filter: ethusdt-1m
var typeIdentifier = messageConverter.GetTypeIdentifier(data, type);
//var messageIdentifier = messageConverter.GetMessageIdentifier(data, type);
if (typeIdentifier == null)
{
// Both deserialization type and identifier null, can't process

View File

@ -114,7 +114,9 @@ namespace CryptoExchange.Net.Sockets
/// <summary>
/// ctor
/// </summary>
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public Subscription(
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
ILogger logger,
bool authenticated,
bool userSubscription = true)
@ -182,6 +184,9 @@ namespace CryptoExchange.Net.Sockets
return matcher.Handle(connection, receiveTime, originalData, data);
}
/// <summary>
/// Handle an update message
/// </summary>
public CallResult Handle(SocketConnection connection, DateTime receiveTime, string? originalData, object data, MessageRoute route)
{
ConnectionInvocations++;

View File

@ -40,6 +40,13 @@ namespace CryptoExchange.Net.Testing
_nestedPropertyForCompare = nestedPropertyForCompare;
}
/// <summary>
/// Validate to subscriptions being established concurrently are indeed handled correctly
/// </summary>
/// <typeparam name="TUpdate">Type of the subscription update</typeparam>
/// <param name="methodInvoke1">Subscription delegate 1</param>
/// <param name="methodInvoke2">Subscription delegate 2</param>
/// <param name="name">Name</param>
public async Task ValidateConcurrentAsync<TUpdate>(
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke1,
Func<TClient, Action<DataEvent<TUpdate>>, Task<CallResult<UpdateSubscription>>> methodInvoke2,
@ -120,7 +127,7 @@ namespace CryptoExchange.Net.Testing
{
var match = matches[0];
var prevMessage = line1[1] == '1' ? lastMessage1 : lastMessage2;
var json = JsonDocument.Parse(prevMessage);
var json = JsonDocument.Parse(prevMessage!);
var propName = match.Value.Substring(1, match.Value.Length - 2);
var split = propName.Split('.');
var jsonProp = json.RootElement;
@ -141,7 +148,7 @@ namespace CryptoExchange.Net.Testing
{
var match = matches[0];
var prevMessage = line1[1] == '1' ? lastMessage1 : lastMessage2;
var json = JsonDocument.Parse(prevMessage);
var json = JsonDocument.Parse(prevMessage!);
var propName = match.Value.Substring(1, match.Value.Length - 2);
var split = propName.Split('.');
var jsonProp = json.RootElement;
@ -164,8 +171,6 @@ namespace CryptoExchange.Net.Testing
if (updates1 != 1 || updates2 != 1)
throw new Exception($"Expected 1 update for both subscriptions, instead got {updates1} and {updates2}");
//await _client.UnsubscribeAllAsync().ConfigureAwait(false);
}