Using generics and reflection to do a POST, then process the JSON, then return the correct type with its data.
Problem is that I won't know in advance if the type will be a single object or a list of objects. So my generic return type needs to be either a single thing, or a list of single things. (This is .NET Core 6, so what's why the !
in some places...)
How do I handle this conundrum?
private static async Task<T> ProcessJsonResponse<TX>(HttpResponseMessage response,
string? returnModel = null)
{
var result = await response.Content.ReadAsStringAsync();
if (returnModel != null)
{
var type = Type.GetType(returnModel);
var obj = (T?)Activator.CreateInstance(type!);
var test = type!.GetMethod("FromJson");
object[] parametersArray = { result };
var tim = test!.Invoke(obj, parametersArray);
if (tim is List<T> nowwhat)
{
//can't return List<t> since the return type is T
}
return (T)tim!;
}
//<snip> other non-relevant code ...
}
CodePudding user response:
There are a few issues with this solution that are making it difficult to reason about.
This method is taking data from a dynamic language (JSON) and converting it into a static language (C#). Although generics may seem like they are dynamic, they are actually not. They need to be known at compile time.
With this in mind, the
returnModel
argument only confuses things. The method is are better off leaving it outThe best thing to do here is to bridge the gap between the dynamic language BEFORE thinking about types. In this case it is pretty easy, JSON arrays will start with a
'['
character so the system can make decisions based on thatAs @DiplomacyNotWar suggested, you need a return type that encapsulates both types of output. Sum types are great, but personally I like to leave them in the functional world. Instead, you could return a
List<T>
, which could handle both cases// 1. Return a List. This type can encapsulate both use cases private static async Task<List<T>> ProcessJsonResponse<T>(HttpResponseMessage response, bool parseFromJson) { var result = await response.Content.ReadAsStringAsync(); // 2: find out what type you will need BEFORE you do anything type based // this code is inefficient and buggy, but you get the gist bool isArray = result.TrimStart()[0] == '['; if (parseFromJson) { // 3. Have two completely separate execution paths for two different types if (isArray) { // 4. Different method to original FromJsonList var test = typeof(T)!.GetMethod("FromJsonList"); object[] parametersArray = { result }; // I am assuming that this will be a static method invoke return (List<T>)test!.Invoke(null, parametersArray)!; } else { var obj = (T?)Activator.CreateInstance(typeof(T)); var test = typeof(T)!.GetMethod("FromJson"); object[] parametersArray = { result }; var tim = (T)test!.Invoke(obj, parametersArray)!; return new List<T> { tim }; } } //<snip> other non-relevant code ... SHOLD }
CodePudding user response:
This is what I came up with - I marked @Shane's answer as correct because it put me on the right track and made me see what I was doing wrong. This isn't complete, either, as there needs to be a bit more error checking, but this is a lot better than what I originally wrote.
private static async Task<List<T>> ProcessJsonResponse<TX>.
(HttpResponseMessage response, string? returnModel = null)
{
var result = await response.Content.ReadAsStringAsync();
if (result == "[]")
{
var obj = (T)Activator.CreateInstance(typeof(T));
return new List<T> { obj };
}
if (returnModel != null)
{
var token = JToken.Parse(result);
switch (token)
{
case JObject:
{
var returnObject = JsonConvert.DeserializeObject<T>(result);
return new List<T> { returnObject };
}
case JArray:
return JsonConvert.DeserializeObject<List<T>>(result);
}
}
var model = JsonConvert.DeserializeObject<T>(result);
Debug.Assert(model != null, nameof(model) " != null");
return new List<T> { model };
}