我该如何修改WCF以处理不同(非SOAP)格式的消息?

12
我正在使用WCF与第三方公司交换消息。这些消息需要在符合ebXML规范的信封中发送和接收。理想情况下,我希望尽可能多地使用WCF堆栈,并避免采用一种方法来处理所有内容的方法,因为在这种情况下,这意味着需要再次编写大部分WCF基础架构。
据我初步研究所示,这将要求我编写自己的自定义绑定,但我在MSDN文档中难以找到清晰的说明。
我已经能够找到许多关于每个实现的详细文档,但很少有关于如何将其全部整合在一起的内容。似乎我拥有的书籍在这些主题上也缺乏相关提及,例如Peiris和Mulder的《Pro WCF》。
我的目标是像以下内容一样。发送和接收的消息必须格式化如下,其中第一个元素的名称是要执行的操作的名称,子元素是请求消息的有效载荷:
<?xml version="1.0" encoding="UTF-8"?>
<op:DoSomething xmlns:op="http://my.ebXML.schema.com" xmlns:payload="http://payload.company.com">
    <op:AnObject>
        <payload:ImportantValue>42</payload:ImportantValue>
    </op:AnObject>
</op:DoSomething>

响应将会是:

<?xml version="1.0" encoding="UTF-8"?>
<op:AcknowledgementResponse xmlns:op="http://my.ebXML.schema.com" xmlns:payload="http://payload.company.com">
    <op:ResponseObject>
        <payload:Ok>True</payload:Ok>
    </op:ResponseObject>
</op:AcknowledgementResponse>

作为消息都是由XML模式描述的,我使用XSD.exe将其转换为强类型对象。请参见https://gist.github.com/740303获取架构。请注意,这些是示例模式。我无法发布真正的模式,因为这会违反客户机密协议(即使你希望我这样做,它们也太大了)。
现在,我想按照以下方式编写服务实现:
public class MyEndpoint : IMyEndpoint
{
    public AcknowledgementResponse DoSomething(AnObject value)
    {
        return new AcknowledgementResponse
            {
                Ok = True;
            };
    }
}

非常感谢您的帮助。


你能否在gist.github.com或类似的服务上提供完整的请求和响应消息(可能包含xsd)? - larsw
@larsw 我不能提供我的客户模式,但我已经为我的示例消息创建了模式,并在我的问题中更新了链接。对我来说,关键是帮助确定一般方法,我认为我的示例消息和模式应该足够了。 - MikeD
3个回答

12

我对Tim的答案的实现细节

我需要为目前所在公司的客户写一份文档,所以我也考虑在这里发布它。希望它能帮助到某些人。我创建了一个示例客户端和服务端,并用它们来尝试一些想法。我已经清理干净并将其添加到github上。你可以在这里下载它

要使用 WCF 满足我的需求,需要实现以下功能:

  1. 不要让 WCF 期望 SOAP 消息
  2. 能够按照要求格式化请求和响应消息
  3. 所有消息都应该被考虑处理
  4. 传入的消息应该被路由到正确的操作以进行处理

1.配置 WCF 不期望 SOAP 消息

第一步是通过 TextMessageEncoder 获取传入的消息。这可以通过使用具有 textMessageEncoding 元素上 MessageVersion.None 设置的自定义绑定来实现。

  <customBinding>
    <binding name="poxMessageBinding">
      <textMessageEncoding messageVersion="None" />
      <httpTransport />
    </binding>
  </customBinding>

2. 格式化消息

为了让现有的XML格式化程序能够正确反序列化输入消息,需要使用消息格式化程序,并在消息契约上添加额外的属性。这通常不是问题,但运行客户端的ebXML模式通过XSD.exe生成了一个33000行的cs文件,我不希望对其进行任何修改。此外,人们在未来可能会忘记重新添加属性,因此这样做可以使维护更加容易。

自定义格式化程序期望将输入消息转换为第一个参数的类型,并将返回类型转换为响应消息。它会检查实现方法以确定构造函数中第一个参数和返回值的类型。

public SimpleXmlFormatter(OperationDescription operationDescription)
{
    // Get the request message type
    var parameters = operationDescription.SyncMethod.GetParameters();
    if (parameters.Length != 1)
        throw new InvalidDataContractException(
"The SimpleXmlFormatter will only work with a single parameter for an operation which is the type of the incoming message contract.");
    _requestMessageType = parameters[0].ParameterType;

    // Get the response message type
    _responseMessageType = operationDescription.SyncMethod.ReturnType;
}

然后它简单地使用XmlSerializer对数据进行序列化和反序列化。对我来说,很有趣的一部分是使用自定义的BodyWriter将对象序列化到Message对象中。以下是服务序列化器的实现的一部分。客户端实现相反。

public void DeserializeRequest(Message message, object[] parameters)
{
    var serializer = new XmlSerializer(_requestMessageType);
    parameters[0] = serializer.Deserialize(message.GetReaderAtBodyContents());
}

public Message SerializeReply(MessageVersion messageVersion, object[] parameters, object result)
{
    return Message.CreateMessage(MessageVersion.None, _responseMessageType.Name,
                                 new SerializingBodyWriter(_responseMessageType, result));
}

private class SerializingBodyWriter : BodyWriter
{
    private readonly Type _typeToSerialize;
    private readonly object _objectToEncode;

    public SerializingBodyWriter(Type typeToSerialize, object objectToEncode) : base(false)
    {
        _typeToSerialize = typeToSerialize;
        _objectToEncode = objectToEncode;
    }

    protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
    {
        writer.WriteStartDocument();
        var serializer = new XmlSerializer(_typeToSerialize);
        serializer.Serialize(writer, _objectToEncode);
        writer.WriteEndDocument();
    }
}

3. 处理所有传入消息

为了让WCF允许处理所有传入的消息,我需要将endpointDispatcher的ContractFilter属性设置为MatchAllMessageFilter的一个实例。以下是我的端点行为配置中说明此示例的代码片段。

public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
{
    endpointDispatcher.ContractFilter = new MatchAllMessageFilter();
    // Do more config ...
}

4. 选择正确的操作

为了实现这一点,创建了一个实现IDispatchOperationSelector接口的类。在SelectOperation方法中,我检查传入的消息。这里我需要查找两件事情: 1. 根元素的命名空间与服务契约的命名空间相同的合理性检查 2. 根元素的名称(请注意使用LocalName删除任何命名空间前缀)

根元素的名称是返回值,它将映射到具有匹配名称或操作属性具有匹配值的契约上的任何操作。

public string SelectOperation(ref Message message)
{
    var messageBuffer = message.CreateBufferedCopy(16384);

    // Determine the name of the root node of the message
    using (var copyMessage = messageBuffer.CreateMessage())
    using (var reader = copyMessage.GetReaderAtBodyContents())
    {
        // Move to the first element
        reader.MoveToContent();

        if (reader.NamespaceURI != _namespace)
            throw new InvalidOperationException(
"The namespace of the incoming message does not match the namespace of the endpoint contract.");

        // The root element name is the operation name
        var action = reader.LocalName;

        // Reset the message for subsequent processing
        message = messageBuffer.CreateMessage();

        // Return the name of the action to execute
        return action;
    }
}

总结

为了更容易部署,我创建了一个端点行为来处理消息格式化器、协定过滤器和操作选择器的配置。我也可以创建一个绑定来包装自定义绑定配置,但我认为那一部分不太难记住。

对我来说,有趣的发现之一是,端点行为可以设置所有操作的消息格式化器,以使用自定义消息格式化器。这样可以避免单独配置它们的需要。我从其中一个Microsoft示例中学到了这个技巧。

有用的文档链接

到目前为止,我发现最好的参考资料是Service Station MSDN杂志文章(Google搜索“site:msdn.microsoft.com service station WCF”)。

深入WCF绑定 - 非常有用的关于配置绑定的信息

使用自定义行为扩展WCF - 是我迄今找到的关于调度程序集成点的最佳信息来源,它们包含一些真正有用的图表,说明所有的整合点以及它们在处理顺序中出现的位置。

Microsoft WCF示例 - 这里有很多其他地方没有很好记录的内容。我发现阅读其中一些源代码非常有教益。


看起来不错!我猜当你实现自定义消息格式化程序时,仍需要选择适当的编码方式,而HTTP绑定默认会提供SOAP。消息格式化程序的实现也是我想要完成的(委托给标准XML序列化程序)。然而,使用EndpointBehavior是我没有考虑到的,但我猜它确实可以简化事情。我+1了你的答案,因为你在发布解决方案方面付出了努力,值得获得声望。我也会在GitHub上查看。 - Tim Roberts

5

我认为你不需要对绑定进行任何操作。我假设你需要通过HTTP发送ebXML格式的消息?

@ladislav的答案是一种方法,但我认为消息编码器的设计要比你尝试实现的更低级。它们基本上是将消息编码为底层流的片段(即消息在流上表示为字节的方式)。

我认为你需要实现一个自定义消息格式化程序。特别是,由于你说你想将消息提交给第三方,那么我认为只需要实现IClientMessageFormatter接口即可。另一个接口(IDispatchMessageFormatter)用于服务器端。

你还需要实现适当的ServiceBehavior和OperationBehavior来安装格式化程序到堆栈中,但这部分代码很少(大部分代码将在实现上述提到的接口中完成)。

一旦实施,您可以使用“一个方法处理所有”方法来测试和调试格式化程序。只需将接收到的消息转储到控制台进行审查,然后还可以发送ebXML响应。您也可以使用相同的方法来构建单元测试。

抱歉,我不小心写错了,我不仅需要发送这些消息,还需要接收这些消息。我需要确保接收到的消息被正确的服务方法处理,但这需要一个操作头。似乎唯一的填充操作头的方法是在绑定中提供它,除非我漏掉了什么。 - MikeD
@MikeD 好的,那么你也将在服务器端工作。在这种情况下,您还需要实现IDispatchMessageFormatter接口,然后您还需要一种方法将传入的消息与正确的操作关联起来。我认为可以使用IDispatchOperationSelector来完成这项工作,但我目前不确定如何将其引入堆栈中(可能是通过您用于添加格式化程序的Service / OperationBehavior)。 - Tim Roberts
我认为你说得对。我已经尝试过使用自定义的IDispatchOperationSelector,如果将其与仅匹配所有消息的Contract Filter配对,那么我就不需要绑定扩展程序了。我今天下午会尝试一下这个方法。 - MikeD
谢谢Tim。你说得对,我可以使用自定义操作选择器和消息格式化程序来完成所有操作。在自定义绑定扩展中添加一个操作头是一个分散注意力的因素,因为我当时不知道合同过滤器。我会在我的问题中发布一个包含解决方案的示例项目。 - MikeD
1
我已经将您的建议实现作为另一个答案发布在这个问题和Github上。我很想听听您的反馈意见。 - MikeD

1

如果您需要自定义消息格式,您需要使用自定义MessageEncoderMSDN包含了创建自定义编码器的示例。如果您使用Reflector,您将找到几个编码器实现,因此您可以学习如何编写它。

您还可以检查一下,如果您尝试使用TextMessageEncoder和MessageVersion.None会发生什么(我从未尝试过)。


mmka,这是一个很好的建议。我一直在寻找类似的解决方案,使用TextMessageEncoder和MessageVersion.None可以实现。现在我需要的是一种获取操作的方法,它是第一个元素的名称... - MikeD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接