.NET 4.5 XmlSerializer、xsi:nillable 属性和字符串值的问题
Problems with .NET 4.5 XmlSerializer, xsi:nillable attribute, and string values
O Whosebug 的伟大人物,听听我的请求:
我正在编写 .NET 4.5 库代码来与 Oracle SalesCloud 服务对话,但我遇到了 C# 对象上具有空字符串值的 SOAP 请求的问题。
属性的XSD指定如下:
<xsd:element minOccurs="0" name="OneOfTheStringProperties" nillable="true" type="xsd:string" />
在 VS2013 中使用 "Add Service Reference..." 实用程序并编写一个请求,其中我正在更新 OneOfTheStringProperties 以外的内容,输出是
<OneOfTheStringProperties xsi:nil="true></OneOfTheStringProperties>
在服务器端,这会导致两个问题。首先,由于只读属性也是以这种方式指定的,服务器拒绝整个请求。其次,这意味着我可能无意中删除了我想要保留的值(除非我每次都将每个 属性 发回......效率低下并且编码很痛苦。)
我的 Google-fu 在这方面很薄弱,我不想深入研究编写自定义 XmlSerializer(以及与之相关的所有 testing/edge 案例),除非它是最佳路线。
到目前为止,我能找到的最好方法是遵循 [属性]指定的模式。这样做,然后,对于每个可用的字符串 属性 意味着我必须将以下内容添加到 Reference.cs
中的定义中
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool OneOfTheStringPropertiesSpecified
{
get { return !String.IsNullOrEmpty(OneOfTheStringProperties); }
set { }
}
需要大量输入,但它有效,而且 SOAP 消息的日志跟踪是正确的。
我希望就三种方法之一获得建议:
一个配置开关、特定的 XmlSerializer 覆盖,或一些其他的修复,这些修复将抑制空字符串的 .NET 4.5 XmlSerializer 输出
类似的秘方会推出"proper"XML比如<OneOfTheStringProperties xsi:nil="true" />
创建扩展(或现有 VS2013 扩展)的有针对性的教程,允许我右键单击字符串 属性 并插入以下模式:
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool [$StringProperty]Specified
{
get { return !String.isNullOrEmpty([$StringProperty]); }
set { }
}
我也乐于接受任何其他建议。如果只是使用正确的搜索词(显然我不是),那也将不胜感激。
知识的守护者啊,为了促进这一请求,我提供这个 sacrificial goat。
添加说明
只是为了确定,我不是在寻找一键式灵丹妙药。作为一名开发人员,尤其是在底层结构经常因需求而变化的团队中工作的开发人员,我知道要保持现状需要大量工作。
不过,我正在寻找的是合理减少每次我必须对结构进行刷新时的工作量(对于其他人,一个简化的方法来实现同样的事情。)例如,使用*Specified 表示为给定的示例输入大约 165 个以上的字符。对于包含 45 个字符串字段的合同,这意味着每次模型更改时我都必须键入超过 7,425 个字符——这是针对一个服务对象的! 大约有 10-20服务对象待售。
右键单击的想法会将其减少到 45 次右键单击操作...更好。
class 上的自定义属性会更好,因为每次刷新只需执行一次。
理想情况下,app.config 中的运行时设置将是一次性的——不管第一次实施有多难,因为它进入了库。
我认为真正的答案比几乎 7500 characters/class 更好,可能不如简单的 app.config 设置好,但它要么存在,要么我相信它可以实现。
这不是完美的解决方案,但沿着 45 右键单击行,您可以使用 T4 Text Template 在与生成的 Web 服务代码分开的部分 class 声明中生成 XXXSpecified 属性.
然后单击鼠标右键 -> 运行 自定义工具可在更新服务引用时重新生成 XXXSpecified 代码。
这是一个示例模板,它为给定命名空间中 classes 的所有字符串属性生成代码:
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="$(SolutionDir)<Path to assembly containing service objects>" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ output extension=".cs" #>
<#
string serviceObjectNamespace = "<Namespace containing service objects>";
#>
namespace <#= serviceObjectNamespace #> {
<#
foreach (Type type in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes())
.Where(t => t.IsClass && t.Namespace == serviceObjectNamespace)) {
var properties = type.GetProperties().Where(p => p.PropertyType == typeof(string));
if (properties.Count() > 0) {
#>
public partial class <#= type.Name #> {
<#
foreach (PropertyInfo prop in properties) {
#>
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool <#= prop.Name#>Specified
{
get { return <#= prop.Name#> != null; }
set { }
}
<#
}
#>
}
<#
} }
#>
}
以下是向 WCF 客户端添加自定义行为的方法,该行为可用于检查消息并跳过属性。
这是以下组合:
- https://wcfpro.wordpress.com/2011/03/29/iclientmessageinspector/
- http://blogs.msdn.com/b/stcheng/archive/2009/02/21/wcf-how-to-inspect-and-modify-wcf-message-via-custom-messageinspector.aspx
完整代码:
void Main()
{
var endpoint = new Uri("http://somewhere/");
var behaviours = new List<IEndpointBehavior>()
{
new SkipConfiguredPropertiesBehaviour(),
};
var channel = Create<IRemoteService>(endpoint, GetBinding(endpoint), behaviours);
channel.SendData(new Data()
{
SendThis = "This should appear in the HTTP request.",
DoNotSendThis = "This should not appear in the HTTP request.",
});
}
[ServiceContract]
public interface IRemoteService
{
[OperationContract]
int SendData(Data d);
}
public class Data
{
public string SendThis { get; set; }
public string DoNotSendThis { get; set; }
}
public class SkipConfiguredPropertiesBehaviour : IEndpointBehavior
{
public void AddBindingParameters(
ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(
ServiceEndpoint endpoint,
ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new SkipConfiguredPropertiesInspector());
}
public void ApplyDispatchBehavior(
ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher)
{
}
public void Validate(
ServiceEndpoint endpoint)
{
}
}
public class SkipConfiguredPropertiesInspector : IClientMessageInspector
{
public void AfterReceiveReply(
ref Message reply,
object correlationState)
{
Console.WriteLine("Received the following reply: '{0}'", reply.ToString());
}
public object BeforeSendRequest(
ref Message request,
IClientChannel channel)
{
Console.WriteLine("Was going to send the following request: '{0}'", request.ToString());
request = TransformMessage(request);
return null;
}
private Message TransformMessage(Message oldMessage)
{
Message newMessage = null;
MessageBuffer msgbuf = oldMessage.CreateBufferedCopy(int.MaxValue);
XPathNavigator nav = msgbuf.CreateNavigator();
//load the old message into xmldocument
var ms = new MemoryStream();
using(var xw = XmlWriter.Create(ms))
{
nav.WriteSubtree(xw);
xw.Flush();
xw.Close();
}
ms.Position = 0;
XDocument xdoc = XDocument.Load(XmlReader.Create(ms));
//perform transformation
var elementsToRemove = xdoc.Descendants().Where(d => d.Name.LocalName.Equals("DoNotSendThis")).ToArray();
foreach(var e in elementsToRemove)
{
e.Remove();
}
// have a cheeky read...
StreamReader sr = new StreamReader(ms);
Console.WriteLine("We're really going to write out: " + xdoc.ToString());
//create the new message
newMessage = Message.CreateMessage(xdoc.CreateReader(), int.MaxValue, oldMessage.Version);
return newMessage;
}
}
public static T Create<T>(Uri endpoint, Binding binding, List<IEndpointBehavior> behaviors = null)
{
var factory = new ChannelFactory<T>(binding);
if (behaviors != null)
{
behaviors.ForEach(factory.Endpoint.Behaviors.Add);
}
return factory.CreateChannel(new EndpointAddress(endpoint));
}
public static BasicHttpBinding GetBinding(Uri uri)
{
var binding = new BasicHttpBinding()
{
MaxBufferPoolSize = 524288000, // 10MB
MaxReceivedMessageSize = 524288000,
MaxBufferSize = 524288000,
MessageEncoding = WSMessageEncoding.Text,
TransferMode = TransferMode.Buffered,
Security = new BasicHttpSecurity()
{
Mode = uri.Scheme == "http" ? BasicHttpSecurityMode.None : BasicHttpSecurityMode.Transport,
}
};
return binding;
}
这是 LinqPad 脚本的 link:http://share.linqpad.net/kgg8st.linq
如果你 运行 它,输出将是这样的:
Was going to send the following request: '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IRemoteService/SendData</Action>
</s:Header>
<s:Body>
<SendData xmlns="http://tempuri.org/">
<d xmlns:d4p1="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<d4p1:DoNotSendThis>This should not appear in the HTTP request.</d4p1:DoNotSendThis>
<d4p1:SendThis>This should appear in the HTTP request.</d4p1:SendThis>
</d>
</SendData>
</s:Body>
</s:Envelope>'
We're really going to write out: <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action a:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none" xmlns:a="http://schemas.xmlsoap.org/soap/envelope/">http://tempuri.org/IRemoteService/SendData</Action>
</s:Header>
<s:Body>
<SendData xmlns="http://tempuri.org/">
<d xmlns:a="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:SendThis>This should appear in the HTTP request.</a:SendThis>
</d>
</SendData>
</s:Body>
</s:Envelope>
O Whosebug 的伟大人物,听听我的请求:
我正在编写 .NET 4.5 库代码来与 Oracle SalesCloud 服务对话,但我遇到了 C# 对象上具有空字符串值的 SOAP 请求的问题。
属性的XSD指定如下:
<xsd:element minOccurs="0" name="OneOfTheStringProperties" nillable="true" type="xsd:string" />
在 VS2013 中使用 "Add Service Reference..." 实用程序并编写一个请求,其中我正在更新 OneOfTheStringProperties 以外的内容,输出是
<OneOfTheStringProperties xsi:nil="true></OneOfTheStringProperties>
在服务器端,这会导致两个问题。首先,由于只读属性也是以这种方式指定的,服务器拒绝整个请求。其次,这意味着我可能无意中删除了我想要保留的值(除非我每次都将每个 属性 发回......效率低下并且编码很痛苦。)
我的 Google-fu 在这方面很薄弱,我不想深入研究编写自定义 XmlSerializer(以及与之相关的所有 testing/edge 案例),除非它是最佳路线。
到目前为止,我能找到的最好方法是遵循 [属性]指定的模式。这样做,然后,对于每个可用的字符串 属性 意味着我必须将以下内容添加到 Reference.cs
中的定义中[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool OneOfTheStringPropertiesSpecified
{
get { return !String.IsNullOrEmpty(OneOfTheStringProperties); }
set { }
}
需要大量输入,但它有效,而且 SOAP 消息的日志跟踪是正确的。
我希望就三种方法之一获得建议:
一个配置开关、特定的 XmlSerializer 覆盖,或一些其他的修复,这些修复将抑制空字符串的 .NET 4.5 XmlSerializer 输出
类似的秘方会推出"proper"XML比如
<OneOfTheStringProperties xsi:nil="true" />
创建扩展(或现有 VS2013 扩展)的有针对性的教程,允许我右键单击字符串 属性 并插入以下模式:
[System.Xml.Serialization.XmlIgnoreAttribute()] public bool [$StringProperty]Specified { get { return !String.isNullOrEmpty([$StringProperty]); } set { } }
我也乐于接受任何其他建议。如果只是使用正确的搜索词(显然我不是),那也将不胜感激。
知识的守护者啊,为了促进这一请求,我提供这个 sacrificial goat。
添加说明
只是为了确定,我不是在寻找一键式灵丹妙药。作为一名开发人员,尤其是在底层结构经常因需求而变化的团队中工作的开发人员,我知道要保持现状需要大量工作。
不过,我正在寻找的是合理减少每次我必须对结构进行刷新时的工作量(对于其他人,一个简化的方法来实现同样的事情。)例如,使用*Specified 表示为给定的示例输入大约 165 个以上的字符。对于包含 45 个字符串字段的合同,这意味着每次模型更改时我都必须键入超过 7,425 个字符——这是针对一个服务对象的! 大约有 10-20服务对象待售。
右键单击的想法会将其减少到 45 次右键单击操作...更好。
class 上的自定义属性会更好,因为每次刷新只需执行一次。
理想情况下,app.config 中的运行时设置将是一次性的——不管第一次实施有多难,因为它进入了库。
我认为真正的答案比几乎 7500 characters/class 更好,可能不如简单的 app.config 设置好,但它要么存在,要么我相信它可以实现。
这不是完美的解决方案,但沿着 45 右键单击行,您可以使用 T4 Text Template 在与生成的 Web 服务代码分开的部分 class 声明中生成 XXXSpecified 属性.
然后单击鼠标右键 -> 运行 自定义工具可在更新服务引用时重新生成 XXXSpecified 代码。
这是一个示例模板,它为给定命名空间中 classes 的所有字符串属性生成代码:
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="$(SolutionDir)<Path to assembly containing service objects>" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ output extension=".cs" #>
<#
string serviceObjectNamespace = "<Namespace containing service objects>";
#>
namespace <#= serviceObjectNamespace #> {
<#
foreach (Type type in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes())
.Where(t => t.IsClass && t.Namespace == serviceObjectNamespace)) {
var properties = type.GetProperties().Where(p => p.PropertyType == typeof(string));
if (properties.Count() > 0) {
#>
public partial class <#= type.Name #> {
<#
foreach (PropertyInfo prop in properties) {
#>
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool <#= prop.Name#>Specified
{
get { return <#= prop.Name#> != null; }
set { }
}
<#
}
#>
}
<#
} }
#>
}
以下是向 WCF 客户端添加自定义行为的方法,该行为可用于检查消息并跳过属性。
这是以下组合:
- https://wcfpro.wordpress.com/2011/03/29/iclientmessageinspector/
- http://blogs.msdn.com/b/stcheng/archive/2009/02/21/wcf-how-to-inspect-and-modify-wcf-message-via-custom-messageinspector.aspx
完整代码:
void Main()
{
var endpoint = new Uri("http://somewhere/");
var behaviours = new List<IEndpointBehavior>()
{
new SkipConfiguredPropertiesBehaviour(),
};
var channel = Create<IRemoteService>(endpoint, GetBinding(endpoint), behaviours);
channel.SendData(new Data()
{
SendThis = "This should appear in the HTTP request.",
DoNotSendThis = "This should not appear in the HTTP request.",
});
}
[ServiceContract]
public interface IRemoteService
{
[OperationContract]
int SendData(Data d);
}
public class Data
{
public string SendThis { get; set; }
public string DoNotSendThis { get; set; }
}
public class SkipConfiguredPropertiesBehaviour : IEndpointBehavior
{
public void AddBindingParameters(
ServiceEndpoint endpoint,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(
ServiceEndpoint endpoint,
ClientRuntime clientRuntime)
{
clientRuntime.MessageInspectors.Add(new SkipConfiguredPropertiesInspector());
}
public void ApplyDispatchBehavior(
ServiceEndpoint endpoint,
EndpointDispatcher endpointDispatcher)
{
}
public void Validate(
ServiceEndpoint endpoint)
{
}
}
public class SkipConfiguredPropertiesInspector : IClientMessageInspector
{
public void AfterReceiveReply(
ref Message reply,
object correlationState)
{
Console.WriteLine("Received the following reply: '{0}'", reply.ToString());
}
public object BeforeSendRequest(
ref Message request,
IClientChannel channel)
{
Console.WriteLine("Was going to send the following request: '{0}'", request.ToString());
request = TransformMessage(request);
return null;
}
private Message TransformMessage(Message oldMessage)
{
Message newMessage = null;
MessageBuffer msgbuf = oldMessage.CreateBufferedCopy(int.MaxValue);
XPathNavigator nav = msgbuf.CreateNavigator();
//load the old message into xmldocument
var ms = new MemoryStream();
using(var xw = XmlWriter.Create(ms))
{
nav.WriteSubtree(xw);
xw.Flush();
xw.Close();
}
ms.Position = 0;
XDocument xdoc = XDocument.Load(XmlReader.Create(ms));
//perform transformation
var elementsToRemove = xdoc.Descendants().Where(d => d.Name.LocalName.Equals("DoNotSendThis")).ToArray();
foreach(var e in elementsToRemove)
{
e.Remove();
}
// have a cheeky read...
StreamReader sr = new StreamReader(ms);
Console.WriteLine("We're really going to write out: " + xdoc.ToString());
//create the new message
newMessage = Message.CreateMessage(xdoc.CreateReader(), int.MaxValue, oldMessage.Version);
return newMessage;
}
}
public static T Create<T>(Uri endpoint, Binding binding, List<IEndpointBehavior> behaviors = null)
{
var factory = new ChannelFactory<T>(binding);
if (behaviors != null)
{
behaviors.ForEach(factory.Endpoint.Behaviors.Add);
}
return factory.CreateChannel(new EndpointAddress(endpoint));
}
public static BasicHttpBinding GetBinding(Uri uri)
{
var binding = new BasicHttpBinding()
{
MaxBufferPoolSize = 524288000, // 10MB
MaxReceivedMessageSize = 524288000,
MaxBufferSize = 524288000,
MessageEncoding = WSMessageEncoding.Text,
TransferMode = TransferMode.Buffered,
Security = new BasicHttpSecurity()
{
Mode = uri.Scheme == "http" ? BasicHttpSecurityMode.None : BasicHttpSecurityMode.Transport,
}
};
return binding;
}
这是 LinqPad 脚本的 link:http://share.linqpad.net/kgg8st.linq
如果你 运行 它,输出将是这样的:
Was going to send the following request: '<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IRemoteService/SendData</Action>
</s:Header>
<s:Body>
<SendData xmlns="http://tempuri.org/">
<d xmlns:d4p1="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<d4p1:DoNotSendThis>This should not appear in the HTTP request.</d4p1:DoNotSendThis>
<d4p1:SendThis>This should appear in the HTTP request.</d4p1:SendThis>
</d>
</SendData>
</s:Body>
</s:Envelope>'
We're really going to write out: <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action a:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none" xmlns:a="http://schemas.xmlsoap.org/soap/envelope/">http://tempuri.org/IRemoteService/SendData</Action>
</s:Header>
<s:Body>
<SendData xmlns="http://tempuri.org/">
<d xmlns:a="http://schemas.datacontract.org/2004/07/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:SendThis>This should appear in the HTTP request.</a:SendThis>
</d>
</SendData>
</s:Body>
</s:Envelope>