UnixEpochDateTimeConverter - 无法将令牌类型 'Number' 的值作为字符串获取

UnixEpochDateTimeConverter - Cannot get the value of a token type 'Number' as a string

我正在尝试将毫秒时间戳转换为 DateTime,但它在 reader.GetString():

处引发异常

System.InvalidOperationException: 'Cannot get the value of a token type 'Number' as a string.'

这意味着当它是一个值时,我正在尝试将其作为字符串读取。如果我用 reader.GetInt64()reader.GetDouble() 替换它,它会起作用,但我在这里写这个问题的原因是因为我从 dotnet 的开源项目之一 GitHub,我怀疑我是否真的需要更改 class。我相信问题可能出在我的 JsonSerializerOptions.

JSON 回应

https://pastebin.com/9AjwSp5L(Pastebin 因为它超出了 SO 的限制)

片段

using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace QSGEngine.Web.Platforms.Converters
{
    /// <summary>
    /// Used for deserializing json with Microsoft date format.
    /// </summary>
    internal sealed class UnixEpochDateTimeConverter : JsonConverter<DateTime>
    {
        private static readonly DateTime EpochDateTime = new(1970, 1, 1, 0, 0, 0);
        private static readonly Regex Regex = new("^/Date\(([^+-]+)\)/$", RegexOptions.CultureInvariant);

        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var formatted = reader.GetString();
            var match = Regex.Match(formatted!);

            return !match.Success || !long.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var unixTime) ?
                throw new JsonException() : EpochDateTime.AddMilliseconds(unixTime);
        }

        public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
        {
            var unixTime = Convert.ToInt64((value - EpochDateTime).TotalMilliseconds);

            var formatted = FormattableString.Invariant($"/Date({unixTime})/");
            writer.WriteStringValue(formatted);
        }
    }
}

using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Ardalis.GuardClauses;
using QSGEngine.Web.Platforms.Extensions;
using RestSharp;

namespace QSGEngine.Web.Platforms.Binance;

/// <summary>
/// Binance REST API implementation.
/// </summary>
internal class BinanceRestApiClient : IDisposable
{
    /// <summary>
    /// The base point url.
    /// </summary>
    private const string BasePointUrl = "https://api.binance.com";

    /// <summary>
    /// The key header.
    /// </summary>
    private const string KeyHeader = "X-MBX-APIKEY";
    
    /// <summary>
    /// REST Client.
    /// </summary>
    private readonly IRestClient _restClient = new RestClient(BasePointUrl);
   
    /// <summary>
    /// Initializes a new instance of the <see cref="BinanceRestApiClient"/> class.
    /// </summary>
    /// <param name="apiKey">Binance API key.</param>
    /// <param name="apiSecret">Binance Secret key.</param>
    public BinanceRestApiClient(string apiKey, string apiSecret)
    {
        Guard.Against.NullOrWhiteSpace(apiKey, nameof(apiKey));
        Guard.Against.NullOrWhiteSpace(apiSecret, nameof(apiSecret));
        
        ApiKey = apiKey;
        ApiSecret = apiSecret;
    }

    /// <summary>
    /// The API key.
    /// </summary>
    public string ApiKey { get; }

    /// <summary>
    /// The secret key.
    /// </summary>
    public string ApiSecret { get; }

    /// <summary>
    /// Gets the total account cash balance for specified account type.
    /// </summary>
    /// <returns></returns>
    /// <exception cref="Exception"></exception>
    public AccountInformation? GetBalances()
    {
        var queryString = $"timestamp={GetNonce()}";
        var endpoint = $"/api/v3/account?{queryString}&signature={AuthenticationToken(queryString)}";
        var request = new RestRequest(endpoint, Method.GET);
        request.AddHeader(KeyHeader, ApiKey);

        var response = ExecuteRestRequest(request);
        if (response.StatusCode != HttpStatusCode.OK)
        {
            throw new Exception($"{nameof(BinanceRestApiClient)}: request failed: [{(int)response.StatusCode}] {response.StatusDescription}, Content: {response.Content}, ErrorMessage: {response.ErrorMessage}");
        }

        var jsonSerializerOptions = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true,
            NumberHandling = JsonNumberHandling.AllowReadingFromString
        };
        var deserialize = JsonSerializer.Deserialize<AccountInformation>(response.Content, jsonSerializerOptions);

        return deserialize;
    }

    /// <summary>
    /// If an IP address exceeds a certain number of requests per minute
    /// HTTP 429 return code is used when breaking a request rate limit.
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private IRestResponse ExecuteRestRequest(IRestRequest request)
    {
        const int maxAttempts = 10;
        var attempts = 0;
        IRestResponse response;

        do
        {
            // TODO: RateLimiter
            //if (!_restRateLimiter.WaitToProceed(TimeSpan.Zero))
            //{
            //    Log.Trace("Brokerage.OnMessage(): " + new BrokerageMessageEvent(BrokerageMessageType.Warning, "RateLimit",
            //        "The API request has been rate limited. To avoid this message, please reduce the frequency of API calls."));

            //    _restRateLimiter.WaitToProceed();
            //}

            response = _restClient.Execute(request);
            // 429 status code: Too Many Requests
        } while (++attempts < maxAttempts && (int)response.StatusCode == 429);

        return response;
    }

    /// <summary>
    /// Timestamp in milliseconds.
    /// </summary>
    /// <returns>The current timestamp in milliseconds.</returns>
    private long GetNonce()
    {
        return DateTime.UtcNow.ToTimestamp();
    }

    /// <summary>
    /// Creates a signature for signed endpoints.
    /// </summary>
    /// <param name="payload">The body of the request.</param>
    /// <returns>A token representing the request params.</returns>
    private string AuthenticationToken(string payload)
    {
        using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(ApiSecret));
        var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
        return BitConverter.ToString(computedHash).Replace("-", "").ToLowerInvariant();
    }

    /// <summary>
    /// The standard dispose destructor.
    /// </summary>
    ~BinanceRestApiClient() => Dispose(false);

    /// <summary>
    /// Returns true if it is already disposed.
    /// </summary>
    public bool IsDisposed { get; private set; }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    /// <param name="disposing">If this method is called by a user's code.</param>
    private void Dispose(bool disposing)
    {
        if (IsDisposed) return;

        if (disposing)
        {

        }

        IsDisposed = true;
    }

    /// <summary>
    /// Throw if disposed.
    /// </summary>
    /// <exception cref="ObjectDisposedException"></exception>
    private void ThrowIfDisposed()
    {
        if (IsDisposed)
        {
            throw new ObjectDisposedException($"{nameof(BinanceRestApiClient)} has been disposed.");
        }
    }
}
using System.Text.Json.Serialization;
using QSGEngine.Web.Platforms.Converters;

namespace QSGEngine.Web.Platforms.Binance;

/// <summary>
/// Information about the account.
/// </summary>
public class AccountInformation
{
    /// <summary>
    /// Commission percentage to pay when making trades.
    /// </summary>
    public decimal MakerCommission { get; set; }

    /// <summary>
    /// Commission percentage to pay when taking trades.
    /// </summary>
    public decimal TakerCommission { get; set; }

    /// <summary>
    /// Commission percentage to buy when buying.
    /// </summary>
    public decimal BuyerCommission { get; set; }

    /// <summary>
    /// Commission percentage to buy when selling.
    /// </summary>
    public decimal SellerCommission { get; set; }

    /// <summary>
    /// Boolean indicating if this account can trade.
    /// </summary>
    public bool CanTrade { get; set; }

    /// <summary>
    /// Boolean indicating if this account can withdraw.
    /// </summary>
    public bool CanWithdraw { get; set; }

    /// <summary>
    /// Boolean indicating if this account can deposit.
    /// </summary>
    public bool CanDeposit { get; set; }

    /// <summary>
    /// The time of the update.
    /// </summary>
    [JsonConverter(typeof(UnixEpochDateTimeConverter))]
    public DateTime UpdateTime { get; set; }

    /// <summary>
    /// The type of the account.
    /// </summary>
    public string? AccountType { get; set; }

    /// <summary>
    /// List of assets with their current balances.
    /// </summary>
    public IEnumerable<Balance>? Balances { get; set; }

    /// <summary>
    /// Permission types.
    /// </summary>
    public IEnumerable<string>? Permissions { get; set; }
}

/// <summary>
/// Information about an asset balance.
/// </summary>
public class Balance
{
    /// <summary>
    /// The asset this balance is for.
    /// </summary>
    public string? Asset { get; set; }

    /// <summary>
    /// The amount that isn't locked in a trade.
    /// </summary>
    public decimal Free { get; set; }

    /// <summary>
    /// The amount that is currently locked in a trade.
    /// </summary>
    public decimal Locked { get; set; }

    /// <summary>
    /// The total balance of this asset (Free + Locked).
    /// </summary>
    public decimal Total => Free + Locked;
}
internal static class DateTimeExtensions
{
    /// <summary>
    /// To Unix Timestamp in milliseconds.
    /// </summary>
    /// <param name="datetime">The <see cref="DateTime"/>.</param>
    /// <returns>The unix timestamp</returns>
    public static long ToTimestamp(this DateTime datetime) => new DateTimeOffset(datetime).ToUnixTimeMilliseconds();
}

让我们尝试让您的转换器更宽容一点:

        public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            if(reader.TryGetInt64(out long x))
               return EpochDateTime.AddMilliseconds(x);

            var formatted = reader.GetString();
            var match = Regex.Match(formatted!);

            return !match.Success || !long.TryParse(match.Groups[1].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var unixTime) ?
                throw new JsonException() : EpochDateTime.AddMilliseconds(unixTime);
        }