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);
}
我正在尝试将毫秒时间戳转换为 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);
}