We need to integrate with some external parties using SOAP request. We use .Net Core and sending SOAP request is supported out of the box. We have the service contracts generated from the WSDL files provided from the external parties.
The raw request we send looks something like this.
<s:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<SampleRequest xmlns="http://some.sample.namespace.com/service/2.19" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SampleRequestContent xmlns="http://some.sample.namespace.com/schema/2.19/">
// … More content goes here
</SampleRequestContent>
</SampleRequest>
</s:Body>
</s:Envelope>
However, the external parties failed to process the request as they expect it to look like below.
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
<SOAP-ENV:Header/>
<SOAP-ENV:Body>
<ns3:SampleRequest xmlns:ns2="http://some.sample.namespace.com/schema/2.19/" xmlns:ns3="http://some.sample.namespace.com/service/2.19/">
<ns2:SampleRequestContent>
// … More content goes here
</ns2:SampleRequestContent>
</ns3:SampleRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
The second xml has custom suffixes for the namespaces. However, these two xml requests are essentially equivalent. In order to make the xml request to look like the expected one from the external parties, we use a custom Message where we will specify the custom suffix for those namespaces.
public class CustomNamespaceMessage : Message
{
private readonly Message _message;
private readonly IEnumerable<string> _customNamespaces;
private const string EnvelopeNamespace = "http://schemas.xmlsoap.org/soap/envelope/";
public CustomNamespaceMessage(
Message message,
IEnumerable<string> customNamespaces)
{
_message = message;
_customNamespaces = customNamespaces;
}
public override MessageHeaders Headers => _message.Headers;
public override MessageProperties Properties => _message.Properties;
public override MessageVersion Version => _message.Version;
protected override void OnWriteBodyContents(XmlDictionaryWriter writer)
{
if (_customNamespaces != null)
{
foreach (string ns in _customNamespaces)
{
var tokens = ns.Split(new char[] { ':' }, 2);
writer.WriteAttributeString("xmlns", tokens[0], null, tokens[1]);
}
}
this._message.WriteBodyContents(writer);
}
protected override void OnWriteStartBody(XmlDictionaryWriter writer)
{
writer.WriteStartElement("SOAP-ENV", "Body", EnvelopeNamespace);
}
protected override void OnWriteStartEnvelope(XmlDictionaryWriter writer)
{
writer.WriteStartElement("SOAP-ENV", "Envelope", EnvelopeNamespace);
}
}
In OnWriteStartEnvelope, we override the Envelope element with a prefix SOAP-ENV. Then, in OnWriteStartBody, we also override the Body element with the same prefix SOAP-ENV. Finally, in OnWriteBodyContents, if custom namespaces are provided, we override the element attribute with the custom namespaces.
Next, we need to have a custom client message formatter where we will use the CustomNamespaceMessage instead of the normal Message.
public class CustomNamespaceMessageFormatter : IClientMessageFormatter
{
private readonly IClientMessageFormatter _formatter;
private readonly IEnumerable<string> _customNamespaces;
public CustomNamespaceMessageFormatter(
IClientMessageFormatter formatter,
IEnumerable<string> customNamespaces)
{
_formatter = formatter;
_customNamespaces = customNamespaces;
}
public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
{
var message = this._formatter.SerializeRequest(messageVersion, parameters);
return new CustomNamespaceMessage(message, _customNamespaces);
}
public object DeserializeReply(Message message, object[] parameters)
{
return _formatter.DeserializeReply(message, parameters);
}
}
Then we need to have a custom attribute that implements the IOperationBehavior to use our custom client message formatter.
[AttributeUsage(AttributeTargets.Method)]
public class CustomNamespacesAttribute : Attribute, IOperationBehavior
{
public string[] CustomNamespaces { get; set; }
public void AddBindingParameters(OperationDescription operationDescription,
BindingParameterCollection bindingParameters)
{
}
public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
{
IOperationBehavior serializerBehavior = operationDescription.Behaviors.Find<XmlSerializerOperationBehavior>();
if (serializerBehavior == null)
serializerBehavior = operationDescription.Behaviors.Find<DataContractSerializerOperationBehavior>();
if (clientOperation.Formatter == null)
serializerBehavior.ApplyClientBehavior(operationDescription, clientOperation);
var innerClientFormatter = clientOperation.Formatter;
clientOperation.Formatter = new CustomNamespaceMessageFormatter(innerClientFormatter, CustomNamespaces);
}
public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
{
}
public void Validate(OperationDescription operationDescription)
{
}
}
Finally, we need to use this new attribute on the service contract.
[System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Tools.ServiceModel.Svcutil", "2.0.0")]
[System.ServiceModel.ServiceContractAttribute(Namespace = "http://some.sample.namespace.com/service/2.19/")]
public interface SampleServiceContract
{
[System.ServiceModel.OperationContractAttribute()]
[System.ServiceModel.XmlSerializerFormatAttribute(SupportFaults = true)]
[CustomNamespaces(CustomNamespaces = new string[] {
"ns1:http://some.sample.namespace.com/service/2.19/",
"ns2:http://some.sample.namespace.com/schema/2.19/"
})]
System.Threading.Tasks.Task<SampleResponse> SomeAction(SampleRequest request);
}