ASP.NET ASMX WebService命名空间适配及SOAP转REST问题
需求与背景
需要搭建中间服务,接收第三方SOAP消息并转发至REST微服务。因消息发送端无法修改,选择ASP.NET ASMX WebService处理SOAP请求,但开发中遇到兼容性问题。
第三方发送的SOAP消息结构
<s:Envelope xmlns:a="http://<sender-website-containing-this-schema>/soap/encoding" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="<SOME_STRING_WITH_THIS_FORMAT>"> <s:Body> <ns1:EndpointRelatedFunction> <data></data> <!-- 其他字段 --> </ns1:EndpointRelatedFunction> </s:Body> </s:Envelope>
自研ASMX服务代码
namespace ASMXWebService { /// <summary> /// Summary description for ProxySOAP /// </summary> [WebService(Namespace = "http://tempuri.org/")] [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)] [System.ComponentModel.ToolboxItem(false)] // 若允许通过ASP.NET AJAX调用,取消下方注释 // [System.Web.Script.Services.ScriptService] public class ASMXWebService : WebService { [WebMethod] public string EndpointRelatedFunction(object data) { return "Hello World"; } } }
ASMX服务兼容的SOAP消息示例
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <EndpointRelatedFunction xmlns="http://tempuri.org/"> <data /> </EndpointRelatedFunction> </soap:Body> </soap:Envelope>
报错信息
用Postman发送第三方SOAP消息时返回500错误:
soap:VersionMismatchSystem.Web.Services.Protocols.SoapException: Possible SOAP version mismatch: Envelope namespace <http://<sender-website-containing-this-schema>/soap/encoding> was unexpected. Expecting http://schemas.xmlsoap.org/soap/envelope/. at System.Web.Services.Protocols.SoapServerProtocol.CheckHelperVersion() at System.Web.Services.Protocols.SoapServerProtocol.Initialize() at System.Web.Services.Protocols.ServerProtocolFactory.Create(Type type, HttpContext context, HttpRequest request, HttpResponse response, Boolean& abortProcessing)
疑问点
- 如何在ASMX代码中定义自定义命名空间前缀(如
:a或:ns1)? xmlns:ns1的非URI格式值是什么含义?
后续更新
更新1
当命名空间URI可正常访问时,服务无需修改即可解析XML,此前错误是因网络无法访问该URI导致。当前核心问题变为ns1对应的非URI格式命名空间,已联系发送端维护人员协调。
更新2
发送端将ns1对应的值改为服务原命名空间http://tempuri.org/后仍报错,服务期望消息格式为<EndpointRelatedFunction xmlns="WEB_SERVICE_NAMESPACE_THAT_PREVIOUSLY_WAS_TEMPURI">,而非带前缀的<ns1:EndpointRelatedFunction>。目前尝试通过SoapExtension在反序列化前修改XML以适配服务Schema。
解决方案建议
1. SoapExtension实现XML预处理
通过自定义SoapExtension拦截SOAP请求,修改XML结构,将带命名空间前缀的节点转换为服务期望的默认命名空间格式:
public class SoapNamespaceRewriteExtension : SoapExtension { private Stream oldStream; private Stream newStream; public override object GetInitializer(Type serviceType) => null; public override object GetInitializer(LogicalMethodInfo methodInfo, SoapExtensionAttribute attribute) => null; public override void Initialize(object initializer) { } public override Stream ChainStream(Stream stream) { oldStream = stream; newStream = new MemoryStream(); return newStream; } public override void ProcessMessage(SoapMessage message) { switch (message.Stage) { case SoapMessageStage.BeforeDeserialize: RewriteNamespace(oldStream, newStream); newStream.Position = 0; break; case SoapMessageStage.AfterSerialize: CopyStream(newStream, oldStream); break; } } private void RewriteNamespace(Stream input, Stream output) { using (StreamReader reader = new StreamReader(input)) using (StreamWriter writer = new StreamWriter(output)) { string xml = reader.ReadToEnd(); // 替换带前缀节点为服务期望的默认命名空间格式 xml = xml.Replace("<ns1:EndpointRelatedFunction>", "<EndpointRelatedFunction xmlns=\"http://tempuri.org/\">"); xml = xml.Replace("</ns1:EndpointRelatedFunction>", "</EndpointRelatedFunction>"); writer.Write(xml); } } private void CopyStream(Stream from, Stream to) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = from.Read(buffer, 0, buffer.Length)) > 0) { to.Write(buffer, 0, bytesRead); } } }
在ASMX服务类上添加特性启用该扩展:
[SoapExtensionAttribute(typeof(SoapNamespaceRewriteExtension), Priority = 1)] public class ASMXWebService : WebService { // 原有代码 }
2. 命名空间前缀说明
ASMX服务不直接控制命名空间前缀定义,前缀是XML解析层面的概念,只要节点绑定的命名空间URI与服务定义一致,前缀不影响解析。但第三方消息的命名空间URI必须合法,若涉及Schema验证则需确保URI可访问或服务能离线解析。
3. 是否需要切换到WCF?
若后续遇到更多SOAP兼容性问题(如复杂类型解析、非标准SOAP协议),WCF是更优选择:
- WCF对SOAP协议支持更灵活,可自定义绑定、消息编码器,适配非标准格式;
- 内置强大的消息拦截与转换能力,无需手动编写
SoapExtension; - ASMX已被微软标记为过时技术,WCF的维护性和扩展性更优。
WCF实现核心示例:
通过IDispatchMessageInspector拦截并修改请求消息:
public class SoapNamespaceInspector : IDispatchMessageInspector { public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext) { XmlDocument doc = new XmlDocument(); using (MemoryStream ms = new MemoryStream()) { request.WriteMessage(ms); ms.Position = 0; doc.Load(ms); XmlNamespaceManager nsManager = new XmlNamespaceManager(doc.NameTable); nsManager.AddNamespace("ns1", "http://tempuri.org/"); XmlNodeList nodes = doc.SelectNodes("//ns1:EndpointRelatedFunction", nsManager); foreach (XmlNode node in nodes) { node.Attributes.Append(doc.CreateAttribute("xmlns", "", "http://tempuri.org/")); node.Name = "EndpointRelatedFunction"; } ms.SetLength(0); doc.Save(ms); ms.Position = 0; XmlReader reader = XmlReader.Create(ms); request = Message.CreateMessage(reader, int.MaxValue, request.Version); } return null; } public void BeforeSendReply(ref Message reply, object correlationState) { } } // 注册Inspector到服务行为 public class CustomServiceBehavior : IServiceBehavior { public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers) { foreach (EndpointDispatcher ed in cd.Endpoints) { ed.DispatchRuntime.MessageInspectors.Add(new SoapNamespaceInspector()); } } } public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { } }
内容的提问来源于stack exchange,提问作者hello-there

