无法反序列化来自 Magento 2 SOAP API 的 SOAP 响应 - XML 响应中的命名空间与服务参考 WSDL 不匹配
Cannot de-serialise SOAP responses from Magento 2 SOAP API - mismatch between XML Namespace in response and Service Reference WSDL
简而言之 - 我需要一种方法让单个代码库能够连接到多个 SOAP API,其中每个 API 的 WSDL 本质上是相同的,除了XML 命名空间因站点而异。
长话短说(抱歉有很多这样的):
我的 .NET 4.5 应用程序充当 Magento SOAP API 的客户端(下载订单、上传产品、库存水平等)。
该应用程序使用对现有 Magento WSDL 的服务引用,对于 Magento 1.x 这工作正常 - 应用程序可以连接到任何网站的 Magento API 只需在实例化时传递不同的端点 URL客户。
然后 Magento 2 出现了,我想制作一个可以与之交互的应用程序的新版本。然而,一个重大的挑战出现了。
我首先创建一个对已知 Magento 2 网站 API 的 WSDL 的服务引用(这并不简单,因为在 Magento 2 下,WSDL 仅在请求经过 OAUTH 身份验证时才会公开,但这是另一个故事)。该应用程序在连接到同一网站 API 时运行良好。但是,当任何其他端点 URL 用于实例化客户端时,每个方法调用似乎都会产生一个空响应对象。如果服务引用是从目标网站的 WSDL 重新创建的,那么它就会开始工作。显然我不能这样做并为每个不同的可能目标网站编译一个新版本的应用程序!
我查看了我的参考 WSDL 与另一个参考 WSDL 之间的区别,并使用 Fiddler 跟踪了请求和响应,我发现了一些我认为是问题根本原因的东西。与 Magento 1.x 下不同,Magento 2 WSDL 具有 XML 特定于 WSDL 来自的网站的名称空间。这转化为服务参考 Reference.cs 中 class 属性中的不同命名空间值,例如:
Magento 1.x 属性(注意通用命名空间值):
[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 属性:
[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)]
我的结论是,除非响应中使用的 XML 命名空间与 Reference.cs.[=17 中的 class 属性完全对应,否则无法反序列化 SOAP 响应=]
最初我尝试在运行时更改 class 属性值 using various techniques 但这没有用。
现在我尝试使用 IClientMessageInspector 拦截响应,并将给定的 XML 命名空间替换为我的 Reference.cs 中的命名空间。我的代码在下面,它似乎正确地进行了替换,但响应对象仍然为空!
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;
}
整个响应中只有 1 个 XML 命名空间,就像我说的,我的 AfterReceiveReply 方法似乎正在进行替换,所以我现在真的不知道下一步该怎么做!
示例响应:
<?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>
注意:我有一个类似的问题,我的应用程序的服务请求会收到 500 错误响应,除非请求中的 XML 命名空间(由 Reference.cs 给出)与目标站点匹配.通过使用上述 IClientMessageInspector 的 BeforeSendRequest 方法进行替换,我成功地解决了这个问题。为清楚起见,我已将该代码省略。
我通过更改 AfterReceiveReply 方法让它工作。出于某种原因,使用 XmlDocument 来帮助创建修改后的回复有效。
private const string ReplyXmlNameSpacePattern = @"xmlns:ns1=""(.+)\?services=(.+)""";
public void AfterReceiveReply(ref Message reply, object correlationState)
{
// Read reply XML
var doc = new XmlDocument();
var ms = new MemoryStream();
var writer = XmlWriter.Create(ms);
reply.WriteMessage(writer);
writer.Flush();
ms.Position = 0;
doc.Load(ms);
// Replace XML namespace in SOAP envelope
var replacementXmlNameSpace = @"xmlns:ns1=""http://www.my-reference-address.net/soap/default?services=""";
var newReplyXml = Regex.Replace(doc.OuterXml, ReplyXmlNameSpacePattern, replacementXmlNameSpace, RegexOptions.Compiled);
doc.LoadXml(newReplyXml);
// Write out the modified reply
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);
}
简而言之 - 我需要一种方法让单个代码库能够连接到多个 SOAP API,其中每个 API 的 WSDL 本质上是相同的,除了XML 命名空间因站点而异。
长话短说(抱歉有很多这样的):
我的 .NET 4.5 应用程序充当 Magento SOAP API 的客户端(下载订单、上传产品、库存水平等)。 该应用程序使用对现有 Magento WSDL 的服务引用,对于 Magento 1.x 这工作正常 - 应用程序可以连接到任何网站的 Magento API 只需在实例化时传递不同的端点 URL客户。
然后 Magento 2 出现了,我想制作一个可以与之交互的应用程序的新版本。然而,一个重大的挑战出现了。
我首先创建一个对已知 Magento 2 网站 API 的 WSDL 的服务引用(这并不简单,因为在 Magento 2 下,WSDL 仅在请求经过 OAUTH 身份验证时才会公开,但这是另一个故事)。该应用程序在连接到同一网站 API 时运行良好。但是,当任何其他端点 URL 用于实例化客户端时,每个方法调用似乎都会产生一个空响应对象。如果服务引用是从目标网站的 WSDL 重新创建的,那么它就会开始工作。显然我不能这样做并为每个不同的可能目标网站编译一个新版本的应用程序!
我查看了我的参考 WSDL 与另一个参考 WSDL 之间的区别,并使用 Fiddler 跟踪了请求和响应,我发现了一些我认为是问题根本原因的东西。与 Magento 1.x 下不同,Magento 2 WSDL 具有 XML 特定于 WSDL 来自的网站的名称空间。这转化为服务参考 Reference.cs 中 class 属性中的不同命名空间值,例如:
Magento 1.x 属性(注意通用命名空间值):
[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 属性:
[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)]
我的结论是,除非响应中使用的 XML 命名空间与 Reference.cs.[=17 中的 class 属性完全对应,否则无法反序列化 SOAP 响应=]
最初我尝试在运行时更改 class 属性值 using various techniques 但这没有用。
现在我尝试使用 IClientMessageInspector 拦截响应,并将给定的 XML 命名空间替换为我的 Reference.cs 中的命名空间。我的代码在下面,它似乎正确地进行了替换,但响应对象仍然为空!
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;
}
整个响应中只有 1 个 XML 命名空间,就像我说的,我的 AfterReceiveReply 方法似乎正在进行替换,所以我现在真的不知道下一步该怎么做!
示例响应:
<?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>
注意:我有一个类似的问题,我的应用程序的服务请求会收到 500 错误响应,除非请求中的 XML 命名空间(由 Reference.cs 给出)与目标站点匹配.通过使用上述 IClientMessageInspector 的 BeforeSendRequest 方法进行替换,我成功地解决了这个问题。为清楚起见,我已将该代码省略。
我通过更改 AfterReceiveReply 方法让它工作。出于某种原因,使用 XmlDocument 来帮助创建修改后的回复有效。
private const string ReplyXmlNameSpacePattern = @"xmlns:ns1=""(.+)\?services=(.+)""";
public void AfterReceiveReply(ref Message reply, object correlationState)
{
// Read reply XML
var doc = new XmlDocument();
var ms = new MemoryStream();
var writer = XmlWriter.Create(ms);
reply.WriteMessage(writer);
writer.Flush();
ms.Position = 0;
doc.Load(ms);
// Replace XML namespace in SOAP envelope
var replacementXmlNameSpace = @"xmlns:ns1=""http://www.my-reference-address.net/soap/default?services=""";
var newReplyXml = Regex.Replace(doc.OuterXml, ReplyXmlNameSpacePattern, replacementXmlNameSpace, RegexOptions.Compiled);
doc.LoadXml(newReplyXml);
// Write out the modified reply
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);
}