WCF-将空元素转换为可为空的本机类型 [英] WCF - convert empty element to nullable native type

查看:87
本文介绍了WCF-将空元素转换为可为空的本机类型的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

将SOAP字段元素保留为空会导致本机类型的转换错误。 (由于客户端的限制,严重无法使用xsi:nil = true)

Leaving a SOAP field element empty results in a cast error for native types. (sadly cannot use xsi:nil="true" due to client constraints)

将WCF合约本机类型标记为nullable<>似乎不足以停止以下错误返回给客户端。

Marking the WCF contract native type as nullable<> does not appear to be enough to stop the following error being returned to the client.


字符串不是有效的布尔值。
在System.Xml.XmlConvert.ToBoolean(String s)
在System.Xml.XmlConverter.ToBoolean(String value)
System.FormatException

The string '' is not a valid Boolean value. at System.Xml.XmlConvert.ToBoolean(String s) at System.Xml.XmlConverter.ToBoolean(String value) System.FormatException

有人知道指示DataContractSerializer将要反序列化的空元素转换为null的最佳方法吗?

does anyone know the best method of instructing the DataContractSerializer to convert empty elements to be deserialized to null?

我的示例WCF服务合同;

My example WCF service contract;

[ServiceContract()]
public interface IMyTest
{
    [OperationContract]
    string TestOperation(TestRequest request);
}

[ServiceBehavior()]
public class Settings : IMyTest
{
    public string TestOperation(TestRequest request)
    {
        if (request.TestDetail.TestBool.HasValue)
            return "Bool was specified";
        else
            return "Bool was null";
    }

}

[DataContract()]
public class TestRequest
{
    [DataMember(IsRequired = true)]
    public int ID { get; set; }

    [DataMember(IsRequired = true)]
    public TestDetail TestDetail { get; set; }
}

[DataContract()]
public class TestDetail
{
    [DataMember()]
    public bool? TestBool { get; set; }
}

我们如何使WCF接受以下提交;

How can we get WCF to accept the following submission;

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ster="mynamespace">
   <soapenv:Header/>
   <soapenv:Body>
      <ster:TestOperation>
         <ster:request>
            <ster:ID>1</ster:ID>
            <ster:TestDetail>
               <ster:TestBool></ster:TestBool>
            </ster:TestDetail>
         </ster:request>
      </ster:TestOperation>
   </soapenv:Body>
</soapenv:Envelope>

客户端只能更改其插入的值< ster: TestBool> {here}< / ster:TestBool> ,因此true或false是唯一的选择。

The client is only able to change the value it inserts <ster:TestBool>{here}</ster:TestBool> so true false or nothing are the only options.

推荐答案

好吧,我相信我已经通过使用操作行为在通过IDispatchMessageFormatter格式化其基础消息之前修改了基础消息来破解了该漏洞。

Ok I believe I have cracked this by using an Operation Behavior to modify the underlying message before its formatted via IDispatchMessageFormatter.

以下代码针对基于WCF无文件激活的服务提供了一种解决方案。

The following code provides a solution against a service that is based on WCF file-less activation.

我想让IOperationBehavior以Attribute类的形式存在。然后,我可以用我的新属性简单地装饰每个服务操作,这将激发该操作的IOperationBehavior-对最终用户而言非常好用和简单。

I wanted to have my IOperationBehavior live in the form of a Attribute class. Then I could simply decorate each Service Operation with my new attribute which would instigate the IOperationBehavior for that Operation - very nice and simple for the end user.

关键问题是您在哪里应用行为,这很关键。通过属性应用行为时,WCF调用的操作行为的顺序与在服务主机上应用时的操作行为的顺序不同。基于属性的顺序如下:

The key problem is where you apply the behavior, this is critical. The order of the operation behaviors that are called by WCF when applying the behavior via an attribute are different to when applying at the service host. The attribute based order is as follows:


  1. System.ServiceModel.Dispatcher.OperationInvokerBehavior

  2. MyOperationBehaviorAttribute

  3. System.ServiceModel.OperationBehaviorAttribute

  4. System.ServiceModel.Description.DataContractSerializerOperationBehavior

  5. System.ServiceModel.Description.DataContractSerializerOperationGenerator

  1. System.ServiceModel.Dispatcher.OperationInvokerBehavior
  2. MyOperationBehaviorAttribute
  3. System.ServiceModel.OperationBehaviorAttribute
  4. System.ServiceModel.Description.DataContractSerializerOperationBehavior
  5. System.ServiceModel.Description.DataContractSerializerOperationGenerator

由于某种原因,操作行为(仅当通过使用属性应用时)在DataContractSerializerOperationBehavior之前称为。这是一个问题,因为在调整消息(参见代码)之后,在我的行为中,我想将反序列化委托给格式化程序中的DataContractSerializerOperationBehavior格式化程序(作为内部格式化程序传递给我的行为)。当Microsoft已经提供了非常好的反序列化程序时,我不需要重新编写反序列化例程。我只是在第一个实例中更正了XML,以便将空格转换为可以在XML中正确表示的空值,以便DataContractSerializer可以将它们绑定到服务接口中的可空类型。

For some reason an operation behavior (only when applied via the use of an attribute) will be called before the DataContractSerializerOperationBehavior. This is a problem because in my behavior I want to delegate deserialization to the DataContractSerializerOperationBehavior Formatter (passed into my behavior as the inner formatter) within my formatter, after I have adjusted the message (see code). I don't want to have to re-write a deserialization routine when Microsoft provided a perfectly good deserializer already. I merely correct the XML in the first instance so that blanks are converted to nulls which are correctly represented within the XML so that the DataContractSerializer can tie them up to nullable types in the service interface.

因此,这意味着我们无法按预期使用基于属性的行为,因为在这里WCF可能会以相当微妙的方式被破坏,因为我看不到出现这种现象的原因。因此,我们仍然可以将IOperationBehavior添加到操作中,我们只需要在服务主机创建阶段手动分配它,因为随后将IOperationBehavior插入正确序列中,也就是说,在创建DataContractSerializerOperationBehavior之后,仅我可以获取内部格式化程序的引用吗?

So this means we cannot use attribute-based behaviors as they were intended since WCF may well be broken in a rather subtle way here since I can see no reason for this phenomenon. So we can still add an IOperationBehavior to an operation, we just have to manually assign it at the service host creation stage, because then our IOperationBehavior is inserted into the 'correct' sequence, that is, after the DataContractSerializerOperationBehavior has been created, only then can I get a reference to the inner formatter.

 // This operation behaviour changes the formatter for a specific set of operations in a web service.

[System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = false)]
public class NullifyEmptyElementsAttribute : Attribute
{
    // just a marker, does nothing
}

public class NullifyEmptyElementsBahavior : IOperationBehavior
{
    #region IOperationBehavior Members

    public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) 
    {

    }

    public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation) {  }

    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
    {
        // we are the server, we need to accept client message that omit the xsi:nill on empty elements
        dispatchOperation.Formatter = new NullifyEmptyElementsFormatter(dispatchOperation.Formatter);

    }

    public void Validate(OperationDescription operationDescription) { }

    #endregion IOperationBehavior Members
}

/// <summary>
///  This customized formatter intercepts the deserialization process to perform extra processing.
/// </summary>
public class NullifyEmptyElementsFormatter : IDispatchMessageFormatter
{
    // Hold on to the original formatter so we can use it to return values for method calls we don't need.
    private IDispatchMessageFormatter _innerFormatter;

    public NullifyEmptyElementsFormatter(IDispatchMessageFormatter innerFormatter)
    {
        // Save the original formatter
        _innerFormatter = innerFormatter;
    }

    /// <summary>
    /// Check each node and add the xsi{namespace}:nil declaration if the inner text is blank
    /// </summary>
    public static void MakeNillable(XElement element)
    {
        XName _nillableAttributeName = "{http://www.w3.org/2001/XMLSchema-instance}nil"; // don't worry, the namespace is what matters, not the alias, it will work

        if (!element.HasElements) // only end nodes
        {
            var hasNillableAttribute = element.Attribute(_nillableAttributeName) != null;

            if (string.IsNullOrEmpty(element.Value))
            {
                if (!hasNillableAttribute)
                    element.Add(new XAttribute(_nillableAttributeName, true));
            }
            else
            {
                if (hasNillableAttribute)
                    element.Attribute(_nillableAttributeName).Remove();
            }
        }
    }

    public void DeserializeRequest(System.ServiceModel.Channels.Message message, object[] parameters)
    {


var buffer = message.CreateBufferedCopy(int.MaxValue);

        var messageSource = buffer.CreateMessage(); // don't affect the underlying stream

        XDocument doc = null;

        using (var messageReader = messageSource.GetReaderAtBodyContents())
        {
            doc = XDocument.Parse(messageReader.ReadOuterXml()); // few issues with encoding here (strange bytes at start of string), this technique resolves that
        }

        foreach (var element in doc.Descendants())
        {
            MakeNillable(element);
        }

        // create a new message with our corrected XML
        var messageTarget = Message.CreateMessage(messageSource.Version, null, doc.CreateReader());
        messageTarget.Headers.CopyHeadersFrom(messageSource.Headers);

        // now delegate the work to the inner formatter against our modified message, its the parameters were after
         _innerFormatter.DeserializeRequest(messageTarget, parameters);
    }

    public System.ServiceModel.Channels.Message SerializeReply(System.ServiceModel.Channels.MessageVersion messageVersion, object[] parameters, object result)
    {
        // Just delegate this to the inner formatter, we don't want to do anything with this.
        return _innerFormatter.SerializeReply(messageVersion, parameters, result);
    }
}


public class MyServiceHost : ServiceHost
{
    public MyServiceHost(Type serviceType, params Uri[] baseAddresses)
        : base(serviceType, baseAddresses) { }

     protected override void OnOpening()
    {
        base.OnOpening();

        foreach (var endpoint in this.Description.Endpoints)
        {
            foreach (var operation in endpoint.Contract.Operations)
            {
                if ((operation.BeginMethod != null && operation.BeginMethod.GetCustomAttributes(_NullifyEmptyElementsBahaviorAttributeType, false).Length > 0)
                       ||
                       (operation.SyncMethod != null && operation.SyncMethod.GetCustomAttributes(_NullifyEmptyElementsBahaviorAttributeType, false).Length > 0)
                       ||
                       (operation.EndMethod != null && operation.EndMethod.GetCustomAttributes(_NullifyEmptyElementsBahaviorAttributeType, false).Length > 0))
                {
                    operation.Behaviors.Add(new NullifyEmptyElementsBahavior());
                }
            }
        }
    }
}

也许因为我只修改传入的消息,所以我可以改用IDispatchMessageInspector,它将删除对IDispatchMessageFormatter激活顺序的依赖性。但这暂时有效;)

Perhaps since I am only modifying the incoming message, I could instead use IDispatchMessageInspector which will remove the dependency on the IDispatchMessageFormatter activation order. But this works for now ;)

用法:


  1. 添加到您的操作中




 [ServiceContract(Namespace = Namespaces.MyNamespace)]
 public interface IMyServiceContrct
 {
       [OperationContract]
       [NullifyEmptyElements]
       void MyDoSomthingMethod(string someIneteger);
 }





  1. 加入您的服务

A。如果您有.svc,只需引用MyServiceHost

A. if you have .svc simply reference MyServiceHost

<%@ ServiceHost 
    Language="C#" 
    Debug="true" 
    Service="MyNameSpace.MyService"
    Factory="MyNameSpace.MyServiceHost"  %>

B。如果您使用的是无文件激活服务,请将其添加到您的web.config文件中。

B. if your using file-less activation services, add this to your web.config file

   <system.serviceModel>
        ... stuff
        <serviceHostingEnvironment multipleSiteBindingsEnabled="true" >

          <!-- WCF File-less service activation - there is no need to use .svc files anymore, WAS in IIS7 creates a host dynamically - less config needed-->
          <serviceActivations >  
            <!-- Full access to Internal services -->
            <add relativeAddress="MyService.svc" 
                 service="MyNameSpace.MyService" 
                 factory="MyNameSpace.MyServiceHost" />

          </serviceActivations>


        </serviceHostingEnvironment>
        ... stuff
    </system.serviceModel>

这篇关于WCF-将空元素转换为可为空的本机类型的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆