【问题标题】:WCF - convert empty element to nullable native typeWCF - 将空元素转换为可为空的本机类型
【发布时间】:2013-10-29 11:14:22
【问题描述】:

将 SOAP 字段元素留空会导致原生类型的转换错误。 (遗憾的是,由于客户端限制,无法使用 xsi:nil="true")

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

字符串 '' 不是有效的布尔值。 在 System.Xml.XmlConvert.ToBoolean(String s) 在 System.Xml.XmlConverter.ToBoolean(字符串值) 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>

客户端只能更改它插入的值&lt;ster:TestBool&gt;{here}&lt;/ster:TestBool&gt;,所以只有 true false or nothing 是唯一的选项。

【问题讨论】:

  • 看起来他们正在发送一个空值而不是nil
  • 确实,我已经用一些限制更新了我的问题。
  • 您可以将属性更改为字符串吗?
  • 这会污染数据合约,从而有效地渲染 .net 类型的点……毫无意义。这是一个消息格式问题,我必须处理客户端的限制以及客户端可以发送给我的 XML 方面的内容。我一直在研究实现一个自定义 IOperationBehavior,它分配一个自定义 IDispatchMessageFormatter 来拦截具有空字符串值的参数。我想让 IOperationBehavior 成为一个属性,我可以在 ServiceContract 中装饰我的每个相关 OperationContract 声明。我会尽快报告。
  • 如果 WCF 期望我们这样做 WCF 期望与描述服务契约的 XSD 匹配的 XML。空元素与nil 不同。 stackoverflow.com/a/774234/242520

标签: wcf serialization nullable datamember


【解决方案1】:

好的,我相信我已经通过使用操作行为来修改底层消息,然后再通过 IDispatchMessageFormatter 对其进行格式化。

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

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

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

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

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

所以这意味着我们不能按预期使用基于属性的行为,因为 WCF 很可能在这里以一种相当微妙的方式被破坏,因为我看不出这种现象的原因。所以我们仍然可以在操作中添加一个 IOperationBehavior,我们只需要在服务主机创建阶段手动分配它,因为然后我们的 IOperationBehavior 被插入到“正确”的序列中,即在创建 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. 添加到您的操作中
 [ServiceContract(Namespace = Namespaces.MyNamespace)]
 public interface IMyServiceContrct
 {
       [OperationContract]
       [NullifyEmptyElements]
       void MyDoSomthingMethod(string someIneteger);
 }
  1. 融入您的服务

A.如果您有 .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>

【讨论】:

  • 这是您在订单依赖项中遇到的一个奇怪问题。不过,就删除字符串文字而言 - 您可以将自定义属性应用于所有需要该行为的方法,但这仅用作标记,而不是实现IOperationBehavior。然后您现有的代码可以遍历所有操作,检索SyncMethod/BeginMethod/TaskMethod 并测试属性是否存在以确定是否应用该行为。
  • 哇,感谢这个写得很好的答案。我现在使用您建议的方式运行我的行为。这是我在此找到的一些 MSDN 文档,有效地建议与此处提出的相同:blogs.msdn.com/b/carlosfigueira/archive/2011/05/03/…
  • WCF 非常擅长扩展性。微软很好地照顾了我们的开发人员。
猜你喜欢
  • 2022-01-22
  • 2012-04-21
  • 1970-01-01
  • 2014-01-26
  • 2017-01-11
  • 2019-07-18
  • 2017-05-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多