For the MD5
hashing, I would suggest making it an extension method, if it's going to be used outside this scope, however, if it's only used in this class, you can keep it as is, and if it's only going to be used only for this payload, then you can put it inside the GenerateSign
method. Also, you can use Convert.ToHexString
instead of BitConverter.ToString
.
this part :
var @params = new Dictionary<string, object>
{
{ "api_key", _apiKey },
{ "req_time", now },
{ "op", "sub.personal" }
};
var javaSorted = @params.OrderBy(item => item.Key, StringComparer.Ordinal)
.ToDictionary(i => i.Key, i => i.Value);
can be simplified by using SortedDictionary<string, object>
, so it would be something like this :
var @params = new SortedDictionary<string, object>(StringComparer.Ordinal)
{
{ "api_key", _apiKey },
{ "req_time", now },
{ "op", "sub.personal" }
};
// the dictionary is sorted, and ready to use
in GenerateSign
, Join
, LINQ
, and StringBuilder
are mixed, it's fine, but we can add more readability to it and also add some micro-improvements, by simply use foreach
and a StringBuilder
. something like this :
private static string GenerateSign(IDictionary<string, object> items, string apiSecret)
{
if (items?.Count == 0) return null;
if (string.IsNullOrWhiteSpace(apiSecret)) return null;
var builder = new StringBuilder();
foreach(var item in items)
{
var key = item.Key;
var value = item.Value?.ToString();
if (!string.IsNullOrWhiteSpace(value))
{
if (builder.Length > 0)
{
builder.Append('&');
}
builder.Append($"{key}={HttpUtility.UrlEncode(value)}");
}
}
builder.Append("&api_secret=").Append(apiSecret);
// hash the payload
using var md5 = MD5.Create();
var sourceBytes = Encoding.UTF8.GetBytes(builder.ToString());
var hash = md5.ComputeHash(sourceBytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
There is another option here, which I found it feasable, since the payload (from the documentation) is small, and fixed length.
this is the request payload example:
{
"op":"sub.personal",
"api_key": "api_key",
"sign": "b8d2ff6432798ef858782d7fd109ab41",
"req_time": "1561433613583"
}
if this is going to have the same object length and order, then how about implementing a class model for it, instead of dictionary ? and use JsonProperty
attribute to set the key name and order.
Example with Json.NET :
public class MexcSubPersonalPayload
{
private readonly string _apiSecret;
[JsonProperty("op", Order = 1)]
public string Operation => "sub.personal";
[JsonProperty("api_key", Order = 2)]
public string ApiKey { get; private set; }
[JsonProperty("sign", Order = 3)]
public string Signature => Sign(this.ToString());
[JsonProperty("req_time", Order = 4)]
public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private string Sign(string str)
{
if (string.IsNullOrWhiteSpace(str)) return null;
using var md5 = MD5.Create();
var sourceBytes = Encoding.UTF8.GetBytes(str);
var hash = md5.ComputeHash(sourceBytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
public MexcSubPersonalPayload(string apiKey, string apiSecret)
{
ApiKey = apiKey;
_apiSecret = apiSecret;
}
public override string ToString()
{
return $"api_key={ApiKey}&req_time={RequestTime}&op={Operation}&api_secret={_apiSecret}";
}
}
Example with System.Text.Json:
public class MexcSubPersonalPayload
{
private readonly string _apiSecret;
[JsonPropertyName("op")]
[JsonPropertyOrder(1)]
public string Operation => "sub.personal";
[JsonPropertyName("api_key")]
[JsonPropertyOrder(2)]
public string ApiKey { get; private set; }
[JsonPropertyName("sign")]
[JsonPropertyOrder(3)]
public string Signature => Sign(this.ToString());
[JsonPropertyName("req_time")]
[JsonPropertyOrder(4)]
public long RequestTime => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
private string Sign(string str)
{
if (string.IsNullOrWhiteSpace(str)) return null;
using var md5 = MD5.Create();
var sourceBytes = Encoding.UTF8.GetBytes(str);
var hash = md5.ComputeHash(sourceBytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
public MexcSubPersonalPayload(string apiKey, string apiSecret)
{
ApiKey = apiKey;
_apiSecret = apiSecret;
}
public override string ToString()
{
return $"api_key={ApiKey}&req_time={RequestTime}&op={Operation}&api_secret={_apiSecret}";
}
}
I don't see a need for using UrlEncode
in this model, as the values will be always valid, except for api_key
and _apiSecret
, if they have invalid characters, then you might use UrlEncode
in them, but I don't know if is it going to invalidate the keys (public and private keys) or the service provider would handle the escaped characters (in this case it won't invalidate them).
anyhow, using the above model, you would only need to pass the public and private keys, and then convert it to JSON.
usage with Json.NET
var json = JsonConvert.SerializeObject(new MexcSubPersonalPayload("api_key", "api_secret"));
usage with System.Text.Json
var json = JsonSerializer.Serialize(new MexcSubPersonalPayload("api_key", "api_secret"));
UPDATE FROM THE COMMENTS
Combine javaSorted
with GenerateSign
private static string GenerateSign(IDictionary<string, object> items, string apiSecret)
{
if (items?.Count == 0) return null;
if (string.IsNullOrWhiteSpace(apiSecret)) return null;
var builder = new StringBuilder();
foreach (var item in items.Where(x => !string.IsNullOrWhiteSpace(x.Value?.ToString())).OrderBy(x => x.Key, StringComparer.Ordinal))
{
var key = item.Key;
var value = item.Value.ToString();
if (builder.Length > 0)
{
builder.Append('&');
}
builder.Append($"{key}={HttpUtility.UrlEncode(value)}");
}
builder.Append("&api_secret=").Append(apiSecret);
//hash the payload
using var md5 = MD5.Create();
var sourceBytes = Encoding.UTF8.GetBytes(builder.ToString());
var hash = md5.ComputeHash(sourceBytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
api_secret
separately ? why wasn't passed within the dictionary like the others ? \$\endgroup\$