使用 WCF 客户端从 Web 服务处理 SoapException 详细信息





5.00/5 (11投票s)
如何捕获和处理 WCF 客户端中的 ASMX 基础 SOAP 异常。
引言
本项目解决了 WCF 客户端应用程序中处理 Web 服务 (ASMX) 的 SoapException
问题。如今,在与外部合作伙伴合作的公司中,使用 Web 服务作为信息共享的手段非常普遍。这在医疗保健、银行业以及许多其他特定公司应用中都是如此。对于与外部 Web 服务合作伙伴合作的客户端来说,一个基本问题是客户端公司通常无法控制 Web 服务返回的内容,特别是异常。这确实是一个所见即所得的情况。
背景
对于“万事俱备,只欠东风”的开发人员来说,服务如何以及为何如此实现并不重要。我见过太多论坛帖子只是告诉你对 Web 服务执行此操作或那样操作,“问题解决!”显然,这些人拥有我们其他人所不具备的便利。普通开发人员不能随意去更改 Web 服务,它通常不属于我们。所以你可以看到我们面临的困境。如何让旧的 SOAP 或 ASMX Web 服务中的 SoapException
信息进入基于 WCF 的客户端?
对于不知道的人来说,WCF 保护我们免受处理 SOAP 的问题——好处/坏处……这取决于你问我哪一天。总的来说,在我看来,WCF 的异常处理远优于其他选择,但这将是另一篇未来的文章。WCF SOAP 保护(包装)SoapException
为 FaultException
。对于我们这些必须获取 SoapException
的“detail
”部分的人来说,运气似乎很糟糕。微软出于只有他们自己知道的原因,决定不公开“detail
”部分。不过,他们确实方便地提供了一个 HasDetail
属性。
因此,我们从 HasDetail
中得知 Web 服务正在为我们提供额外的 SoapException
信息,但我们无法访问它。现在,谈论一个真正“让我抓狂”的问题。这是其中之一。我研究了这个问题,并找到了几种无需放弃 WCF 的方法。WCF 提供的优点 outweighs 了异常问题。我在 MSDN 论坛上找到了一个帖子——Problem Catching SoapExceptions。看来我不是唯一遇到这个问题的开发人员。在论坛上工作时,John Saunders (MVP) 提出了一个简洁的变通方法;虽然不是乌托邦,但绝对有效。下面,我将讨论几种处理 SoapException
到 FaultException
管理问题的方法。
Using the Code
我提供了一个完整的测试 ASMX Web 服务和一个 WCF SoapClientTest 项目。关于设置解决方案(.NET 3.5 SP1 Visual Studio 2008)的说明包含在 zip 文件中。
我们试图解决的实际问题是处理 Web 服务抛出的 SOAP 异常。我必须指出,“严谨派”的开发者,作为一名称职的开发人员,应该研究 Web 服务 API 并针对 API 进行编码,以避免异常。例如,假设一项服务要求在尝试写入记录之前必须存在一个客户端 ID。另外,假设服务提供了一个“获取客户端信息”的查询方法,让你知道客户端 ID 是否存在。那么,通过良好的编码实践,完全可以避免异常。但在现实世界中,公司向终端用户客户端公开越来越多的服务,这些服务通常必须访问外部合作伙伴(如银行或货运公司)的 Web 服务,因此异常处理问题是一个真实存在的问题。
下面是一个真实的(尽管经过净化)SOAP 异常。注意异常的 detail
部分,这是关键问题,我们将在本文中看到。detail
在 SOAP 规范中进行了规定,而你并不拥有它。detail
中的所有内容都归 Web 服务所有者所有。正如我们下面将看到的,detail
的内容由 Web 服务开发人员自行决定,这就是 SOAP 异常处理如此繁琐的原因。
//This is what a complete soap exception
//looks like when captured using fiddler2
//http://www.fiddler2.com
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<soap:Body>
<soap:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>System.Web.Services.Protocols.SoapException:
Provisioning violation: unknown IP address. IP address [xxxxxxx]
is not recognized or client application is not
a licensed subscriber.</faultstring>
<faultactor>GetCustomerProfile</faultactor>
<detail>
<faulttype>Data</faulttype>
<errorlist>
<field errorcode="Client"
errortype="Provision">[127.0.0.1]</field>
</errorlist>
</detail>
</soap:Fault>
</soap:Body>
</soap:Envelope>
许多 Web 服务开发人员采取捷径,返回你请求的对象,并在对象中嵌入错误响应。其他 Web 服务开发人员走正道,并在其系统中实际开发 SOAP 异常处理。SOAP 异常有两个来源:客户端生成的异常,以及 Web 服务生成的异常。你必须同时处理这两种情况。客户端异常是诸如终结点问题、超时、网络问题等问题,这些问题阻止你访问 Web 服务或与 Web 服务断开连接。Web 服务生成的异常(如上一个)有三种形式:Schema 问题、Client 数据问题(应用程序级别)和Server 端问题。Schema 问题在 Web 服务 (XSD) 和客户端之间的合同不再同步时出现(有人在某处更改了某些内容)。客户端数据问题通常应该尽早捕获,但有时会溜过。服务器端问题可能是 Web 服务提供商网络内的任何问题;例如,他们的数据库可能已离线。在这种情况下,SOAP 异常由服务器生成,你只会收到一个“server”异常(<faultcode>soap:Server</faultcode>
);他们不会告诉你他们的内部系统为什么失败,只会告诉你它失败了。通常,在这种情况下,你唯一的选择是稍后再试。
好的。我们知道 SOAP 异常是生活中的常态,我们该怎么办?
本项目中提供的 Web 服务是基于 ASMX 的,因为在 WCF 到 WCF 客户端/服务器配置中,SOAP 异常处理不是什么大问题。此外,许多 .NET Web 服务采用者已经实现了 ASMX 解决方案,并且改用 WCF 的速度很慢。当然,还有所有其他不是并且永远不会是 .NET 的 Web 服务。因此,在你作为开发人员的职业生涯中,你很有可能会面临处理你不控制的 Web 服务的 SOAP 异常。我在本项目中创建了三种Web 服务提供商常用的 SOAP 异常生成方法。我称它们为 Simple、Multiple-element 和 Complex。由于这些异常完全是可选的,因此除了 Web 服务提供商在其 API 文档中给出的标准外,没有其他标准。这些是常见的实现;然而,可能存在任何组合,这就是为什么它不容易。
简单 SOAP 异常
“简单”SOAP 异常将在detail
内部的单个 XML 元素中包含整个错误响应。它可以是文本或额外的 XML 数据,但都包含在一个元素中。下面是几种可能性。这很简单,因为使用 WCF,你可以轻松获取第一个元素(及其所有内容)。
<!--Simple SoapException: All elements are contained
inside a single XML element inside the <detail>.-->
<detail>
<soapexception1>
<faulttype>Data</faulttype>
<errorlist>
<errorcode>123</errorcode>
<errorsource>methodName<errorsource>
<errordata>Mr. James O'brian</errordata>
</errorlist>
</soapexception1>
</detail>
//
<!--Simple SoapException: A text based (csv) exception also
contained inside a single XML element inside the <detail>.-->
<detail>
<soapexception1>
Data, 123, methodName, Mr. James O'brian
</soapexception1>
</detail>
从 SOAP 异常捕获简单元素详细信息
//Catches Simple SoapException (WCF FaultException)
catch (FaultException faultException)
//Catches the SOAP exceptions thrown by web services
{
MessageFault messageFault = faultException.CreateMessageFault();
if (messageFault.HasDetail)
{
var doc = new XmlDocument();
var node = doc.CreateNode(XmlNodeType.Element,
SoapException.DetailElementName.Name,
SoapException.DetailElementName.Namespace);
//It is easier to manage XML with LINQ so get
//the "detail" node just created above into a LINQ class
var detail = GetXElement(node); //helper method in project code
var xElement = GetXElement(messageFault.GetDetail());
//*** This is the workhorse of this exception handler.
detail.Add(xElement);
node = GetXmlNode(detail);
//helper method in project code
//information available from the WCF FaultException object
string faultNameSpace = "CODE NAMESPACE : " + faultException.Code.Namespace;
string faultCode = "CODE NAME : " + faultException.Code.Name;
string faultString = "EXCEPTION MESSAGE:\n" + faultException.Message;
//This is where you implement you API interpretation of the exception
//You can extract the data directly as shown below
//or use the XmlDocument (node) to navigate the data.
//from the element contained in the detail
string faultTypeName = "XML ELEMENT NAME : " + xElement.Name.LocalName;
string faultType = "XML ELEMENT VALUE: " + xElement.Value;
string innerText = "RAW INNERTEXT :\n" + node.InnerText;
this.rtbMessage.Text = faultNameSpace + "\n" +
faultCode + "\n" +
faultString + "\n\n" +
faultTypeName + "\n" +
faultType + "\n\n" + innerText;
}
}
当捕获到 SoapException
(由 WCF 转换为 FaultException
)时,我们希望获取 Web 服务提供的 detail
(如果存在)。假设 Web 服务开发人员提供了一些关于异常性质的详细信息,我们将希望获取这些信息。我们会根据 API 进行解释,因为使用 ASMX,我们无法获得 WCF 中可能出现的异常类。
代码漫步
首先,我们创建一个加载了抛出的 SOAP 异常的 MessageFault
实例。然后,我们检查它是否 Web 服务提供了任何详细信息。如果提供了,则获取它。在此示例中,我期望收到异常数据的文本字符串表示形式。我将故障数据放入 XmlDocument
中,以防返回的不是文本而是更多 XML,在这种情况下,我们可以轻松地导航数据。因此,对于文本响应,我们只需花费少量资源创建一个 XmlDocument
容器。CreateNode
方法为我们创建了一个“根”元素,其名称与 SoapException.DetailElementName.Name
相同,该名称是在 SoapException
中创建的。我们将把我们的故障作为该节点的一个子节点附加。顺便说一句,这会创建一个正确的 XML 文档。我发现使用 LINQ 处理 XML 更容易,所以我使用了一个辅助方法,并将节点信息放入 XElement
中。接下来,我使用相同的方法将实际的故障详细信息也放入 XElement
中。我将其附加到该节点。接下来,我将所有内容放回节点(我们的 XmlDocument
)中。你现在可以使用标准的 XmlDocument
导航来解码数据。但是,由于这是一个简单的异常,所有内容在此都已立即可用,如上面的代码所示。
多元素 SOAP 异常
至少对于 WCF 来说,更难提取的是包含多个离散 XML 元素的 SOAP 异常。为什么?还记得上面我们的 messageFault.GetDetail()
吗?它只会返回第一个 detail
项。由于 Microsoft 未在 FaultException
类中提供“detail”属性,因此其他所有内容都超出了 messageFault
对象的范围。在下面的示例 XML 数据中,我们只能通过 messageFault.GetDetail()
方法获取第一个元素。必须有办法,而且确实有,请看下面的代码。
<!--Multiple Element SoapException: Multiple discrete elements
provided in the soap exception inside the <detail>.-->
<detail>
<faulttype>Data</faulttype>
<errorcode>123</errorcode>
<errorsource>methodName<errorsource>
<errordata>Mr. James O'brian</errordata>
</detail>
从 SOAP 异常捕获多元素详细信息
//Catches Multi-Element SoapException (WCF FaultException)
catch (FaultException faultException)
//Catches the SOAP exceptions thrown by web services
{
MessageFault messageFault = faultException.CreateMessageFault();
if (messageFault.HasDetail)
{
string faultString = "EXCEPTION MESSAGE:\n" +
faultException.Message + "\n";
string faultNameSpace = "CODE NAMESPACE : " +
faultException.Code.Namespace;
string faultCode = "CODE NAME : " +
faultException.Code.Name;
//we will create dictionary of our element
//and values that exist in the XML elements.
var theXElements = new Dictionary<string, string>();
using (var xmlReader = messageFault.GetReaderAtDetailContents())
{
while (!xmlReader.EOF)
{
if (xmlReader.NodeType != XmlNodeType.EndElement)
{
string localName = xmlReader.LocalName;
string readElementString = xmlReader.ReadElementString();
theXElements.Add(localName, readElementString);
}
else
{
xmlReader.ReadEndElement();
}
}
}
//Application specific implementation of the API goes here...
this.rtbMessage.Text = faultNameSpace + "\n" +
faultCode + "\n" +
faultString + "\n";
foreach (var element in theXElements)
{
this.rtbMessage.Text += element.Key + ": " +
element.Value + "\n";
}
}
}
代码漫步
在此示例中,由于我们的 Web 服务 API 指定了多个简单的 XML 元素(无属性),因此我们将每个元素逐个读取到一个字典中,将元素标签名和元素中的值存储起来。NodeType
测试过滤掉 XML 结束标签,例如 "</errorlist>"
。ReadEndElement()
是实际“读取”行以将 reader
前移到下一个要读取的元素所必需的。当遇到 EOF
时,reader
退出。一旦我们退出 reader
,我们就有了异常处理程序的 API 实现,可以执行有意义的操作。
复杂 SOAP 异常
并没有说异常处理很容易!情况变得更糟!接下来,我们将看一下我称之为复杂 SOAP 异常的东西。它包含多个元素,以及带有元素上属性的嵌套元素。跳回本文的顶部,再看看真实的 SOAP 异常。
<!--Complex soap exception with multiple
and nested elements with attributes.-->
<detail>
<faulttype>Data</faulttype>
<errorlist>
<field errorcode="Client"
errortype="Provision">[127.0.0.1]</field>
</errorlist>
</detail>
捕获复杂 SOAP 异常
catch (FaultException faultException)
//Catches the SOAP exceptions thrown by web services
{
var fault = faultException.CreateMessageFault();
var doc = new XmlDocument();
var nav = doc.CreateNavigator();
if (nav != null)
{
using (var writer = nav.AppendChild())
{
fault.WriteTo(writer, EnvelopeVersion.Soap11);
}
//this is where you implement your API specific code
//to handle the returned detail.
string s = doc.InnerXml; //do something with it
this.rtbMessage.Text = s;
}
}
哇!这个实际上比多元素示例要小。这是因为 Web 服务提供商 API 指定了 XML 数据详细信息。这意味着我们只需将 XML 放入 XmlDocument
对象中。然后,你的任务是使用标准的 XML 方法来管理数据。如果碰巧数据是文本,那么你可以像我在上面的示例中所做的那样,将其转换为字符串。然后,你可以将字符串解析为有意义的数据。
关注点
我不会告诉你花了多少天时间才找到这个解决方案。我也知道很多开发人员在异常处理方面做得非常糟糕。我的客户中使用 ASMX 和 SOAP 异常的情况比使用 WCF 的情况要多。作为一名软件架构师,我看到了许多解决方案,好的和坏的。我的经验是,开发项目是由最低报价驱动的,或者是由内部需求驱动的,而这些需求并不有利于最佳实践。公司在匆忙投入生产的项目上损失了大量金钱,当维护开始时。公司也没有对代码进行持续审查以查找问题的政策。作为一名外部承包商,我最常在现有系统耗尽所有现有 IT 人员的时间,并且濒临崩溃时介入。
希望这能帮到你。再次感谢 John Saunders 的贡献。你可以在这里看到论坛帖子。