.NET 4.5 XmlSerializer中xsi:nillable属性与字符串值存在问题

3

哦,伟大的StackOverflow之神,请听我一声叹息:

我正在编写.NET 4.5库代码与Oracle SalesCloud服务通信,并且在处理C#对象上值为null字符串的SOAP请求时遇到了问题。

属性的XSD规范如下:

<xsd:element minOccurs="0" name="OneOfTheStringProperties" nillable="true" type="xsd:string" />

在VS2013中使用“添加服务引用...”工具并编写请求更新除OneOfTheStringProperties以外的其他内容时,输出结果如下:
<OneOfTheStringProperties xsi:nil="true></OneOfTheStringProperties>

在服务器端,这会引起两个问题。首先,由于只读属性也是以这种方式指定的,因此服务器拒绝整个请求。其次,这意味着我可能会无意中清除我想要保留的值(除非我每次都发送每个属性...效率低下且编码麻烦)。
我在谷歌上搜索了很久,但并没有找到最好的解决方法,我不想深入编写自定义XmlSerializer(以及所有相关的测试/边缘情况),除非这是最佳选择。
到目前为止,我能找到的最好方法是遵循[Property]Specified模式。因此,对于每个可用的字符串属性,我必须将以下内容添加到Reference.cs中的定义中。
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool OneOfTheStringPropertiesSpecified 
{
    get { return !String.IsNullOrEmpty(OneOfTheStringProperties); }
    set { }
}

这需要打很多字,但它有效,并且SOAP消息的日志跟踪是正确的。

我希望得到以下三种方法之一的建议:

  1. A config switch, specific XmlSerializer override, or some other fix that will suppress the .NET 4.5 XmlSerializer output for null strings

  2. Something like the same secret formula that will put out "proper" XML such as <OneOfTheStringProperties xsi:nil="true" />

  3. A targeted tutorial to create an extension (or an existing VS2013 extension) that will allow me to right-click on a string property and insert the the following pattern:

    [System.Xml.Serialization.XmlIgnoreAttribute()]
    public bool [$StringProperty]Specified 
    {
        get { return !String.isNullOrEmpty([$StringProperty]); }
        set { }
    }
    

我也愿意接受任何其他建议。如果只是需要使用正确的搜索术语(显然我不知道),那将非常感激。

为了实现这个请求,作为知识守护者,我献上这只牺牲羊

为了澄清

我并不是在寻找一键式魔法。作为一名开发人员,尤其是在一个由于需求而经常更改底层结构的团队中工作的人,我知道保持事物运行需要很多工作。

但是,我所寻找的是在每次刷新结构时减少工作量的合理方法(对于其他人来说,是实现相同目标的简化方案)。例如,使用*Specified表示在给定示例中输入大约165个字符。对于具有45个字符串字段的合同,这意味着每次模型更改时我都要输入超过7,425个字符 - 这只是一个服务对象!共有大约10-20个可用的服务对象。

右键单击的想法将把它缩减到45个右键单击操作...更好。

将自定义属性放在类上会更好,因为每次刷新只需要做一次。

理想情况下,app.config中的运行时设置将是一劳永逸的--第一次实现可能有多难,因为它进入了库。

我认为真正的答案比每个类中的近7500个字符要好得多,可能不如一个简单的app.config设置那么好,但这些方案肯定存在或者可以被制作出来。


1
如果您正在使用旧的“添加 Web 引用”功能,则我成功地使用了 SOAP 扩展来任意修改 SOAP 输入和输出 - https://msdn.microsoft.com/zh-cn/library/system.web.services.protocols.soapextension(v=vs.110).aspx - a-h
只是为了明确,您自动生成了一些C#类,代码生成器将[XmlElement(IsNullable=true)]应用于一堆字符串属性,您想以编程方式忽略此属性,而不是手动从生成的代码中删除它? - dbc
@dbc--基本上正确,尽管它不是在程序上“忽略此属性”,而是更改生成的SOAP消息/XML输出的行为。 - angus_thermopylae
WCF中SOAP扩展的等效接口是IDispatchMessageFormatter接口,这篇文章可以完成这项工作:http://blogs.msdn.com/b/endpoint/archive/2011/05/03/wcf-extensibility-message-formatters.aspx - 它包括使用Json.Net格式化WCF消息的示例。 - a-h
你还提到将设置放入配置中,你编写的消息格式化器可以使用流畅的API风格语法来定义忽略的属性,例如message.Ignore(m => m.OneOfTheStringProperties),或者你可以从app.config加载列表。 - a-h
显示剩余4条评论
2个回答

3
这并不是完美的解决方案,但在45右键行附近,您可以使用T4文本模板来生成XXXSpecified属性,在生成的Web服务代码之外的部分类声明中。当服务引用更新时,只需单击右键-运行自定义工具即可重新生成XXXSpecified代码。
以下是一个示例模板,它为给定命名空间内类的所有字符串属性生成代码:
<#@ 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 { }
        }

    <#
        } 
    #>

    }

<#
    } }
#>

}

非常好 - 我今天会尝试实现这个。而且链接非常有帮助 - 这是我可以为行业特定的样板代码做的定制。一旦我有了什么东西,我会立即告诉你。 - angus_thermopylae

3
以下是如何为WCF客户端添加自定义行为,以便于检查消息并跳过属性。
这是以下内容的组合: 完整代码:
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 脚本链接: 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>

这是来自 Fiddler 的输出,证明 HTTP 消息实际上已被修改:http://i.imgur.com/1G6xQRP.png - a-h

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接