WCF - 将空元素转换为可为空的本机类型

时间:2013-10-29 11:14:22

标签: wcf serialization nullable datamember

将SOAP字段元素留空会导致本机类型的强制转换错误。 (遗憾的是,由于客户端限制,不能使用xsi:nil =“true”)

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

  

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

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

我的示例WCF服务合同;

[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接受以下提交;

<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,或者没有什么是唯一选项。

1 个答案:

答案 0 :(得分:2)

好的我相信我已经通过使用操作行为在通过IDispatchMessageFormatter格式化之前修改基础消息来解决这个问题。

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

我想让我的IOperationBehavior以Attribute类的形式存在。然后我可以使用我的新属性简单地装饰每个Service Operation,这将启动该操作的IOperationBehavior - 对于最终用户来说非常简单和简单。

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

  1. System.ServiceModel.Dispatcher.OperationInvokerBehavior
  2. MyOperationBehaviorAttribute
  3. System.ServiceModel.OperationBehaviorAttribute
  4. System.ServiceModel.Description.DataContractSerializerOperationBehavior
  5. System.ServiceModel.Description.DataContractSerializerOperationGenerator
  6. 由于某种原因,操作行为(仅在通过使用属性应用时)将被称为之前的 DataContractSerializerOperationBehavior。这是一个问题,因为在我的行为中,我想在我调整消息之后将反序列化委托给我的格式化程序中的DataContractSerializerOperationBehavior Formatter(作为内部格式化程序传递给我的行为)(请参阅代码)。当Microsoft提供了一个非常好的解串器时,我不想重新编写反序列化例程。我只是在第一个实例中更正了XML,以便将空格转换为在XML中正确表示的空值,以便DataContractSerializer可以将它们绑定到服务接口中的可空类型。

    所以这意味着我们不能像预期那样使用基于属性的行为,因为WCF可能会在这里以相当微妙的方式被打破,因为我看不出这种现象的原因。因此我们仍然可以向操作添加IOperationBehavior,我们只需在服务主机创建阶段手动分配它,因为我们的IOperationBehavior将被插入到正确的&#39;序列,也就是说,在创建了DataContractSerializerOperationBehavior之后,才能获得对内部格式化程序的引用。

     // 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激活顺序的依赖。但这现在适用;)

    用法:

    1. 添加到您的操作
    2.  [ServiceContract(Namespace = Namespaces.MyNamespace)]
       public interface IMyServiceContrct
       {
             [OperationContract]
             [NullifyEmptyElements]
             void MyDoSomthingMethod(string someIneteger);
       }
      
      1. 加入您的服务
      2. 一个。如果你有.svc只是引用MyServiceHost

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

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

           <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>