I'm working with a REST API, that returns 2 different kinds of XML responses for the same request.
For example if I ask for a ticket using some ticket number, say 12345
to this API, it either returns:
(I couldn't format my XML for some reason so just pasted the screenshot.)
Note that the status code comes to be Ok
in both the cases. I'm aware that it's a bad api design but we can't change anything about it.
With some help from this JSON2Csharp website, I came up with these classes to represent the response:
The Ticket class:
[XmlRoot(ElementName = "Tickets")]
public class TicketsResponse
{
public List<Ticket> Tickets { get; set; } = new List<Ticket>();
public bool HasTickets() => Tickets.Any();
}
[XmlRoot(ElementName = "Ticket")]
public class Ticket
{
[XmlElement(ElementName = "Field1", IsNullable = true)]
public string Field1 { get; set; }
public bool ShouldSerializeField1() { return Field1 != null; }
[XmlElement(ElementName = "TicketNumber")]
public int TicketNumber { get; set; }
[XmlElement(ElementName = "SomeOtherDetails")]
public SomeOtherDetails SomeOtherDetails { get; set; }
[XmlElement(ElementName = "Accessorials")]
public object Accessorials { get; set; }
}
[XmlRoot(ElementName = "SomeOtherDetails")]
public class SomeOtherDetails
{
[XmlElement(ElementName = "SomeOtherField1", IsNullable = true)]
public string SomeOtherField1 { get; set; }
public bool ShouldSerializeSomeOtherField1() { return SomeOtherField1 != null; }
}
The Error class:
[XmlRoot(ElementName = "response")]
public class ErrorResponse
{
public byte requestId { get; set; }
public byte errorCode { get; set; }
public string errorDesc { get; set; }
public ErrorResponseBody body { get; set; }
public bool HasErrors()
{
var hasTopLevelError = errorCode != 0;
var hasErrorBody = body?.errors?.Any() ?? false;
if (hasTopLevelError || hasErrorBody)
{
return true;
}
return false;
}
public string ErrorMessage()
{
var hasTopLevelError = errorCode != 0;
var hasErrorBody = body?.errors?.Any() ?? false;
if (hasTopLevelError)
{
return errorDesc;
}
else if (hasErrorBody)
{
return string.Join(", ", body.errors.Select(e => e.errorDescription));
}
return null;
}
}
[XmlRoot(ElementName = "body")]
public class ErrorResponseBody
{
[XmlElement("errors")]
public List<Error> errors { get; set; }
}
[XmlRoot(ElementName = "Error")]
public class Error
{
public byte errorId { get; set; }
public string errorDescription { get; set; }
public string errorObjectId { get; set; }
}
I then call the API using a TicketNumber
that exists.
I'm using RestSharp for calling the api:
public async void SendRequestAndReceiveResponse()
{
var restClient = new RestClient("https://someapiaddress.net");
var requestXMLBody = "<request><request_id>1</request_id><operation>retrieve</operation><method /><entity>ticket</entity><user>someuser</user><password>somepassword</password><body><ticket><TicketNumber>12345</TicketNumber></ticket></body></request>";
var request = new RestRequest("somexmlwebservice!process.action", Method.POST);
request.AddParameter("xmlRequest", requestXMLBody, "text/xml", ParameterType.QueryString);
var response = await restClient.ExecuteAsync<TicketsResponse>(request);
// Do other stuffs with this response...
}
Now this works very well. Because I know my response will have the ticket and that will correctly deserialize to TicketsResponse
object.
But if I call the API using a TicketNumber
that doesn't exist, I simply get TicketsResponse
object that has an empty list of Tickets
because this time I'm getting error response. The status code comes to be OK
in this case too.
What I want to do here is that I want to capture the error message from the error response. (Response of either Ticket or Error applies to bunch of other processes as well, so it's important to grab this information in a single call.)
And if I knew this ticket doesn't exist, I could simply call the API this way and capture the errors
. But that's not ideal nor even a good idea:
var response = await restClient.ExecuteAsync<ErrorResponse>(request);
So I thought of combining TicketsResponse
and ErrorResponse
, like this:
[XmlRoot]
public class CombinedResponse
{
[XmlElement(ElementName = "Tickets")]
public TicketsResponse Data { get; set; }
[XmlElement(ElementName = "response")]
public ErrorResponse NonData { get; set; }
}
And get the response using that class:
var response = await restClient.ExecuteAsync<CombinedResponse>(request);
The Status code comes OK
(when it returns either data or error message) and I get my correct response in response.Content
, but the deserialization doesn't work, so my response.Data
will show 2 fields Data
and NonData
both as null
. Ideally I should have gotten either my Ticket data or Error data in response.Data
.
So my question is:
Is it possible to make this work using a single class for deserialization?
I have spent too much time on this so any help is appreciated. Also please look at my model classes and suggest if there's better way of doing things.
This is how I solved this issue. I'm posting here so others may find it helpful.
If there's a better way of doing this, please advise.
I created a method to call the API and deserialize the response to multiple types:
public async Task<(T1, T2)> SendRequestAndReceiveResponse<T1, T2>(RestRequest request)
{
// This can be done in the constructor so we don't instantiate new client for every request.
var restClient = new RestClient("https://someapiaddress.net");
// Get response:
var response = await restClient.ExecuteAsync(request);
// Log request and response here if you want.
if (response.ErrorException != null)
{
var message = $"An error occured during this request. Check request response entries for more details.";
var newException = new Exception(message, response.ErrorException);
throw newException;
}
else
{
var xmlDeserilizer = new RestSharp.Deserializers.XmlDeserializer();
var data = xmlDeserilizer.Deserialize<T1>(response);
var nonData = xmlDeserilizer.Deserialize<T2>(response);
return (data, nonData);
}
}
And used it, by sending the types I need:
public async Task<IEnumerable<Ticket>> FetchTickets()
{
var xmlRequestBody = "<request><request_id>1</request_id><operation>retrieve</operation><method /><entity>ticket</entity><user>someuser</user><password>somepassword</password><body><ticket><TicketNumber>12345</TicketNumber></ticket></body></request>";
var request = new RestRequest("somexmlwebservice!process.action", Method.POST);
request.AddParameter("xmlRequest", xmlRequestBody, "text/xml", ParameterType.QueryString);
var apiCallResult = await SendRequestAndReceiveResponse<TicketsResponse, ErrorResponse>(request);
if (apiCallResult.Item1 != null && apiCallResult.Item1.HasTickets())
{
// Do something with the tickets...
}
else if (apiCallResult.Item2 != null && apiCallResult.Item2.HasErrors())
{
// Do something with the errors...
}
// And so on...
}