I have to manage a lot of API calls to a service and all the response messages have a common structure except the "data" field which varies according to the endpoint called and whether the call was successful or not. Looking for a way to smartly manage the whole situation I produced a satisfying solution with the use of generics and Mapster. This is a typical response message:
{
"type": "send",
"datetime": "2022-02-21",
"correlation_id": "dc659b16-0781-4e32-ae0d-fbe737ff3215",
"data": {
"id": 22,
"description": "blue t-shirt with stripes",
"category": "t-shirt",
"size": "XL"
}
}
The data field is totally variable, sometimes it is a one-level structure, sometimes a multi-level structure, sometimes an array, sometimes a simple string and sometimes a null. Clearly the response messages are known and depends on the called endpoint so I know what to expect when I make the call but there is a case where the structure can still change. If the call is unsuccessful and there is an error, a 200 is still returned from the endpoint but the response is like this:
{
"type": "error",
"datetime": "2022-02-21",
"correlation_id": "dc659b16-0781-4e32-ae0d-fbe737ff3215",
"data": {
"id": 1522,
"description": "product code not found",
}
}
Looking for an elegant and concise solution to manage all cases with a single method, I was able to find a solution: These are my models:
public class Response<T> where T : class
{
public string type { get; set; }
public T data { get; set; } = null;
public Error Error { get; set; } = null;
public bool IsError => Error != null;
public string ErrorMessage => IsError ? $"An error occurred. Error code {Error.id} - {Error.description}" : "";
}
public class Customer
{
public int customerId { get; set; }
public string name { get; set; }
}
public class Supplier
{
public int supplierId { get; set; }
public string company { get; set; }
}
public class Error
{
public int id { get; set; }
public string description { get; set; }
}
And this is my function that manage all the deserializations:
private static Response<T> GetData<T>(string json) where T : class
{
//Deserialize the json using dynamic as T so can receive any kind of data structure
var resp = JsonConvert.DeserializeObject<Response<dynamic>>(json);
var ret = resp.Adapt<Response<T>>();
if (resp.type == "error")
{
//Adapt the dynamic to Error property
ret.Error = ((object)resp.data).Adapt<Error>();
ret.data = null;
}
return ret;
}
So I call my function in this way:
var customerData = GetData<Customer>("{\"type\":\"send\", \"data\": {\"id\":1, \"name\": \"John Ross\"}}");
if (customerData.IsError)
Console.WriteLine($"ERROR! {customerData.ErrorMessage}");
else
Console.WriteLine($"The response is OK. Customer name is {customerData.data.name}");
As you can see, the solution adopted is elegant and works very well. The only problem I haven't found solution is that Mapster.Adapt doesn't fail if I try to fit the wrong json to type T. So if I deserialize the json of the customer in the supplier class I would not notice the problem.
Is there a way in Mapster to know if the object I'm trying to adapt isn't compatible with the target type? So I could raise an exception and my procedure would be perfect.
Here is a repo with working example https://github.com/mmassari/MapDynamicWIthMapster
Thank you