0

I want to convert my below nested request to Query String

var parameter = new RulesCommandServiceAppendRequest()
{
    Chain = Chain.INPUT,
    Data = new RuleInputModel()
    {
        Protocol = "tcp",
        SourceIp = "2.2.2.2",
        DestinationIp = "1.1.1.1",
        SourcePort = "111",
        DestinationPort = "222",
        Jump = "DROP"
    }
};

to something like below

Chain=INPUT&Data.DestinationIp=1.1.1.1&Data.DestinationPort=222&Data.Jump=DROP&Data.Protocol=tcp&Data.SourceIp=2.2.2.2&Data.SourcePort=111

WebSerializer workaround:

I try with the WebSerializer library with call WebSerializer.ToQueryString(parameter). I see the below output:

Chain=INPUT&Data=DestinationIp=1.1.1.1&DestinationPort=222&Jump=DROP&Protocol=tcp&SourceIp=2.2.2.2&SourcePort=111"

As you can see, the output is not as expected and my ASP.NET Web API .NET6 sample server does not accept this. (for more info you can see https://github.com/BSVN/IpTables.Api/pull/18)

Did you know another library for doing this or some trick to correct using WebSerializer?

Yong Shun
  • 35,286
  • 4
  • 24
  • 46
sorosh_sabz
  • 2,356
  • 2
  • 32
  • 53
  • _"As you can see, the output is incorrect."_ - Not exactly. It's flattened. So, it's not what you _expected_ but it's not particularly "incorrect". If it cannot be configured to not flatten, this may still be made to work overall. – Fildor Aug 10 '23 at 07:58
  • Thanks, I change my word in question, Did you know to how to `WebSerializer` work in not flatten mode? – sorosh_sabz Aug 10 '23 at 08:06
  • Just do a simple Replace and move on. Looking for a tool to do something that is easy to fix, seems like a waste of time to me: `WebSerializer.ToQueryString(parameter).Replace("&", "&Data.").Replace("&Data.Data=", "&Data.")` – Jonathan Willcock Aug 10 '23 at 08:19
  • [Nested Type and Nameprefix](https://github.com/Cysharp/WebSerializer#nested-type-and-nameprefix) seems like what you want. – Fildor Aug 10 '23 at 08:19
  • @Fildor-standswithMods Did you can post answer for my question? – sorosh_sabz Aug 10 '23 at 08:37
  • @Fildor-standswithMods If I understand correctly, `WebSerializer` does not support this feature, Did you know better libarry for this? – sorosh_sabz Aug 10 '23 at 08:44
  • _"WebSerializer does not support this feature"_ - it does. See my answer. – Fildor Aug 10 '23 at 10:30

4 Answers4

1

try this :

static List<(string Key, string Value)> SerializeObject(object obj, string parent) 
{
    var result = new List<(string Key, string Value)>();
    foreach (var p in obj.GetType().GetProperties().Where(p => p.CanRead))
    {
        var v = p.GetValue(obj);
        if (v is null) continue;
        var pp = $"{(!string.IsNullOrEmpty(parent) ? $"{parent}." : "")}{p.Name}";
    
        if (CanSeriazlieToString(p)) result.Add(new (pp, v!.ToString()!));
        else result.AddRange(SerializeObject(v, $"{pp}"));
    }
    return result;
}
static bool CanSeriazlieToString(PropertyInfo pInfo)
{
    return (pInfo.PropertyType == typeof(string) || pInfo.PropertyType == typeof(Guid) || pInfo.PropertyType.IsPrimitive);
}
var queryString = string.Join("&", SerializeObject(parameter, string.Empty).Select(x => $"{x.Key}={HttpUtility.UrlEncode(x.Value)}"));
Roozbeh
  • 127
  • 3
  • Would say this approach is quite interesting and get my attention. Just a minor bug is that the `Chain` query param is not printed. Adding 1 more condition `pInfo.PropertyType.IsEnum`in `CanSeriazlieToString` method will solve the issue. =) – Yong Shun Aug 10 '23 at 09:35
1

Well, if you look for the answer other than using the WebSerializer way,

Implement the DataContract attribute seems the quickest, but I highly doubt next time if you want to convert the RuleInputModel instance only to query string or if you have other models with the property with RuleInputModel type (but different name), it will result in the query param name will have the "Data." prefix.

Thus, recommend writing a custom implementation for it. For this case, the expected result is flattened JSON and then converted to a query string.

You can use the Newtonsoft.Json library to achieve the JSON flattening, converting an object to Dictionary<string, object> type, credit to the GuruStron's answer on Generically Flatten Json using c#.

For the second method will be converting the Dictionary<string, object> to query string.

using System.Web;
using System.Text;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

public static class Helpers
{
    public static Dictionary<string, object> FlattenObject<T>(T source)
        where T : class, new()
        {
            return JObject.FromObject(source)
                .Descendants()
                .OfType<JValue>()
                .ToDictionary(jv => jv.Path, jv => jv.Value<object>());
        }
    
    public static string ToQueryString(this Dictionary<string, object> dict)
    {
        StringBuilder sb = new StringBuilder();
        
        foreach (var kvp in dict)
        {
            sb.Append("&")
                .Append($"{kvp.Key}={HttpUtility.UrlEncode(kvp.Value.ToString())}");
        }
        
        return sb.ToString()
            .Trim('&');
    }
}

The minor change in your RulesCommandServiceAppendRequest class which you want to serialize the Chain value as string instead of integer.

using Newtonsoft.Json.Converters;

public class RulesCommandServiceAppendRequest
{
    [JsonConverter(typeof(StringEnumConverter))]
    public Chain Chain { get; set; }
    
    public RuleInputModel Data { get; set; }
}

Caller method:

Helpers.FlattenObject(parameter)
        .ToQueryString();
Yong Shun
  • 35,286
  • 4
  • 24
  • 46
1

I got it to work using WebSerializerAttribute:

using System;
using Cysharp.Web;
                    
public class Program
{
    public static void Main()
    {
        var parameter = new RulesCommandServiceAppendRequest()
            {
                Chain = Chain.INPUT,
                Data = new RuleInputModel()
                {
                    Protocol = "tcp",
                    SourceIp = "2.2.2.2",
                    DestinationIp = "1.1.1.1",
                    SourcePort = "111",
                    DestinationPort = "222",
                    Jump = "DROP"
                }
            };
        
        WebSerializer.ToQueryString(parameter).Dump();
    }
}

public enum Chain
{
    INPUT
}

public class RulesCommandServiceAppendRequest
{
    public Chain Chain {get; set;}

    [WebSerializer(typeof(RuleInputModelSerializer))]
    public RuleInputModel Data {get; set;}
}

public class RuleInputModel
{
    public string Protocol {get; set;}
    public string SourceIp {get; set;}
    public string DestinationIp {get; set;}
    public string SourcePort {get; set;}
    public string DestinationPort {get; set;}
    public string Jump {get; set;}
}

public class RuleInputModelSerializer : IWebSerializer<RuleInputModel>
{
    public void Serialize(ref WebSerializerWriter writer, RuleInputModel value, WebSerializerOptions options)
    {
        var prefix = writer.NamePrefix;
        writer.NamePrefix = "Data.";
        WebSerializer.ToQueryString(writer, value, options);
        writer.NamePrefix = prefix;
    }
}

Fiddle : https://dotnetfiddle.net/h28gKx

Output:

Chain=INPUT&Data=Data.DestinationIp=1.1.1.1&Data.DestinationPort=222&Data.Jump=DROP&Data.Protocol=tcp&Data.SourceIp=2.2.2.2&Data.SourcePort=111

Fildor
  • 14,510
  • 4
  • 35
  • 67
0

If you want the query string parameters to include the "Data" prefix for the nested object properties, then try this:

class Program
{
    static void Main(string[] args)
    {
        var parameter = new RulesCommandServiceAppendRequest()
        {
            Chain = Chain.INPUT,
            Data = new RuleInputModel()
            {
                Protocol = "tcp",
                SourceIp = "2.2.2.2",
                DestinationIp = "1.1.1.1",
                SourcePort = "111",
                DestinationPort = "222",
                Jump = "DROP"
            }
        };

        string queryString = ConvertToQueryString("Data", parameter);
        Console.WriteLine(queryString);
    }

    static string ConvertToQueryString(string prefix, object obj)
    {
        var properties = obj.GetType().GetProperties();
        var queryStringParams = new List<string>();

        foreach (var property in properties)
        {
            var value = property.GetValue(obj);
            if (value != null)
            {
                var propertyName = $"{prefix}.{property.Name}";
                if (property.PropertyType.IsClass && property.PropertyType != typeof(string))
                {
                    queryStringParams.Add(ConvertToQueryString(propertyName, value));
                }
                else
                {
                    queryStringParams.Add($"{propertyName}={Uri.EscapeDataString(value.ToString())}");
                }
            }
        }

        return string.Join("&", queryStringParams);
    }
}
Amit Mohanty
  • 387
  • 1
  • 9