5

Leaving a SOAP field element empty results in a cast error for native types. (sadly cannot use xsi:nil="true" due to client constraints)

Marking the WCF contract native type as nullable<> does not appear to be enough to stop the following error being returned to the client.

The string '' is not a valid Boolean value. at System.Xml.XmlConvert.ToBoolean(String s) at System.Xml.XmlConverter.ToBoolean(String value) System.FormatException

does anyone know the best method of instructing the DataContractSerializer to convert empty elements to be deserialized to null?

My example WCF service contract;

[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; }
}

How can we get WCF to accept the following submission;

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

The client is only able to change the value it inserts <ster:TestBool>{here}</ster:TestBool> so true false or nothing are the only options.

Microsoft Developer
  • 1,919
  • 1
  • 20
  • 27
  • It looks like they are sending an empty value instead of `nil` – ta.speot.is Oct 29 '13 at 11:24
  • 1
    Indeed, I have updated my question with some constraints. – Microsoft Developer Oct 29 '13 at 11:30
  • You could change the property to a string? – ta.speot.is Oct 29 '13 at 19:50
  • 1
    That pollutes the data contract effectively rendering the point of .net types.. pointless. This is a message formatting concern, where I am having to deal with a limitation of a client and what that client can muster in terms of the XML it can send me. I have been looking at implementing a custom IOperationBehavior which allocates a custom IDispatchMessageFormatter to intercept the parameters which have a nullstring value. I wanted to have the IOperationBehavior be an Attribute I can decorate each of my relevant OperationContract declarations in the ServiceContract. I will report back soon. – Microsoft Developer Oct 29 '13 at 20:38
  • *if WCF expects us to do this* WCF expects XML that matches the XSD that describes the service contract. An empty element is not the same as `nil`. http://stackoverflow.com/a/774234/242520 – ta.speot.is Oct 29 '13 at 20:40

1 Answers1

4

Ok I believe I have cracked this by using an Operation Behavior to modify the underlying message before its formatted via IDispatchMessageFormatter.

The following code provides a solution against a service that is based on WCF file-less activation.

I wanted to have my IOperationBehavior live in the form of a Attribute class. Then I could simply decorate each Service Operation with my new attribute which would instigate the IOperationBehavior for that Operation - very nice and simple for the end user.

The key problem is where you apply the behavior, this is critical. The order of the operation behaviors that are called by WCF when applying the behavior via an attribute are different to when applying at the service host. The attribute based order is as follows:

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

For some reason an operation behavior (only when applied via the use of an attribute) will be called before the DataContractSerializerOperationBehavior. This is a problem because in my behavior I want to delegate deserialization to the DataContractSerializerOperationBehavior Formatter (passed into my behavior as the inner formatter) within my formatter, after I have adjusted the message (see code). I don't want to have to re-write a deserialization routine when Microsoft provided a perfectly good deserializer already. I merely correct the XML in the first instance so that blanks are converted to nulls which are correctly represented within the XML so that the DataContractSerializer can tie them up to nullable types in the service interface.

So this means we cannot use attribute-based behaviors as they were intended since WCF may well be broken in a rather subtle way here since I can see no reason for this phenomenon. So we can still add an IOperationBehavior to an operation, we just have to manually assign it at the service host creation stage, because then our IOperationBehavior is inserted into the 'correct' sequence, that is, after the DataContractSerializerOperationBehavior has been created, only then can I get a reference to the inner formatter.

 // 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());
                }
            }
        }
    }
}

Perhaps since I am only modifying the incoming message, I could instead use IDispatchMessageInspector which will remove the dependency on the IDispatchMessageFormatter activation order. But this works for now ;)

Usage:

  1. Add to your operation
 [ServiceContract(Namespace = Namespaces.MyNamespace)]
 public interface IMyServiceContrct
 {
       [OperationContract]
       [NullifyEmptyElements]
       void MyDoSomthingMethod(string someIneteger);
 }
  1. Tie into your service

A. if you have .svc simply reference MyServiceHost

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

B. if your using file-less activation services, add this to your web.config file

   <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>
Microsoft Developer
  • 1,919
  • 1
  • 20
  • 27
  • That's a weird problem you've run into with the order dependency. As far as removing the string literal goes, though - you could apply a custom attribute to all the methods which need the behaviour, but which is used only as a marker, not implementing `IOperationBehavior`. Then your existing code can loop over all operations, retrieving the [`SyncMethod`/`BeginMethod`/`TaskMethod`](http://msdn.microsoft.com/en-us/library/System.ServiceModel.Description.OperationDescription_properties%28v=vs.110%29.aspx) and testing for the existence of the attribute to decide whether the behaviour is applied. – anton.burger Oct 30 '13 at 12:29
  • Wow, thanks for this well-written answer. I got my behaviour running now using the way you propose. Here is some MSDN doc I have found on this, effectively suggesting the same as proposed here: http://blogs.msdn.com/b/carlosfigueira/archive/2011/05/03/wcf-extensibility-message-formatters.aspx – Marcel Apr 16 '15 at 14:56
  • WCF is very good at extensibility. Microsoft look after us dev's quite well. – Microsoft Developer Apr 21 '15 at 17:46