The short story is - I need a way for a single code base to be able to connect to multiple SOAP APIs where each API's WSDL is essentially the same except that the XML Namespace varies site by site.
The long story is (sorry there is a lot of this):
My .NET 4.5 application acts as a client to the Magento SOAP API (downloads orders, uploads products, stock levels etc). The application uses a Service Reference to a stock Magento WSDL, and for Magento 1.x this worked fine - the application could connect to any website's Magento API just by passing a different endpoint URL when instantiating the client.
So then Magento 2 came along, and I wanted to make a new version of the application that could interface with it. However a significant challenge arose.
I started by creating a Service Reference to a known Magento 2 website API's WSDL (this was not straightforward as under Magento 2 the WSDL is only exposed if the request is OAUTH authenticated, but that is another story). The application worked fine when connecting to that same website API. However when any other endpoint URL is used to instantiate the client, every method call seems to result in a null response object. If the Service Reference is re-created from the target website's WSDL, then it starts working. Obviously I cannot do this and compile a new version of the application for every different possible target website!
I looked at the difference between my reference WSDL and another, and traced the request and response with Fiddler, and I noticed something that I believe to be the root cause of the problem. Unlike under Magento 1.x, a Magento 2 WSDL has XML Namespaces specific to the website the WSDL came from. This translates to different Namespace values in class attributes in the Reference.cs of the Service Reference, for example:
Magento 1.x attributes (note the generic Namespace value):
[System.Xml.Serialization.XmlTypeAttribute(Namespace="urn:Magento")]
[System.ServiceModel.ServiceContractAttribute(Namespace="urn:Magento", ConfigurationName="MagentoAPI.Mage_Api_Model_Server_Wsi_HandlerPortType")]
[System.ServiceModel.MessageBodyMemberAttribute(Namespace="urn:Magento", Order=0)]
Magento 2 attributes:
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://www.my-magento-site.net/soap/default?services=salesCreditmemoRepositoryV1")]
[System.ServiceModel.ServiceContractAttribute(Namespace="http://www.my-magento-site.net/soap/default?services=salesCreditmemoRepositoryV1", ConfigurationName="MagentoV2SoapApiV1.SalesCreditmemoRepositoryV1.salesCreditmemoRepositoryV1PortType")]
[System.ServiceModel.MessageBodyMemberAttribute(Namespace="http://www.my-magento-site.net/soap/default?services=salesCreditmemoRepositoryV1", Order=0)]
My conclusion is that the SOAP response cannot be deserialised unless the XML Namespace used within the response exactly corresponds to that in the class attributes in the Reference.cs.
Initially I tried altering the class attribute values at runtime using various techniques but this didn't work.
Now I am trying to intercept the response using an IClientMessageInspector, and replacing the given XML Namespace with the one in my Reference.cs. My code is below and it seems to correctly make the replacement, but STILL the response object is null!
public class CustomInspectorBehavior : IEndpointBehavior
{
private readonly CustomMessageInspector _clientMessageInspector = new CustomMessageInspector();
public string LastRequestXml { get { return _clientMessageInspector.LastRequestXml; } }
public string LastResponseXml { get { return _clientMessageInspector.LastRequestXml; } }
public void AddBindingParameters(ServiceEndpoint endpoint, System.ServiceModel.Channels.BindingParameterCollection bindingParameters) {}
public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) {}
public void Validate(ServiceEndpoint endpoint) {}
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { clientRuntime.MessageInspectors.Add(_clientMessageInspector); }
}
public class CustomMessageInspector : IClientMessageInspector
{
public string LastRequestXml { get; private set; }
public string LastResponseXml { get; private set; }
public void AfterReceiveReply(ref Message reply, object correlationState)
{
LastResponseXml = reply.ToString();
var doc = new XmlDocument();
var ms = new MemoryStream();
var writer = XmlWriter.Create(ms);
reply.WriteMessage(writer);
writer.Flush();
ms.Position = 0;
// Do namespace substitution
doc.Load(ms);
doc.DocumentElement.SetAttribute("xmlns:ns1", "http://www.my-reference-address.net/soap/default?services=salesCreditmemoRepositoryV1");
ms.SetLength(0);
writer = XmlWriter.Create(ms);
doc.WriteTo(writer);
writer.Flush();
ms.Position = 0;
var reader = XmlReader.Create(ms);
reply = Message.CreateMessage(reader, int.MaxValue, reply.Version);
}
public object BeforeSendRequest(ref Message request, System.ServiceModel.IClientChannel channel) { LastRequestXml = request.ToString(); }
}
public static salesCreditmemoRepositoryV1PortTypeClient GetCreditMemosServiceClient(string apiAddress)
{
const string serviceName = "salesCreditmemoRepositoryV1";
var apiClient = new salesCreditmemoRepositoryV1PortTypeClient(GetSoap12Binding(), new EndpointAddress(apiAddress));
var requestInterceptor = new CustomInspectorBehavior();
apiClient.Endpoint.Behaviors.Add(requestInterceptor);
return apiClient;
}
There is only 1 XML Namespace in the entire response, and like I said, my AfterReceiveReply method seems to be making the substitution, so I am now REALLY STUCK for what to do next!
Example response:
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="http://www.my-magento-site.net/soap/default?services=salesCreditmemoRepositoryV1">
<env:Body>
<ns1:salesCreditmemoRepositoryV1GetListResponse>
<result>
<items/>
<searchCriteria>
<filterGroups>
<item>
<filters>
</filters>
</item>
</filterGroups>
</searchCriteria>
<totalCount>0</totalCount>
</result>
</ns1:salesCreditmemoRepositoryV1GetListResponse>
</env:Body>
</env:Envelope>
Note: I had a similar issue where my application's service request would get a 500 error response unless the XML Namespace in the request (which is given by the Reference.cs) matched the target site. I got around this successfully by doing a substitution using the BeforeSendRequest method of the above IClientMessageInspector. I have left that code out for clarity.