BizTalk ESB 异常处理 - 消费 WCF 服务 - 第一部分





5.00/5 (3投票s)
管理通过 BizTalk ESB Toolkit 消费 WCF 服务时的异常
引言
你一直在使用 ESB Toolkit 构建 BizTalk 服务,并且拥有一个 ESB 路径,它正在调用 WCF 服务,你发现一切都运行得非常好。然后,你的第一个异常发生了,你的路径没有按照你期望的方式执行。如果是这种情况,我想我可以提供帮助。
背景 - BizTalk / ESB Toolkit 101
在深入探讨之前,有一些关于 BizTalk 和 ESB Toolkit 的要点值得了解。
当 BizTalk 接收到消息时,只有在管道完成其工作后,消息才会序列化到 MessageBox。
这意味着,在你的路径中的所有映射和转换之后,只有最终的最终消息才会到达 MessageBox。原始消息和中间消息都会丢失。
我从路径调用 WCF 服务时遇到的另一个有趣事实是,当 WCF 适配器引发异常时,发送端口的接收管道不会执行。我第一次看到这种情况是在尝试验证服务请求时,目标服务拒绝了我的凭据。
最后,路径通常在管道而不是业务流程中执行。你可以让它们在业务流程中执行,但这不是我今天要讨论的场景。
因此,如果你公开一个使用路径的 Web 服务,任何接收到的消息都会在 WCF 适配器配置运行的主机下的接收管道中处理。通常是隔离主机 (IIS)。
当向另一个服务发送消息时,它们由在 BizTalk 主机内执行的发送管道处理。
还有其他场景,但这些是我一直在工作的环境中常见的情况。
问题 - 捕获异常
为了帮助解释,这里有一个我发现我正在讨论的问题时所处理场景的图表。你可以看到我有一个 BizTalk 服务在中间,它调用了一个 WCF 服务。
在测试此服务期间,我想检查它在某些错误条件下是否提供了所需的响应。当我从下游服务接收到异常时,我发现结果与我预期的相去甚远。我以为路径会在收到错误时执行,我可以映射到对我的服务消费者更有意义的内容。然而,我发现我的服务消费者收到了与我从下游服务收到的完全相同的错误消息,即我的消费者收到了我从下游服务得到的“凭据失败”错误。这对我来说是一个大问题。
我看到这种行为的原因是 WCF 适配器管理异常的方式。即,它不会通过接收管道发送它们,因此路径中的任何剩余步骤都不会执行。请参阅上面的 101 部分。
解决问题
我找到了三种解决问题的方法。简单地说,它们是
- 回到使用业务流程
- 使用路径和业务流程的混合解决方案
- 深入 WCF 堆栈并强制 WCF 适配器执行我希望它做的事情。
我不喜欢第一个选项。主要原因是,我发现将基于业务流程的服务部署到不同的环境是一项费力且耗时的工作。有一些框架可以提供帮助,但它比部署基于路径的解决方案要复杂得多(这是另一个主题)。
第二个选项……好吧,这不只是给我已经试图避免的解决方案增加复杂性吗?即基于业务流程的服务。
最后一个选项正是我想要的。没有业务流程,我的服务可以更快地响应并缩短周转时间。这是因为,没有业务流程,我可以消除对 MessageBox 的多次往返,这将使我的服务响应时间缩短几百毫秒。这更具吸引力,因为速度对我的客户很重要。
解决方案 – WCF 通道包装器
事实证明,你可以让 WCF 适配器执行我希望它做的事情,而且并没有花费太大的力气。该解决方案实现起来相当简单,并且比我之前提到的两种方法灵活得多。(我确实尝试了第二种方法,并将在以后提供详细信息——第二部分)
我的解决方案是用我自己的通道包装默认的 WCF 通道,我可以在其中捕获异常并创建错误消息。
我在这里不打算深入 WCF 堆栈。你只需要知道,当调用 WCF 服务时,消息会通过一系列级别或层,其方式与 BizTalk 端口的工作方式不无相似。你可以将自己的组件(扩展、处理程序等)挂接到每一层,甚至可以用自己的组件替换一层。如果你想了解更多信息,请在网上搜索“WCF 堆栈”。
因此,经过大量的研究和与配置设置的斗争,我建立了一种用我自己的通道包装默认 WCF 通道的方法。有了这个,我就能够完全按照我想要的方式做。我的通道包装器使用默认通道来调用服务,但也允许我捕获异常。我可以将错误转换为消息,而 WCF 适配器完全不知道这一事实,只是将消息传递给接收管道,就像它是一条普通消息一样。
它是这样工作的……
步骤 1 – 创建通道包装器
供你参考。在我的示例中,我使用的是 BizTalk 2009、Visual Studio 2008 和 ESB Toolkit 2.0。
所以,这里是一些创建通道包装器的详细信息。我有一个普通的程序集,其中包含以下类型。
这看起来很复杂,但我们需要的部件并不复杂。我不会深入介绍异步、基类或实用程序类的详细信息。你可以在示例中查看这些。
该解决方案始于 ServiceBindingExtensionElement 类。这是一个配置元素,我们将在设置发送端口时引用它。反过来,Binding Extension Element 提供 ServiceBindingElement 类的实例。Binding Element 负责提供服务请求和回复通道工厂。工厂负责为我们创建通道。
我的工厂类创建自定义通道来包装默认通道。这是我的请求通道工厂如何构建包装默认通道的自定义请求通道。
protected override TChannel OnCreateChannel(EndpointAddress to, Uri via)
{
//Create the default channel
TChannel innerChannel = _innerChannelFactory.CreateChannel(to, via);
//Make sure we are working with a request channel
if (typeof(IRequestChannel) == typeof(TChannel))
{
//Create and return our wrapper channel. This wrapper is where we trap and manage
// exceptions
return (TChannel)(object)(new ServiceRequestChannel(this,
(IRequestChannel)innerChannel,
_maxBufferSize,
_messageVersion));
}
throw new InvalidOperationException();
}
实际工作发生在我的自定义 ServiceRequestChannel 中。它使用默认通道调用服务。它在 try...catch 块中执行此操作,以便我们可以在异常发生时捕获它们。这就是它的核心样子。
//Grab a buffer from the message. We use this when creating the response
// when an exception occurs
MessageBuffer messageBuffer = message.CreateBufferedCopy(int.MaxValue);
try
{
//By grabbing the buffer above, we can't use the original message so we
// create a copy to use when consuming the service
Message messageToSend = messageBuffer.CreateMessage();
/*
*
* Here is where we use the inner channel to consume the service
*
*/
reply = InnerChannel.Request(messageToSend);
string messageString = reply.ToString();
//If the response contains the word "fault" then we assume we have received an error
// NOTE: This needs to be reworked to cater for valid response messages that
// contain the word "fault". An exercise for another day.
if (messageString.ToLower().Contains("fault"))
{
string action = string.Empty;
if (message != null)
action = message.Headers.Action;
string faultstring = "An error occured processing the service request.";
XmlDocument xmlDocument = new XmlDocument();
XmlNode detailXmlNode = null;
try
{
xmlDocument = Utilities.GetXmlDocument(messageString);
faultstring = Utilities.GetXPathValue(xmlDocument, "//*[local-name()='faultstring']");
detailXmlNode = Utilities.GetXPathXmlNode(xmlDocument, "//*[local-name()='detail']");
}
catch { }
//throw an exception so that our exception handler can take over
throw new SoapException(faultstring,
SoapException.ClientFaultCode,
action,
detailXmlNode);
}
}
catch (SoapException exception)
{
//Create the reply message from the exception
reply = CreateResponseMessage(messageBuffer.CreateMessage(),
"An error occured comunicating with a sub-system.",
exception);
}
catch (CommunicationException exception)
{
//Create the reply message from the exception
reply = CreateResponseMessage(messageBuffer.CreateMessage(),
"An general error occured comunicating with a sub-system.",
exception);
}
catch (TimeoutException exception)
{
//Create the reply message from the exception
reply = CreateResponseMessage(messageBuffer.CreateMessage(),
"The operation timed out while communicating with a sub-system.",
exception);
}
catch (Exception exception)
//Create the reply message from the exception
reply = CreateResponseMessage(messageBuffer.CreateMessage(),
"An unexpected error occured",
exception);
}
finally { }
我的 ServiceRequestChannel 类有一个名为 CreateResponseMessage 的方法,它创建了一个响应
消息给我们。它看起来是这样的。
private Message CreateResponseMessage(Message message, string errorMessage, Exception exception)
{
string action = null;
if (message != null)
action = message.Headers.Action;
//Create a reply message by using our custom BodyWriter
Message replyMessage = Message.CreateMessage(MessageVersion.Default,
"*",
new ResponseBodyWriter("soap:Client",
exception.Message,
action,
errorMessage,
exception));
if (message != null)
{
if (message.Properties != null &&
message.Properties.Count > 0)
{
//Copy the properties from the source messages to the new message
replyMessage.Properties.CopyProperties(message.Properties);
}
}
//Generate a new message ID
replyMessage.Headers.MessageId = new UniqueId();
return replyMessage;
}
如你所见,我们正在使用一个名为 ResponseBodyWriter 的自定义 BodyWriter。我在这里编写一个 XML 正文,它将被转换为消息。我的 Body Writer 创建一个 SOAP 错误,可以通过我路径中的映射进行处理。这是一个来自我的 Body Writer 的回复消息示例。详细信息部分包含有关异常和所有内部异常的信息。
<SOAP:Envelope SOAP:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:SOAP="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP:Body>
<SOAP:Fault>
<faultcode>soap:Client</faultcode>
<faultstring>The request channel timed out attempting to send after 00:00:10. Increase the timeout value passed to the call to Request or increase the SendTimeout value on the Binding. The time allotted to this operation may have been a portion of a longer timeout.</faultstring>
<faultactor>http://namespace.org/AnAction</faultactor>
<detail>
<ErrorMessage>The operation timed out while communicating with a sub-system.</ErrorMessage>
<Exception>
<Message>The request channel timed out attempting to send after 00:00:10. Increase the timeout value passed to the call to Request or increase the SendTimeout value on the Binding. The time allotted to this operation may have been a portion of a longer timeout.</Message>
<Type>System.TimeoutException</Type>
<Exception>
<Message>The HTTP request to 'http://nowhere/nowhere.svc' has exceeded the allotted timeout of 00:00:00. The time allotted to this operation may have been a portion of a longer timeout.</Message>
<Type>System.TimeoutException</Type>
</Exception>
</Exception>
</detail>
</SOAP:Fault>
</SOAP:Body>
</SOAP:Envelope>
步骤 2 – 部署通道包装器
要部署,只需编译程序集并将其添加到 GAC。然后,为了使新通道可用于你的服务,打开 machine.config 文件并添加一个新的绑定扩展,如下所示。
<system.serviceModel>
<extensions>
<bindingElementExtensions>
<!-- other extensions here -->
<add name="ESBExceptionHandlingChannel" type="ESB.ExceptionHandling.ServiceModel.Configuration.ServiceBindingExtensionElement, ESB.ExceptionHandling.ServiceModel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=a496c9267b296339" />
<!-- other extensions here -->
</bindingElementExtensions >
</extensions>
</system.serviceModel>
仅将其添加到 BizTalk 配置文件意味着 IIS 将无法访问它。
如果你正在运行 64 位操作系统,请记住将此添加到 32 位和 64 位 machine.config 文件中。
此外,请记住重新启动 IIS 和所有 BizTalk 主机以使配置文件更改生效。
步骤 3 – 创建 BizTalk 应用程序
下一步是创建名为 ESB.ExceptionHandling 的 BizTalk 应用程序。我们将在创建路径时使用它。它引用 Microsoft.Practices.ESB 应用程序,因此我们可以在配置端口时选择 ESB 管道。
我们的应用程序将包含所有模式、映射和端口。我的示例包含两个模式。一个用于请求消息,另一个用于响应。应用程序还包含一个映射,它将 SOAP 错误转换为符合我们响应模式的消息。此映射的源消息类型是 Microsfot.BizTalk.GlobalPropertySchema 引用中的 BTS.soap_envelope_1__1。
我的端口配置包括一个名为 TestReceivePort 的 WCF 接收端口。它已按照 ESB Toolkit 教程中所述的方式进行设置。即,它具有所需的过滤器并使用具有以下设置的 ItinerarySelectReceiveXml 管道。
属性名称 | 值 |
ItineraryFactKey | Resolver.Itinerary |
ResolverConnectionString | ITINERARY:\\name=ESB.ExceptionHandling.Test |
我使用 WCF 发布向导通过 IIS 公开接收端口。我使用了 WCF-BasicHttp 并发布了模式,因为没有业务流程要发布。现在我可以使用 soapUI 来调用我的服务。
发送端口配置为动态端口,使用 ItinerarySendPassthrough 和 ItineraryForwarderSendReceive 管道,并采用默认设置。
这基本上是标准的 ESB 服务配置。
步骤 4 – 构建路径
现在我们准备构建路径。这是我的路径的样子。它基本上是一个直通服务,在下坡之后有一个转换。转换将把我们从适配器获得的 SOAP 错误消息转换为我们的响应类型。
下坡配置为在“Route Message”服务下的“Route to WCF Service”解析器中调用 WCF 服务。它需要有一个 WCF-Custom 传输类型,以便我们可以将我们的通道包装器添加到其中。以下是我使用的设置。你可以像我为客户那样使用 BRE,但在此示例中,我将使用静态配置。
属性 | 值 |
解析器实现 | 静态解析器扩展 |
消息交换模式 | 双向 |
传输名称 | WCF-Custom |
传输位置 | http://nowhere/nowhere.svc |
操作 | 一个动作 |
目标命名空间 | http://namespace.org |
端点配置 | BindingConfiguration=<binding name="customBinding" closeTimeout="00:00:10" openTimeout="00:00:10" sendTimeout="00:00:10"><ESBExceptionHandlingChannel /><httpTransport /></binding>&BindingType=customBinding&PropagateFaultMessage=true |
这里有趣的属性是 Endpoint Configuration。它需要设置为 customBinding,我启用了 PropagateFaultMessage 选项,但这不是必需的。
在自定义绑定中,我们添加了通道包装器和传输类型。使用我们添加到 machine.config 文件中的扩展名(即 ESBExceptionHandlingChannel)来激活我们的包装器。所以,这就是我们所拥有的。
<binding name="customBinding" closeTimeout="00:00:10" openTimeout="00:00:10" sendTimeout="00:00:10">
<ESBExceptionHandlingChannel />
<httpTransport />
</binding>
注意:我已经将超时值设置得非常低,这样在测试时就不必等待太长时间。此外,<httpTransport /> 元素必须放在最后,否则 WCF 会抱怨。
拥有路径后,你可以像往常一样部署它,然后就可以进行测试了。
步骤 5 – 测试
如前所述,我一直在使用 soapUI 测试我的服务。调用服务后,我得到的结果符合我的响应模式,如下所示。
<ns0:Response xmlns:ns0="http://ESB.ExceptionHandling.ResponseMessage">
<ResponseElement1>The operation timed out while communicating with a sub-system.</ResponseElement1>
<ResponseElement2>The request channel timed out attempting to send after 00:00:10. Increase the timeout value passed to the call to Request or increase the SendTimeout value on the Binding. The time allotted to this operation may have been a portion of a longer timeout.</ResponseElement2>
<ResponseElement3>http://namespace.org/AnAction</ResponseElement3>
</ns0:Response>
如你所见,我的示例映射只是从 SOAP 错误中提取值并将它们放入我的响应消息中。
结论
你现在拥有所有必要的工具来管理和控制你的服务在通过路径调用 WCF 服务时对异常的反应方式。
希望你觉得这有用。
在下一版中,我将探讨一种结合路径和业务流程的混合方法。我还将研究处理路径中发生的异常的方法。你可以在这里查看这些内容。
关注点
我对 WCF 堆栈以及如何扩展它以最适合我的需求有了更好的理解。尤其是在 BizTalk 领域。机会是无限的。
历史
版本 1.0
版本 1.1 - 添加了第 II 和 III 部分的链接