3

背景

不久前我写了一个 WCF 服务,它大量使用了自定义操作调用程序、错误处理程序和行为——其中许多严重依赖于特定类型的输入消息或消息的基本消息类型(每个 DataContract 都继承自基类和许多接口)。还为所涉及的各种接口和类设置了许多单元和集成测试。另外,软件每次修改都要经过严格的签核流程,重写服务层也不是我想的好玩的。

它当前配置为允许 JSON 和 SOAP 请求进入。

问题

由于旧版软件的限制,客户希望使用 application/x-www-form-urlencoded 内容类型向此服务发布。通常,服务会接受如下所示的 JSON 请求:

{
"username":"jeff",
"password":"mypassword",
"myvalue":12345
}

而客户端可以发送的 application/x-www-form-urlencoded 消息体看起来有点像这样:

username=jeff&password=mypassword&myvalue=12345

或者,客户通知我他们可以按如下方式格式化消息(如果有用的话):

myjson={username:jeff,password:mypassword,myvalue:12345}

还要考虑服务合同如下所示:

[ServiceContract(Namespace = "https://my.custom.domain.com/")]
public interface IMyContract {
    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, UriTemplate = "process")]
    MyCustomResponse Process(MyCustomRequest req);
}

我想保留 MyCustomRequest,并避免用 Stream 替换它(根据下面的链接)。

我发现许多帖子建议如何使用 Stream OperationContract 参数来实现这一点,但在我的特定实例中,更改 OperationContract 参数的类型需要做很多工作。下面的帖子会详细介绍:

在 WCF 中使用 x-www-form-urlencoded Content-Type

使用 WCF 支持“application/x-www-form-urlencoded”发布数据的最佳方式?

http://www.codeproject.com/Articles/275279/Developing-WCF-Restful-Services-with-GET-and-POST

虽然我没有发现任何特别有用的东西。

问题

有什么方法可以在消息到达操作合同之前拦截消息,并将其从客户端的输入转换为我的自定义类,然后让应用程序的其余部分按正常方式处理它?

自定义消息检查器?操作选择器?自从我进入 WCF 的胆量以来已经有一段时间了,所以我现在有点生疏了。我花了一段时间寻找下图,因为我记得使用它来提醒我调用堆栈 - 如果它仍然相关!

http://i.stack.imgur.com/pT4o0.gif

4

2 回答 2

4

所以,我使用消息检查器解决了这个问题。它不漂亮,但它适用于我的情况!

using System;

public class StreamMessageInspector : IDispatchMessageInspector {
    #region Implementation of IDispatchMessageInspector

    public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) {
        if (request.IsEmpty) {
            return null;
        }

        const string action = "<FullNameOfOperation>";

        // Only process action requests for now
        var operationName = request.Properties["HttpOperationName"] as string;
        if (operationName != action) {
            return null;
        }

        // Check that the content type of the request is set to a form post, otherwise do no more processing
        var prop = (HttpRequestMessageProperty)request.Properties[HttpRequestMessageProperty.Name];
        var contentType = prop.Headers["Content-Type"];
        if (contentType != "application/x-www-form-urlencoded") {
            return null;
        }

        ///////////////////////////////////////
        // Build the body from the form values
        string body;

        // Retrieve the base64 encrypted message body
        using (var ms = new MemoryStream()) {
            using (var xw = XmlWriter.Create(ms)) {
                request.WriteBody(xw);
                xw.Flush();
                body = Encoding.UTF8.GetString(ms.ToArray());
            }
        }

        // Trim any characters at the beginning of the string, if they're not a <
        body = TrimExtended(body);

        // Grab base64 binary data from <Binary> XML node
        var doc = XDocument.Parse(body);
        if (doc.Root == null) {
            // Unable to parse body
            return null;
        }

        var node = doc.Root.Elements("Binary").FirstOrDefault();
        if (node == null) {
            // No "Binary" element
            return null;
        }

        // Decrypt the XML element value into a string
        var bodyBytes = Convert.FromBase64String(node.Value);
        var bodyDecoded = Encoding.UTF8.GetString(bodyBytes);

        // Deserialize the form request into the correct data contract
        var qss = new QueryStringSerializer();
        var newContract = qss.Deserialize<MyServiceContract>(bodyDecoded);

        // Form the new message and set it
        var newMessage = Message.CreateMessage(OperationContext.Current.IncomingMessageVersion, action, newContract);
        request = newMessage;
        return null;
    }

    public void BeforeSendReply(ref Message reply, object correlationState) {
    }

    #endregion

    /// <summary>
    ///     Trims any random characters from the start of the string. I would say this is a BOM, but it doesn't seem to be.
    /// </summary>
    /// <param name="s"></param>
    /// <returns></returns>
    private string TrimExtended(string s) {
        while (true) {
            if (s.StartsWith("<")) {
                // Nothing to do, return the string
                return s;
            }

            // Replace the first character of the string
            s = s.Substring(1);
            if (!s.StartsWith("<")) {
                continue;
            }
            return s;
        }
    }
}

然后我创建了一个端点行为并通过 WCF 配置添加它:

public class StreamMessageInspectorEndpointBehavior : BehaviorExtensionElement, IEndpointBehavior {
    public void Validate(ServiceEndpoint endpoint) {

    }

    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) {

    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) {
        endpointDispatcher.DispatchRuntime.MessageInspectors.Add(new StreamMessageInspector());
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) {

    }

    #region Overrides of BehaviorExtensionElement

    protected override object CreateBehavior() {
        return this;
    }

    public override Type BehaviorType {
        get { return GetType(); }
    }

    #endregion
}

以下是配置更改的摘录:

<extensions>
    <behaviorExtensions>
        <add name="streamInspector" type="My.Namespace.WCF.Extensions.Behaviors.StreamMessageInspectorEndpointBehavior, My.Namespace.WCF, Version=1.0.0.0, Culture=neutral" />
    </behaviorExtensions>
</extensions>
<behaviors>
    <endpointBehaviors>
        <behavior name="MyEndpointBehavior">
            <streamInspector/>
        </behavior>
    </endpointBehaviors>

QueryStringSerializer.Deserialize() 将查询字符串反序列化为 DataContract(基于 DataMember.Name 属性,如果 DataMember 属性不存在,则基于属性名称)。

于 2014-05-09T07:00:54.870 回答
2

不知道你有多少自由来更新你的ServiceContract,但我会尝试按如下方式扩展它:

[ServiceContract(Namespace = "https://my.custom.domain.com/")]
public interface IMyContract {
    [OperationContract]
    [WebInvoke(Method = "POST", RequestFormat = WebMessageFormat.Json, UriTemplate = "process")]
    MyCustomResponse Process(MyCustomRequest req);

    [OperationContract]
    [WebInvoke(Method = "POST", UriTemplate = "processForm")]
    MyCustomResponse ProcessForm(MyCustomRequest req);
}

然后会给这个客户新的 URL 来发布。

于 2014-05-08T20:11:45.570 回答