Mic*_*per 5 wcf serialization nullable datamember
将 SOAP 字段元素留空会导致本机类型的强制转换错误。(遗憾的是不能使用 xsi:nil="true" 由于客户端限制)
将 WCF 合同本机类型标记为 nullable<> 似乎不足以阻止向客户端返回以下错误。
字符串 '' 不是有效的布尔值。在 System.Xml.XmlConvert.ToBoolean(String s) 在 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; }
}
Run Code Online (Sandbox Code Playgroud)
我们怎样才能让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>
Run Code Online (Sandbox Code Playgroud)
客户端只能更改它插入的值,<ster:TestBool>{here}</ster:TestBool>所以 true false 或没有是唯一的选择。
好吧,我相信我已经通过使用操作行为在通过 IDispatchMessageFormatter 格式化底层消息之前修改底层消息来解决这个问题。
以下代码提供了针对基于 WCF 无文件激活的服务的解决方案。
我想让我的 IOperationBehavior 以属性类的形式存在。然后我可以简单地用我的新属性来装饰每个服务操作,这将激发该操作的 IOperationBehavior - 对于最终用户来说非常好且简单。
关键问题是你在哪里应用该行为,这很关键。通过属性应用行为时 WCF 调用的操作行为的顺序与在服务主机上应用时不同。基于属性的顺序如下:
由于某种原因,操作行为(仅当通过使用属性应用时)将在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());
}
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
也许因为我只修改传入消息,所以我可以改用 IDispatchMessageInspector,这将消除对 IDispatchMessageFormatter 激活顺序的依赖。但这目前有效;)
用法:
Run Code Online (Sandbox Code Playgroud)[ServiceContract(Namespace = Namespaces.MyNamespace)] public interface IMyServiceContrct { [OperationContract] [NullifyEmptyElements] void MyDoSomthingMethod(string someIneteger); }
A. 如果您有 .svc,只需引用 MyServiceHost
<%@ ServiceHost
Language="C#"
Debug="true"
Service="MyNameSpace.MyService"
Factory="MyNameSpace.MyServiceHost" %>
Run Code Online (Sandbox Code Playgroud)
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>
Run Code Online (Sandbox Code Playgroud)