Search code examples
c#soapwsdlnullreferenceexception

C# SOAP service method receives null parameter instead of


As part of my job, I'm to write a SOAP 1.2-based webservice that receives and processes XML requests. During testing with the remote client, however, I ran into a rather baffling and inconsistent problem.

This is the signature of one of the service's callable methods:

[WebMethod]
[ScriptMethod(UseHttpGet = false, ResponseFormat = ResponseFormat.Xml)]
public XmlDocument PostOrders(XmlDocument request)

The problem here is this:

  • When the method is called from a minimalist testing application I wrote, it works properly.
  • When my client's php-based application calls the method, the method is passed a null request and accordingly throws a NullReferenceException down the line when the application tries to write its contents to file. It doesn't matter what the client actually sent, attaching Visual Studio to the IIS process reveals that the method is called with a null request, with what the client actually sent having been lost somewhere on the .NET level.

What I tried:

  • Independent testing. This produces the same issue - but the very same request copypasted into SoapUI is received properly. This is why I stated above that this problem is inconsistent: two out of four applications, each on different computers, reproduce the issue, the other two don't.
  • Apparently, this can happen if the server expects SOAP 1.2 but receives 1.1, and I was able to reproduce it by intentionally posting the request as 1.1 in SoapUI. So I disabled both 1.1 and basic HTTP in web.config (verifying that both disappeared from WSDL, leaving only the 1.2) and had the client explicitly define version 1.2 on their side. No dice.
  • After modifying the service, the client-side WSDL definition of the method abruptly switched from XmlDocument to Linq XElement. Suspecting a server-side deserialization problem due to discovering that WSDL abstracts the expected data types for multiplatform compatibility, I changed the method's parameter from XmlDocument to XElement. No dice.

Contents of web.config:

<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.5.2" />
    <httpRuntime />
    <webServices>
      <protocols>
        <remove name="HttpGet" />
        <remove name="HttpPost" />
        <remove name="HttpSoap"/>     <!-- disables SOAP 1.1 -->
      </protocols>
      <conformanceWarnings>
        <remove name='BasicProfile1_1'/>
      </conformanceWarnings>
    </webServices>
    <globalization uiCulture="en-US" />
    <customErrors mode="Off" />
    <pages controlRenderingCompatibilityVersion="3.5" clientIDMode="AutoID" />
    <httpModules>
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" />
    </httpModules>
  </system.web>
  <system.codedom>
    <compilers>
      <compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35">
          <providerOption name="CompilerVersion" value="v4.0" />
      </compiler>
      <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.VisualBasic.VBCodeProvider, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\&quot;Web\&quot; /optionInfer+" />
    </compilers>
  </system.codedom>
  <system.webServer>
    <modules>
      <remove name="ApplicationInsightsWebTracking" />
      <add name="ApplicationInsightsWebTracking" type="Microsoft.ApplicationInsights.Web.ApplicationInsightsHttpModule, Microsoft.AI.Web" preCondition="managedHandler" />
    </modules>
    <handlers>
      <remove name="ExtensionlessUrlHandler-Integrated-4.0" />
      <remove name="OPTIONSVerbHandler" />
      <remove name="TRACEVerbHandler" />
      <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
    </handlers>
    <validation validateIntegratedModeConfiguration="false" />
  </system.webServer>
  <system.serviceModel>
    <bindings />
    <client />
  </system.serviceModel>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

Considering that I was able to successfully send request from two separate computers in two separate applications without any manual configuration, but not from another two, is this a server-side problem or a client-side misconfiguration?


Solution

  • Found the solution and am posting it just in case someone else runs into this issue.

    It was a server-side error. If you look at the signature of the method I posted above:

    public XmlDocument PostOrders(XmlDocument request)
    

    The problem was the parameter being an XmlDocument. As the payload of a SOAP request is already encased inside an XML, it cannot be deserialized as an XmlDocument due to the payload's root element not being a root element where it's coming from, only as an XmlElement or Linq XElement. Yet even changing the parameter type didn't work - and that's when I looked at the WSDL.

    As it turned out, neither .NET XML types worked because even when I picked a serializable class (XmlElement/XElement) whatever generates the WSDL didn't recognize them and couldn't figure out what kind of data the method is expecting, so the WSDL didn't declare the parameter's data type. With no data type declared in the WSDL, the client-side application didn't know what to send either, so it cut out the payload and sent only the SOAP header. I don't know why php's SOAP classes work like that, but they apparently do.

    I didn't notice it during testing because my testing application is also .NET and thus automatically knows what to send, while SoapUI's requests are raw strings without any serialization before sending so it neither needs nor cares about the data type.

    The solution was to change the parameter type to string and disable basic HTTP. While basic HTTP was enabled, the deserialization always threw an exception when the method was called with a string parameter, hence why I didn't find this solution sooner.

    Bottom line: if you're writing a .NET webservice not exclusively for .NET clients, don't use .NET classes as input parameters regardless of whether they're serializable or not. Use strings. You'll save yourself a lot of headache.