解析 C# 中不同可能 类 的 REST api 响应

Parse REST api response for different possible classes in C#

我正在使用 REST API,returns 2 种不同类型的 XML 对同一请求的响应。

例如,如果我使用一些票号要票,对这个 API 说 12345,它要么是 returns:

  1. 门票:
  1. 或者说没有票:

(由于某些原因我无法格式化我的 XML 所以只是粘贴了屏幕截图。)

请注意,在这两种情况下,状态代码都变为 Ok。我知道这是一个糟糕的 api 设计,但我们无法对其进行任何更改。

在这个 JSON2Csharp 网站的帮助下,我想出了这些 classes 来表示响应:

门票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; }
}

错误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; }
}

然后我使用 存在 TicketNumber 调用 API。 我正在使用 RestSharp 调用 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...
}

现在效果很好。因为我知道我的回复会有票,并且会正确反序列化为 TicketsResponse 对象。

但是,如果我使用不存在的 TicketNumber 调用 API,我只会得到 TicketsResponse 具有空列表 Tickets 的对象,因为这我收到错误响应的时间。在这种情况下,状态代码也变为 OK

我这里想做的是,我想从错误响应中捕获错误信息。 (TicketError 的响应也适用于许多其他进程,因此在一次调用中获取此信息很重要。)

如果我知道这张票不存在,我可以简单地以这种方式调用 API 并捕获 errors。但这并不理想,甚至不是一个好主意:

var response = await restClient.ExecuteAsync<ErrorResponse>(request);

所以我想到了把TicketsResponseErrorResponse结合起来,像这样:

[XmlRoot]
public class CombinedResponse
{
    [XmlElement(ElementName = "Tickets")]
    public TicketsResponse Data { get; set; }
    [XmlElement(ElementName = "response")]
    public ErrorResponse NonData { get; set; }
}

并使用 class:

得到响应
var response = await restClient.ExecuteAsync<CombinedResponse>(request);

状态代码出现 OK(当它 returns 数据或错误消息时),我在 response.Content 中得到正确的响应,但反序列化不起作用,所以我的 response.Data 将显示 2 个字段 DataNonData 都显示为 null。理想情况下,我应该在 response.Data.

中获得我的 Ticket 数据或 Error 数据

所以我的问题是:

是否可以使用单个 class 进行反序列化来完成这项工作?

我在这上面花了太多时间,所以非常感谢您的帮助。 也请看看我的模型 classes 并建议是否有更好的做事方式。

我就是这样解决这个问题的。 我在这里发帖,希望对其他人有所帮助。

如果有更好的方法,请指教

我创建了一个方法来调用 API 并将响应反序列化为多种类型:

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);
    }
}

并通过发送我需要的类型来使用它:

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...
}

我的完整解决方案。如果您只是需要答案,请查看已接受的答案。

对于使用 RestSharp 和 XML 的任何人来说,这更像是整个过程的文档。

要求:

要形成一个像这样的请求体,我们需要像下面这样的几个 类:

[XmlRoot(ElementName = "request")]
[XmlInclude(typeof(RequestBodyWithTicketNumberOnly))] // To make sure 'body' can be serialized to RequestBodyWithTicketNumberOnly
public class TicketRequestBase
{
    public byte request_id { get; set; }
    public string operation { get; set; }
    public string method { get; set; }
    public string entity { get; set; }
    public string user { get; set; }
    public string password { get; set; }
    // body can have different shapes, so not giving it any specific class name.
    public object body { get; set; }
}

[XmlRoot(ElementName = "body")]
public class RequestBodyWithTicketNumberOnly
{
    public TicketWithTicketNumberOnly ticket { get; set; }
}

[XmlRoot(ElementName = "ticket")]
public class TicketWithTicketNumberOnly
{
    public string TicketNumber { get; set; }
}

一种将 C# 对象转换为 XML 字符串的方法,如下所示:

public static string ToXml<T>(T obj)
{
    var settings = new XmlWriterSettings
    {
        Indent = false,
        OmitXmlDeclaration = true,
        NewLineHandling = NewLineHandling.None,
        NewLineOnAttributes = false
    };

    var objType = obj.GetType();
    var serializer = new XmlSerializer(objType);
    var emptyNamespaces = new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });

    using (var stream = new StringWriter())
    using (var writer = XmlWriter.Create(stream, settings))
    {
        serializer.Serialize(writer, obj, emptyNamespaces);
        return stream.ToString();
    }
}

一种方法return请求正文作为XML字符串:

public static string GetTicketFetchRequestBody(string ticketNumber)
{
    if (ticketNumber== null) throw new ArgumentNullException(nameof(ticketNumber));

    var singleTicketRequest = new TicketRequestBase()
    {
        request_id = 1,
        operation = "retrieve",
        method = string.Empty,
        entity = "ticket",
        user = "sauser",
        password = "sapassword",
        body = new RequestBodyWithTicketNumberOnly() { ticket = new TicketWithTicketNumberOnly { TicketNumber = ticketNumber} }
    };

    return ToXml(singleTicketRequest);
}

回复:

回答的所有类都已经记录在这个问题中。请看一看。

获取Ticket的顶级方法:

public static async Task<IEnumerable<Ticket>> FetchTickets()
{
    var xmlRequestBody = GetTicketFetchRequestBody("12345");
    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...
}

实际调用 API 的方法使用 Proxy,记录请求和响应,并使用 Polly 重试策略执行请求:

public async Task<(T1, T2)> SendRequestAndReceiveResponse<T1, T2>(RestRequest request, bool useProxy = true)
{
    // This can be done in the constructor so we don't instantiate new client for every request.
    var restClient = new RestClient("https://someapiaddress.net");
    
    if (useProxy) // This variable can even be initialized in the constructor of this RestClient
    {
        var proxy = GetWebProxy();
        restClient.Proxy = proxy;
    }

    // Request Logging Part:
    var requestAsJSONString = GetRequestForLogging(request, restClient);
    // Log it using your logging provider.

    // Response Part:
    var response = await ExecuteAsyncWithPolicy(request, restClient);

    // Response Logging Part:
    var responseAsString = response.Content;
    // Log it using your logging provider.

    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);
    }
}

像这样创建 Web 代理:

private static WebProxy GetWebProxy()
{
    var proxyUrl = "http://proxy.companyname.com:9090/";
    return new WebProxy()
    {
        Address = new Uri(proxyUrl),
        BypassProxyOnLocal = false,
        //UseDefaultCredentials = true, // This uses: Credentials = CredentialCache.DefaultCredentials
        //*** These creds are given to the proxy server, not the web server ***
        Credentials = CredentialCache.DefaultNetworkCredentials
        //Credentials = new NetworkCredential("proxyUserName", "proxyPassword")
    };
}

使用所有参数创建请求字符串,如下所示:

private string GetRequestForLogging(IRestRequest request, IRestClient client)
{
    var serializer = new JsonSerializer();
    var requestToLog = new
    {
        // This will generate the actual Uri used in the request
        RequestUri = client.BuildUri(request),
        // Parameters are custom anonymous objects in order to have the parameter type as a nice string
        // otherwise it will just show the enum value
        parameters = request.Parameters.Select(parameter => new
        {
            name = parameter.Name,
            value = parameter.Value,
            type = parameter.Type.ToString()
        }),
        // ToString() here to have the method as a nice string otherwise it will just show the enum value
        method = request.Method.ToString()
    };

    return serializer.Serialize(requestToLog);
}

Polly重试策略:

private AsyncPolicy<IRestResponse> GetRetryPolicy()
{
    var policy = Polly.Policy.HandleResult<IRestResponse>((response) =>
    {
        return response.ResponseStatus != ResponseStatus.Completed;
    })
    //.Or<SomeKindOfCustomException>()
    .RetryAsync();

    return policy;
}

使用重试策略调用 API:

private async Task<IRestResponse> ExecuteAsyncWithPolicy(IRestRequest request, IRestClient restClient)
{
    var policy = GetRetryPolicy();
    var policyResult = await policy.ExecuteAndCaptureAsync(async () => await restClient.ExecuteAsync(request));

    return (policyResult.Outcome == OutcomeType.Successful) ? policyResult.Result : new RestResponse
    {
        Request = request,
        ErrorException = policyResult.FinalException
    };
}

希望这对您有所帮助。