将带有 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

还有很多不同的方式,我现在懒得去想了