Problem
All solutions provided are either extremely situational, partially situational, or resource intensive due to runtime Type
checks, which heavily involve System.Reflection
which subsequently is involving the interpretation of code at runtime, rather than compilation. To combat these issues, a query formatting mechanism that is working with any type of objects, any type of nested objects, and any number of nested objects, and any type of propriety, must be created. This solution uses the Newtonsoft.Json
Nuget library to process the JSON objects.
Solution
In order to be able to transform any object in an HTTP query string the data to be formatted must be firstly serialised in a JSON object. The JSON serialisation is done to limit the data types to a very limited variation in order to make the data types predictable and easy to format as an HTTP query string. All the implementations shown involve Type checks that are set to work only on a limited set of data types and child proprieties with certain set dimensions (e.g. 1D, 2D, etc.). This approach is also increasing the performance, by using JSON object classes and verifying their JSON data types, thus avoiding the the use of reflection.
Implementation
public class Query_Formater
{
public static async Task<string> ObjectQueryFormatter<Object>(Object? value)
{
// STRING BUILDER OBJECT THAT CONTAINS THE FORMATED HTTP QUERY STRING
StringBuilder query = new StringBuilder();
// OBJECT TO BE FORMATED AS A HTTP QUERY STRING SERIALISED AS A JSON STRING AND PARSED INTO A "JObject" OBJECT
JObject json_value = JObject.Parse(Newtonsoft.Json.JsonConvert.SerializeObject(value));
if (value != null)
{
// IF THE VALUE IS NOT NULL EXTRACT THE PARAMETERS OF THE "JObject" OBJECT AND FORMAT THEM IN HTTP QUERY STRING FORMAT
await Query_Object_Parameters_Extractor(query, json_value, null);
}
// REMOVE ALL OBJECTS IN THE "JObject" OBJECT FROM THE MEMORY
json_value?.RemoveAll();
// RETURN THE FORMATED HTTP QUERY STRING
return query.ToString();
}
private static async Task<bool> Query_Array_Parameters_Extractor(StringBuilder query, JArray json_value, string name)
{
// CREATE A "StringBuilder" TO GENERATE THE PROPRIETY NAME WITHIN IT
StringBuilder name_builder = new StringBuilder();
// ENUMERATE EACH PROPRIETY IN THE "JArray"
for (int i = 0; i < json_value.Count; i++)
{
// STORE THE CURRENT PROPRIETY OF THE "JArray" OBJECT
JToken current_element = json_value.ElementAt(i);
// BUILD THE ARRAYS CURRENT PROPRIETY NAME
// IF THE ARRAY IS NOT THE CHILD OF AN OBJECT THE PROPRIETY NAME WILL BE SET BY
// USING THE ARRAY PROPRIETY NAME IS SET IN THE "Query_Object_Parameters_Extractor"
// METHOD AND PASSED TO THIS METHOD THROUGH THE "name" PARAMETER.
// (e.g. arrayValues[0], arrayValues[1] ...)
// IF THE ARRAY IS THE CHILD OF AN OBJECT THE PARENT TO CHILD RELATION IS SET IN THE
// "Query_Object_Parameters_Extractor" METHOD AND PASSED TO THIS METHOD THROUGH
// THE "name" PARAMETER.
// (e.g. object1.arrayValues[0], object1.arrayValues[1] ...)
// (e.g. object1.object2.arrayValues[0], object1.object2.arrayValues[1] ...)
name_builder.Append(name);
name_builder.Append("[");
name_builder.Append(i);
name_builder.Append("]");
// STORE THE PROCESSED PROPRIETY NAME IN A VARIABLE
string name_result = name_builder.ToString();
// CLEAR THE "StringBuilder" OBJECT
name_builder.Clear();
// IF THE CURRENT PROPRIETY IS A JSON OBJECT
if (current_element.Type == JTokenType.Object)
{
// CALL THE "Query_Object_Parameters_Extractor" METHOD AND PASS THE CURRENT OBJECT AS THE
// "JObject" OBJECT, THE "query" "StringBuilder" AS A REFERENCE, AND THE "name_result".
await Query_Object_Parameters_Extractor(query, (JObject)current_element, name_result);
}
// IF THE CURRENT PROPRIETY IS A JSON ARRAY
else if (current_element.Type == JTokenType.Array)
{
// PERFORM RECURSION TO EXTRACT THE VALUES
// FROM THE CURRENT "JArray" PROPRIETY.
await Query_Array_Parameters_Extractor(query, (JArray)current_element, name_result);
}
else
{
// FORMAT THE NAME OF THE PROPRIETY AND ITS VALUE AND ADD THEM IN THE "query" "StringBuilder"
// (e.g. PROPRIETY NAME: Query Test;
// PROPRIETY VALUE: Testing;
// RESULTING VALUE: Query+Test=Testing)
query.Append(System.Web.HttpUtility.UrlEncode(name_result));
query.Append("=");
query.Append(System.Web.HttpUtility.UrlEncode(json_value.ElementAt(i).ToString()));
}
// IF THE CURRENT JSON PROPRIETY IS NOT THE LAST ONE, APPEND "&"
// TO CONCATENATE THE PRECEDING ELEMENTS TO IT
// (e.g Query+Test=Testing&PRECEDING=1 )
if (i < json_value.Count - 1)
{
query.Append("&");
}
}
return true;
}
private static async Task<bool> Query_Object_Parameters_Extractor(StringBuilder query, JObject json_value, string? name)
{
// CREATE A "StringBuilder" TO GENERATE THE PROPRIETY NAME WITHIN IT
StringBuilder name_builder = new StringBuilder();
// GET ALL JSON PROPRIETIES OF THE "JObject" OBJECT
IEnumerable<JProperty> proprieties = json_value.Properties();
// ENUMERATE THROUGH THE PROPRIETIES OF THE "JObject" OBJECT WITH AN "IEnumerator"
IEnumerator<JProperty> proprieties_enumerator = proprieties.GetEnumerator();
try
{
// GET THE NUMBER OF PROPRIETIES WITHIN THE "JObject" OBJECT
int count = proprieties.Count();
// WHILE A PROPRIETY IS AVAILABLE WITHIN THE "IEnumerator"
while (proprieties_enumerator.MoveNext() == true)
{
// IF THE METHOD'S "name" PARAMETER IS NOT NULL
if (name != null)
{
// FOR EXAMPLE IF THE CURRENT PROPRIETY IS AN INNER OBJECT,
// APPEND THE CHILD OBJECT TO THE PARENT OBJECT NAME
// (e.g. PARENT OBJECT: calendarDates;
// CHILD OBJECT: currentDate;
// RESULTING OBJECT NAME: calendarDates.currentDate)
name_builder.Append(name);
name_builder.Append(".");
name_builder.Append(proprieties_enumerator.Current.Name);
}
else
{
// IF THE CURRENT PROPRIETY IS NOT AN INNER OBJECT, APPEND ONLY
// THE CURRENT PROPRIETY NAME
name_builder.Append(proprieties_enumerator.Current.Name);
}
// STORE THE PROCESSED PROPRIETY NAME IN A VARIABLE
string name_result = name_builder.ToString();
name_builder.Clear();
// IF THE CURRENT PROPRIETY IS A JSON OBJECT
if (proprieties_enumerator.Current.Value.Type == JTokenType.Object)
{
// PERFORM RECURSION AND PASS THE CURRENT OBJECT AS THE "JObject" OBJECT,
// THE "query" "StringBuilder" AS A REFERENCE, AND THE "name_result".
await Query_Object_Parameters_Extractor(query, (JObject)proprieties_enumerator.Current.Value, name_result);
}
// IF THE CURRENT PROPRIETY IS A JSON ARRAY
else if (proprieties_enumerator.Current.Value.Type == JTokenType.Array)
{
// CALL THE "Query_Array_Parameters_Extractor" METHOD TO EXTRACT THE VALUES
// FROM THE CURRENT "JArray" PROPRIETY.
await Query_Array_Parameters_Extractor(query, (JArray)proprieties_enumerator.Current.Value, name_result);
}
else
{
// FORMAT THE NAME OF THE PROPRIETY AND ITS VALUE AND ADD THEM IN THE "query" "StringBuilder"
// (e.g. PROPRIETY NAME: Query Test;
// PROPRIETY VALUE: Testing;
// RESULTING VALUE: Query+Test=Testing)
query.Append(System.Web.HttpUtility.UrlEncode(name_result));
query.Append("=");
query.Append(System.Web.HttpUtility.UrlEncode(proprieties_enumerator.Current.Value.ToString()));
}
// IF THE CURRENT JSON PROPRIETY IS NOT THE LAST ONE, APPEND "&"
// TO CONCATENATE THE PRECEDING ELEMENTS TO IT
// (e.g Query+Test=Testing&PRECEDING=1 )
if (count > 1)
{
query.Append("&");
}
// DECREMENT THE ELEMENT COUNT BY ONE AT EACH ITERATION
count--;
}
}
catch
{
}
finally
{
// DEALLOCATE THE "proprieties_enumerator" OBJECT FROM MEMORY
proprieties_enumerator?.Dispose();
}
return true;
}
}
The implementation has a best time complexity of O(N), if the root proprieties are not objects or lists. The implementation has an average and worst time complexity of O((N * TCO) + CP), where N equals with the number of proprieties in the root object, TCO equals with the total number of child objects, and TCP equals with the total number of child proprieties.
Testing the implementation
Test object with complex structure
The parameters of the test object are set as non-nullable in order for the API to throw errors if any parameter was not serialised properly as an HTTP query string.
public class InnerInnerTestObject
{
public string sTring { get; set; }
public List<string> t { get; set; }
}
public class InnerTestObject
{
public string sTring { get; set; }
public List<InnerInnerTestObject> t { get; set; }
}
public class TrasnsistiveObject
{
public List<InnerTestObject> t { get; set; }
}
public class TestObject
{
public string? s { get; set; }
public TrasnsistiveObject trasnsistiveObject { get; set; }
public List<InnerTestObject> t { get; set; }
}
Value assignment of the test object
TestObject obj = new TestObject();
obj.s = "Hall test string";
obj.t = new List<InnerTestObject>()
{
new InnerTestObject()
{
sTring = "Testing query formater",
t = new List<InnerInnerTestObject>()
{
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 1",
"query formater test 2",
"query formater test 3"
}
},
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 4",
"query formater test 5",
"query formater test 6"
}
},
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 7",
"query formater test 8",
"query formater test 9"
}
}
}
},
new InnerTestObject()
{
sTring = "Testing query formater",
t = new List<InnerInnerTestObject>()
{
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 10",
"query formater test 11",
"query formater test 12"
}
},
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 13",
"query formater test 14",
"query formater test 15"
}
},
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 16",
"query formater test 17",
"query formater test 18"
}
}
}
}
};
obj.trasnsistiveObject = new TrasnsistiveObject()
{
t = new List<InnerTestObject>()
{
new InnerTestObject()
{
sTring = "Testing transistive query formater",
t = new List<InnerInnerTestObject>()
{
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 1",
"query formater test 2",
"query formater test 3"
}
},
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 4",
"query formater test 5",
"query formater test 6"
}
},
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 7",
"query formater test 8",
"query formater test 9"
}
}
}
},
new InnerTestObject()
{
sTring = "Testing transistive query formater",
t = new List<InnerInnerTestObject>()
{
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 100",
"query formater test 200",
"query formater test 300"
}
},
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 400",
"query formater test 500",
"query formater test 600"
}
},
new InnerInnerTestObject()
{
sTring = "Testing query formater",
t = new List<string>()
{
"query formater test 700",
"query formater test 800",
"query formater test 900"
}
}
}
}
}
};
Query transmission to the API endpoint
// FORMAT THE OBJECT INTO A QUERY STRING AND SEND IT TO THE API ENDPOINT
System.Net.Http.HttpClient client = new HttpClient();
client.BaseAddress = new Uri(Navigation_Manager.BaseUri);
HttpResponseMessage m = await client.GetAsync("auth/get-account?" + (await Query_Formater.ObjectQueryFormatter<TestObject>(obj)));
// READ THE API ENDPOINT RESPONSE
Console.WriteLine(await m.Content.ReadAsStringAsync());
Query retrieval at API Endpoint
[HttpGet("get-account")]
public Task<ActionResult<string?>> Get([FromQuery] TestObject? data)
{
// SERIALISE THE RECEIVED QUERY OBJECT INTO JSON STRING TO READ THE DATA MORE EASILY
Console.WriteLine("Result:\n" + Newtonsoft.Json.JsonConvert.SerializeObject(data, Newtonsoft.Json.Formatting.Indented));
return Task.FromResult<ActionResult<string?>>(Content("OK Get"));
}
Live server generated response
{
"s": "Hall test string",
"trasnsistiveObject": {
"t": [
{
"sTring": "Testing transistive query formater",
"t": [
{
"sTring": "Testing query formater",
"t": [
"query formater test 1",
"query formater test 2",
"query formater test 3"
]
},
{
"sTring": "Testing query formater",
"t": [
"query formater test 4",
"query formater test 5",
"query formater test 6"
]
},
{
"sTring": "Testing query formater",
"t": [
"query formater test 7",
"query formater test 8",
"query formater test 9"
]
}
]
},
{
"sTring": "Testing transistive query formater",
"t": [
{
"sTring": "Testing query formater",
"t": [
"query formater test 100",
"query formater test 200",
"query formater test 300"
]
},
{
"sTring": "Testing query formater",
"t": [
"query formater test 400",
"query formater test 500",
"query formater test 600"
]
},
{
"sTring": "Testing query formater",
"t": [
"query formater test 700",
"query formater test 800",
"query formater test 900"
]
}
]
}
]
},
"t": [
{
"sTring": "Testing query formater",
"t": [
{
"sTring": "Testing query formater",
"t": [
"query formater test 1",
"query formater test 2",
"query formater test 3"
]
},
{
"sTring": "Testing query formater",
"t": [
"query formater test 4",
"query formater test 5",
"query formater test 6"
]
},
{
"sTring": "Testing query formater",
"t": [
"query formater test 7",
"query formater test 8",
"query formater test 9"
]
}
]
},
{
"sTring": "Testing query formater",
"t": [
{
"sTring": "Testing query formater",
"t": [
"query formater test 10",
"query formater test 11",
"query formater test 12"
]
},
{
"sTring": "Testing query formater",
"t": [
"query formater test 13",
"query formater test 14",
"query formater test 15"
]
},
{
"sTring": "Testing query formater",
"t": [
"query formater test 16",
"query formater test 17",
"query formater test 18"
]
}
]
}
]
}
SetQueryParams
does exactly what you're looking for. If you just want the URL builder and not all the HTTP stuff, it's available here. [disclaimer: I'm the author] – Rosemarie