【问题标题】:Parse REST api response for different possible classes in C#解析 C# 中不同可能类的 REST api 响应
【发布时间】:2022-01-06 17:37:58
【问题描述】:

我正在使用 REST API,它为同一请求返回 2 种不同类型的 XML 响应。

例如,如果我使用某个票号请求一张票,例如对该 API 说 12345,它会返回:

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

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

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

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

Ticket 类:

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

错误类:

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

并使用该类获取响应:

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

状态码来自OK(当它返回数据或错误消息时),我在response.Content 中得到正确响应,但反序列化不起作用,所以我的response.Data 将显示2 个字段@987654346 @ 和 NonData 都是 null。理想情况下,我应该在response.Data 中获得我的Ticket 数据或Error 数据。

所以我的问题是:

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

我在这方面花费了太多时间,因此不胜感激。 另外请查看我的模型类并建议是否有更好的处理方式。

【问题讨论】:

  • 您能否说明如何您正在进行 http 调用并解析响应? HTTP 状态码是否随着返回的 XML 模式而改变?
  • 您可以通过将所有这些字段和属性组合到一个类中来创建一个 C# 类。我很好奇你为什么只想要1节课?此外,如果您使用 Visual Studio,您可以复制 XML 数据,并使用菜单编辑 -> 选择性粘贴 -> 将 XML 粘贴为类。
  • 尝试使用控制器。它可以处理多种类型的响应并将结果放入您在上面发布的类中:docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions-1/…
  • @gunr2171 抱歉,我回复晚了。我只是将到目前为止我所做的所有事情都添加到了原始问题中。请看一看。
  • @thewallrus 我尝试将它们组合成一个类。而且我还更新了我的问题,说明我为什么想要这个。而且我不喜欢将 XML 粘贴为 Visual Studio 中的类,因为它生成的类的属性不是自动实现的(至少这是我的经验,也许我做错了)。

标签: c# .net xml-parsing .net-5 restsharp


【解决方案1】:

这就是我解决这个问题的方法。 我在这里发帖,以便其他人可能会发现它有帮助。

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

我创建了一个方法来调用 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...
}

【讨论】:

    【解决方案2】:

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

    对于任何处理 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();
        }
    }
    

    将请求正文作为 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);
        }
    }
    

    像这样创建网络代理:

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

    希望这对您有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-05-08
      • 1970-01-01
      • 2016-12-14
      • 2018-02-23
      • 1970-01-01
      相关资源
      最近更新 更多