将带有 JsonPropertyName 装饰的 POCO 对象转换为 URL 查询字符串
Converting a POCO object with JsonPropertyName decorations into a URL query string
是否有 .NET API 可以将带有 JsonPropertyName
装饰的 POCO 对象转换为正确编码的 URL 查询字符串?
例如,对于:
public record AuthEndPointArgs
{
[JsonPropertyName("response_type")]
public string? ResponseType { get; set; } // "code"
[JsonPropertyName("client_id")]
public string? ClientId { get; set; } // "a:b"
// ...
}
我预计:?response_type=code&client_id=a%3Ab
.
我目前使用的国产版本:
/// <summary>
/// Convert a shallow POCO object into query string,
/// with JsonPropertyName decorations becoming query string argument names
/// </summary>
public static string ConvertObjectToQueryString(object source, string query)
{
var uriArgs = System.Web.HttpUtility.ParseQueryString(query);
var jsonNode = System.Text.Json.JsonSerializer.SerializeToNode(source) ??
throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
foreach (var item in jsonNode.AsObject())
{
uriArgs[item.Key] = item.Value?.ToString() ?? String.Empty;
}
return uriArgs.ToString() ?? String.Empty;
}
已更新,为了完整起见,反向转换:
/// <summary>
/// Convert a query string into a POCO object with string properties,
/// decorated with JsonPropertyName attributes
/// </summary>
public static T ConvertQueryStringToObject<T>(string query)
{
var args = System.Web.HttpUtility.ParseQueryString(query);
var jsonObject = new JsonObject();
foreach (var key in args.Keys.Cast<string>())
{
jsonObject.Add(key, JsonValue.Create(args[key]));
}
return jsonObject.Deserialize<T>() ??
throw new InvalidOperationException(typeof(T).Name);
}
一种可能性是 System.Text.Json.Serialization.JsonConverter<T>
记录在 How to customize property names and values with System.Text.Json
class AuthEndpointMarshaller : JsonConverter<AuthEndPointArgs>
{
public override AuthEndPointArgs? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
//TODO check typeToConvert
AuthEndPointArgs result = new();
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return result;
}
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
string ? propertyName = reader.GetString();
reader.Read();
string? propertyValue = reader.GetString();
switch (propertyName)
{
case "response_type":
result.ResponseType = propertyValue;
break;
case "client_id":
result.ClientId = HttpUtility.UrlDecode(propertyValue);
break;
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, AuthEndPointArgs value, JsonSerializerOptions options)
{
value.ClientId = HttpUtility.UrlEncode(value.ClientId);
JsonSerializer.Serialize(writer,value);
}
}
然后您可以使用
调用它
string ConvertShallowObjectToQueryStringAlternate(AuthEndPointArgs source)
{
var options = new JsonSerializerOptions()
{
Converters =
{
new AuthEndpointMarshaller()
}
};
var jsonNode = JsonSerializer.SerializeToNode(source, options) ??
throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
return jsonNode.ToString();
}
或者如果您正在寻找快速解决方案
public record AuthEndPointArgs
{
// ...
public string ToQueryParams()
{
var sb = new StringBuilder($"?response_type={ResponseType}&client_id={HttpUtility.UrlEncode(ClientId)}");
return sb.ToString();
}
}
据我所知,没有原生 .NET API,似乎 ASP.NET(核心)在某种程度上支持读取它(检查 and ),但我不知道如何创建一个。
最懒惰的解决方案可能是将您的对象序列化为 JSON,然后 HttpUtility.UrlEncode(json)
,然后将其传递给查询参数,就像这样:
&payload=%7B%22response_type%22%3A%20%22code%22%2C%22client_id%22%3A%20%22a%3Ab%22%7D
在另一端 JsonSerializer.Deserialize<AuthEndPointArgs>(HttpUtility.UrlDecode(payload))
就像 so。这是假设您可以编辑两端。
虽然听起来有点愚蠢,但它确实有效,在某些方面甚至可能比直接将 AuthEndPointArgs
序列化为查询字符串更好,因为查询字符串的标准缺少一些定义,比如如何处理数组,还有复杂的选项。似乎 JS 和 PHP 社区有非官方标准,但它们需要在两端手动实施。所以我们还需要推出我们自己的“标准”和实现,除非我们说我们只能序列化满足以下条件的对象:
- 没有复杂对象作为属性
- 没有列表/数组作为属性
旁注: URL 的最大长度取决于很多因素,通过查询参数发送复杂的对象可能会很快超出该限制,请参阅 here for more on this topic. It may just be best to hardcode something like ToQueryParams
like Ady suggested in
如果我们确实想要一个符合这些标准的通用实现,我们的实现实际上非常简单:
public static class QueryStringSerializer
{
public static string Serialize(object source)
{
var props = source.GetType().GetProperties(
BindingFlags.Instance | BindingFlags.Public
);
var output = new StringBuilder();
foreach (var prop in props)
{
// You might want to extend this check, things like 'Guid'
// serialize nicely to a query string but aren't primitive types
if (prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string))
{
var value = prop.GetValue(source);
if (value is null)
continue;
output.Append($"{GetNameFromMember(prop)}={HttpUtility.UrlEncode(value.ToString())}");
}
else
throw new NotSupportedException();
}
}
}
private static string GetNameFromMember(MemberInfo prop)
{
string propName;
// You could also implement a 'QueryStringPropertyNameAttribute'
// if you want to be able to override the name given, for this you can basically copy the JSON attribute
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPropertyNameAttribute.cs
if (Attribute.IsDefined(prop, typeof(JsonPropertyNameAttribute)))
{
var attribute = Attribute.GetCustomAttribute(prop, typeof(JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
// This check is technically unnecessary, but VS wouldn't shut up
if (attribute is null)
propName = prop.Name;
else
propName = attribute.Name;
}
else
propName = prop.Name;
return propName;
}
如果我们想要支持将可枚举对象作为属性或将“复杂”对象作为成员的对象,我们需要定义如何序列化它们,例如
class Foo
{
public int[] Numbers { get; set; }
}
可以序列化为
?numbers[]=1&numbers[]=2
或到 1 索引“列表”
?numbers[1]=1&numbers[2]=2
或以逗号分隔的列表
?numbers=1,2
或者只是一个实例的倍数 = 可枚举
?numbers=1&numbers=2
可能还有更多格式。但是所有这些都是特定于接收这些调用的框架/实现的,因为没有官方标准,同样适用于
class Foo
{
public AuthEndPointArgs Args { get; set; }
}
可能是
?args.response_type=code&args.client_id=a%3Ab
还有很多不同的方式,我现在懒得去想了
是否有 .NET API 可以将带有 JsonPropertyName
装饰的 POCO 对象转换为正确编码的 URL 查询字符串?
例如,对于:
public record AuthEndPointArgs
{
[JsonPropertyName("response_type")]
public string? ResponseType { get; set; } // "code"
[JsonPropertyName("client_id")]
public string? ClientId { get; set; } // "a:b"
// ...
}
我预计:?response_type=code&client_id=a%3Ab
.
我目前使用的国产版本:
/// <summary>
/// Convert a shallow POCO object into query string,
/// with JsonPropertyName decorations becoming query string argument names
/// </summary>
public static string ConvertObjectToQueryString(object source, string query)
{
var uriArgs = System.Web.HttpUtility.ParseQueryString(query);
var jsonNode = System.Text.Json.JsonSerializer.SerializeToNode(source) ??
throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
foreach (var item in jsonNode.AsObject())
{
uriArgs[item.Key] = item.Value?.ToString() ?? String.Empty;
}
return uriArgs.ToString() ?? String.Empty;
}
已更新,为了完整起见,反向转换:
/// <summary>
/// Convert a query string into a POCO object with string properties,
/// decorated with JsonPropertyName attributes
/// </summary>
public static T ConvertQueryStringToObject<T>(string query)
{
var args = System.Web.HttpUtility.ParseQueryString(query);
var jsonObject = new JsonObject();
foreach (var key in args.Keys.Cast<string>())
{
jsonObject.Add(key, JsonValue.Create(args[key]));
}
return jsonObject.Deserialize<T>() ??
throw new InvalidOperationException(typeof(T).Name);
}
一种可能性是 System.Text.Json.Serialization.JsonConverter<T>
记录在 How to customize property names and values with System.Text.Json
class AuthEndpointMarshaller : JsonConverter<AuthEndPointArgs>
{
public override AuthEndPointArgs? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
//TODO check typeToConvert
AuthEndPointArgs result = new();
if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return result;
}
if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
string ? propertyName = reader.GetString();
reader.Read();
string? propertyValue = reader.GetString();
switch (propertyName)
{
case "response_type":
result.ResponseType = propertyValue;
break;
case "client_id":
result.ClientId = HttpUtility.UrlDecode(propertyValue);
break;
}
}
throw new JsonException();
}
public override void Write(Utf8JsonWriter writer, AuthEndPointArgs value, JsonSerializerOptions options)
{
value.ClientId = HttpUtility.UrlEncode(value.ClientId);
JsonSerializer.Serialize(writer,value);
}
}
然后您可以使用
调用它string ConvertShallowObjectToQueryStringAlternate(AuthEndPointArgs source)
{
var options = new JsonSerializerOptions()
{
Converters =
{
new AuthEndpointMarshaller()
}
};
var jsonNode = JsonSerializer.SerializeToNode(source, options) ??
throw new InvalidOperationException(nameof(JsonSerializer.SerializeToNode));
return jsonNode.ToString();
}
或者如果您正在寻找快速解决方案
public record AuthEndPointArgs
{
// ...
public string ToQueryParams()
{
var sb = new StringBuilder($"?response_type={ResponseType}&client_id={HttpUtility.UrlEncode(ClientId)}");
return sb.ToString();
}
}
据我所知,没有原生 .NET API,似乎 ASP.NET(核心)在某种程度上支持读取它(检查
最懒惰的解决方案可能是将您的对象序列化为 JSON,然后 HttpUtility.UrlEncode(json)
,然后将其传递给查询参数,就像这样:
&payload=%7B%22response_type%22%3A%20%22code%22%2C%22client_id%22%3A%20%22a%3Ab%22%7D
在另一端 JsonSerializer.Deserialize<AuthEndPointArgs>(HttpUtility.UrlDecode(payload))
就像 so。这是假设您可以编辑两端。
虽然听起来有点愚蠢,但它确实有效,在某些方面甚至可能比直接将 AuthEndPointArgs
序列化为查询字符串更好,因为查询字符串的标准缺少一些定义,比如如何处理数组,还有复杂的选项。似乎 JS 和 PHP 社区有非官方标准,但它们需要在两端手动实施。所以我们还需要推出我们自己的“标准”和实现,除非我们说我们只能序列化满足以下条件的对象:
- 没有复杂对象作为属性
- 没有列表/数组作为属性
旁注: URL 的最大长度取决于很多因素,通过查询参数发送复杂的对象可能会很快超出该限制,请参阅 here for more on this topic. It may just be best to hardcode something like ToQueryParams
like Ady suggested in
如果我们确实想要一个符合这些标准的通用实现,我们的实现实际上非常简单:
public static class QueryStringSerializer
{
public static string Serialize(object source)
{
var props = source.GetType().GetProperties(
BindingFlags.Instance | BindingFlags.Public
);
var output = new StringBuilder();
foreach (var prop in props)
{
// You might want to extend this check, things like 'Guid'
// serialize nicely to a query string but aren't primitive types
if (prop.PropertyType.IsPrimitive || prop.PropertyType == typeof(string))
{
var value = prop.GetValue(source);
if (value is null)
continue;
output.Append($"{GetNameFromMember(prop)}={HttpUtility.UrlEncode(value.ToString())}");
}
else
throw new NotSupportedException();
}
}
}
private static string GetNameFromMember(MemberInfo prop)
{
string propName;
// You could also implement a 'QueryStringPropertyNameAttribute'
// if you want to be able to override the name given, for this you can basically copy the JSON attribute
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Attributes/JsonPropertyNameAttribute.cs
if (Attribute.IsDefined(prop, typeof(JsonPropertyNameAttribute)))
{
var attribute = Attribute.GetCustomAttribute(prop, typeof(JsonPropertyNameAttribute)) as JsonPropertyNameAttribute;
// This check is technically unnecessary, but VS wouldn't shut up
if (attribute is null)
propName = prop.Name;
else
propName = attribute.Name;
}
else
propName = prop.Name;
return propName;
}
如果我们想要支持将可枚举对象作为属性或将“复杂”对象作为成员的对象,我们需要定义如何序列化它们,例如
class Foo
{
public int[] Numbers { get; set; }
}
可以序列化为
?numbers[]=1&numbers[]=2
或到 1 索引“列表”
?numbers[1]=1&numbers[2]=2
或以逗号分隔的列表
?numbers=1,2
或者只是一个实例的倍数 = 可枚举
?numbers=1&numbers=2
可能还有更多格式。但是所有这些都是特定于接收这些调用的框架/实现的,因为没有官方标准,同样适用于
class Foo
{
public AuthEndPointArgs Args { get; set; }
}
可能是
?args.response_type=code&args.client_id=a%3Ab
还有很多不同的方式,我现在懒得去想了