Nicolas Pretot
Nicolas Pretot

Reputation: 192

Deserialize Json using C# with variable content

I'm reposting this question because someone marked it as duplicate but it's not, so I detailed more to avoid the confusion.

I'm creating an API in c# that my WebApp can reach to get some data. In my web app, i have a view with a list of (let's say) products. For now i have a route that return all my products stored in Database.

I would like now to add the ability for the webapp to make complexe filters. I would like to be able in the webApp or with Postman for exemple, to reach my route (let's say) "/api/product" with a URlParam named "filters" that will contains all filters that the webapp need to apply.

Now back to my WebApp (build with angular) I have a service that can create like a tree of filters (See class architecture below). (And yes this architecture could be improved)

enter image description here

The idea is that i can build filter like this : If i want to fetch all Product that have "foo" in their names and with a price gretter than 15$ :

let filter = new AndNode();
filter.Add(new LikeNode("name", "foo"));
filter.Add(new GTNode("price", 15));
filter.generateJson();

Will result in this Json :

{
    "$and": [
        { 
            "$like": { "key": "name", "value": "foo" }
        },
        {
            "$gt": { "key": "price", "value": 15 }
        }
    ]
}  

Now, this Json have 1 requirement : It should never contains more than one property at is first level. Because this is meant to be used to generate SQL Filters, it make sense that, the json must contains only one node on his first level : an EQ, LIKE, GT, or LT node, or if we have more than one filters an AND / OR nodes that define the logical "conjunction" between each filters of the next level.

So it makes sense that :

{
    "$like": { "key": "name", "value": "foo" }
}

Is valid and could result in this SQL statement :

SELECT * FROM products WHERE name LIKE "%foo%";

Also :

{
    "$and": [
        { 
            "$like": { "key": "name", "value": "foo" }
        },
        {
            "$gt": { "key": "price", "value": 15 }
        }
    ]
}  

Is valid and could result as this sql statement :

SELECT * FROM products WHERE name LIKE "%foo%" AND price > 15;

But :

{
    "$like": { "key": "name", "value": "foo" },
    "$eq": { "key": "price", "value": 5 },
}

Is not valid because it will result a this sql statement :

SELECT * FROM products WHERE name LIKE "%foo%" price > 15

Witch is not valid because of the missing AND or OR keyword.

What I would like to do : In a beautiful world, I would like to be able to serialize this json in my web app, convert it to a valid URL param string, and send it to the API (c#) deserialize that JSON to get the same object structure that I had on the WebApp. For our example with the price and the name, I would like to have an instance of the AndNode class with a member "child" that contains 2 object: 1 instance of the LikeNode class and 1 instance of the GTNode class.

This implementation is maybe debatable, but this is what we need for now.

For now, I have :

public class EQNode
{
    [JsonProperty("key")]
    public string key { get; set; }

    [JsonProperty("value")]
    public int value { get; set; }
}

public class LikeNode
{
    [JsonProperty("key")]
    public string key { get; set; }

    [JsonProperty("value")]
    public string value { get; set; }
}

public class GTNode
{
    [JsonProperty("key")]
    public string key { get; set; }

    [JsonProperty("value")]
    public int value { get; set; }
}

public class LTNode
{
    [JsonProperty("key")]
    public string key { get; set; }

    [JsonProperty("value")]
    public int value { get; set; }
}

public class OrNode
{
    [JsonProperty("$eq")]
    public EQNode eq { get; set; }
}

public class AndNode
{
    [JsonProperty("$eq")]
    public EQNode eq { get; set; }

    [JsonProperty("$like")]
    public LikeNode like { get; set; }

    [JsonProperty("$gt")]
    public GTNode gt { get; set; }

    [JsonProperty("$lt")]
    public LTNode lt { get; set; }

    [JsonProperty("$or")]
    public List<OrNode> or { get; set; }

    [JsonProperty("$and")]
    public List<OrNode> and { get; set; }
}

public class RootObject
{
    [JsonProperty("$and")]
    public List<AndNode> and { get; set; }

    [JsonProperty("$or")]
    public List<OrNode> or { get; set; }

    [JsonProperty("$eq")]
    public EQNode eq { get; set; }

    [JsonProperty("$like")]
    public LikeNode like { get; set; }

    [JsonProperty("$gt")]
    public GTNode gt { get; set; }

    [JsonProperty("$lt")]
    public LTNode lt { get; set; }
}

This has been generated by the "past special" feature of visual studio (and I added all the JsonProperty decorator to match the names in my json). It works, but, this is not really what I expected.

First, i would like to have the closest structure from my WebApp structure (with inheritance). Second, i don't like the way this generated code handle the fact that i can have either $eq, $gt, $lt, $like, $or or $and node in my root object, i can't believe there is no way to have a clean structure like that : (note: this is for example purpose some decorator used doesn't exist or are badly used, this is just for demo)

public class RootObject {
    public FilterNode root;
} 

public class FilterNode {
}

public class ConjunctionNode: FilterNode {
    public FilterNode[] childs
}

[JsonObject("$and")]
public class AndNode: ConjunctionNode {
}

[JsonObject("$or")]
public class OrNode: ConjunctionNode {
}

[JsonObject("$like")]
public class LikeNode: FilterNode {
    public string key;
    public string value;
}

[JsonObject("$eq")]
public class EQNode: FilterNode {
    public string key;
    public string value;
}

[JsonObject("$gt")]
public class GTNode: FilterNode {
    public string key;
    public string value;
}

[JsonObject("$lt")]
public class LTNode: FilterNode {
    public string key;
    public string value;
}

So, if we use the sample json :

{
    "$and": [
        { 
            "$like": { "key": "name", "value": "foo" }
        },
        {
            "$gt": { "key": "price", "value": 15 }
        }
    ]
}  

I could use :

RootObject obj = JsonConvert.DeserializeObject<RootObject>(filters);

With the "root" member of the RootObject as an instance of the AndNode. This AndNode would contain two objects in this child's array, which are an instance of the LikeNode and an instance of the GTNode.

Can this be done?

Upvotes: 1

Views: 1901

Answers (1)

inthevortex
inthevortex

Reputation: 334

I went through your problem in detail. There is flaw in the object model to which you tend to deserialize. Also you cant use direct deserialization as the keys may keep changing. So you would have to go through the json step by step and detect and store it in a dictionary or any data type of your choice.

Here is the proposed object classes:

using System.Collections.Generic;

namespace ConsoleApp1
{
    public class RootObject
    {
        public RootObject()
        {
            ConjunctionNode = new List<ConjunctionNode>();
        }

        public List<ConjunctionNode> ConjunctionNode { get; set; }
    }

    public class FilterNode
    {
        public string Key { get; set; }
        public string Value { get; set; }
    }

    public class ConjunctionNode
    {
        public LikeNode Like { get; set; }
        public EQNode Eq { get; set; }
        public GTNode Gt { get; set; }
        public LTNode Lt { get; set; }
    }

    public class AndNode : ConjunctionNode
    {
    }

    public class OrNode : ConjunctionNode
    {
    }

    public class LikeNode : FilterNode
    {
    }

    public class EQNode : FilterNode
    {
    }

    public class GTNode : FilterNode
    {
    }

    public class LTNode : FilterNode
    {
    }
}

See how the objects are made. Now you will need to read the json step by step and create bifurcations and store then and then read them, or read them and create your query on the go together.

Here I just started to get the data, you can try once using this approach. If you face difficulty, feel free to come back and I can do some more work or figure out other workarounds.

using Newtonsoft.Json;
using System.IO;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            RootObject rootObject = new RootObject();
            string json = @"{ ""$and"": [ { ""$like"": { ""key"": ""name"", ""value"": ""foo"" } }, {""$gt"": { ""key"": ""price"", ""value"": 15 } } ] }  ";
            //var rootObject = JsonConvert.DeserializeObject<RootObject>(json);


            using (var reader = new JsonTextReader(new StringReader(json)))
            {
                while (reader.Read())
                {
                    //Console.WriteLine("{0} - {1} - {2}", reader.TokenType, reader.ValueType, reader.Value);
                    if (reader.TokenType.ToString() == "PropertyName")
                    {
                        //Console.WriteLine("Hi");
                        CreateConjunctionNode(reader, rootObject);
                        //CreateFilterNode(reader, rootObject);
                        //Console.WriteLine(reader.Value);
                    }
                }
            }
        }

        private static void CreateFilterNode(JsonTextReader reader, RootObject rootObject)
        {
            if (reader.Value.ToString() == "$like")
            {
                LikeNode likeNode = new LikeNode();
            }
            else if (reader.Value.ToString() == "$gt")
            {
                GTNode gTNode = new GTNode();
            }
            else if (reader.Value.ToString() == "$lt")
            {
                LTNode lTNode = new LTNode();
            }
            else if (reader.Value.ToString() == "$eq")
            {
                EQNode eQNode = new EQNode();
            }
        }

        private static void CreateConjunctionNode(JsonTextReader reader, RootObject rootObject)
        {
            if (reader.Value.ToString() == "$and")
            {
                rootObject.ConjunctionNode.Add(new AndNode());
            }
            else if (reader.Value.ToString() == "$or")
            {
                rootObject.ConjunctionNode.Add(new OrNode());
            }
        }
    }
}

Upvotes: 1

Related Questions