So it turned out that I needed to create my own MessageFormatter and attach it as a behavior to client operations.
For someone who needs this, you need 3 files;
First, you create a custom message class that implements Message. Here, in the OnWriteStartEnvelope method, you add / define the namespaces you want in the envelope.
class RoyalMailMessage: Message { private readonly Message message; public RoyalMailMessage(Message message) { this.message = message; } public override MessageHeaders Headers { get { return this.message.Headers; } } public override MessageProperties Properties { get { return this.message.Properties; } } public override MessageVersion Version { get { return this.message.Version; } } protected override void OnWriteStartBody(XmlDictionaryWriter writer) { writer.WriteStartElement("Body", "http://schemas.xmlsoap.org/soap/envelope/"); } protected override void OnWriteBodyContents(XmlDictionaryWriter writer) { this.message.WriteBodyContents(writer); } protected override void OnWriteStartEnvelope(XmlDictionaryWriter writer) { writer.WriteStartElement("s", "Envelope", "http://schemas.xmlsoap.org/soap/envelope/"); writer.WriteAttributeString("xmlns", "v2", null, "http://www.royalmailgroup.com/api/ship/V2"); writer.WriteAttributeString("xmlns", "v1", null, "http://www.royalmailgroup.com/integration/core/V1"); writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance"); writer.WriteAttributeString("xmlns", "xsd", null, "http://www.w3.org/2001/XMLSchema"); } }
Then you create a custom class that implements IClientMessageFormatter. This uses the Message class, which we defined above for outgoing requests made by the client;
public class RoyalMailMessageFormatter: IClientMessageFormatter { private readonly IClientMessageFormatter formatter; public RoyalMailMessageFormatter(IClientMessageFormatter formatter) { this.formatter = formatter; } public object DeserializeReply(Message message, object[] parameters) { return this.formatter.DeserializeReply(message, parameters); } public Message SerializeRequest(MessageVersion messageVersion, object[] parameters) { var message = this.formatter.SerializeRequest(messageVersion, parameters); return new RoyalMailMessage(message); } }
Then we need to create a custom class that implements IOperationBehavior. This is necessary so that we can adapt the custom message formatter to service actions as behavior;
class RoyalMailIEndpointBehavior: IOperationBehavior { public RoyalMailIEndpointBehavior() {} public void ApplyClientBehavior(OperationDescription description, ClientOperation proxy) { IClientMessageFormatter currentFormatter = proxy.Formatter; proxy.Formatter = new RoyalMailMessageFormatter(currentFormatter); } public void AddBindingParameters(OperationDescription operationDescription, BindingParameterCollection bindingParameters) { } public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation) { } public void Validate(OperationDescription operationDescription) { } }
Finally, we need to add the custom IOperation behavior to all WCF generated service operations;
private shippingAPIPortTypeClient GetProxy() { BasicHttpBinding myBinding = new BasicHttpBinding(BasicHttpSecurityMode.Transport); myBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; shippingClient = new shippingAPIPortTypeClient(myBinding, new EndpointAddress(new Uri(shippingClientSandboxEndpoint), EndpointIdentity.CreateDnsIdentity("api.royalmail.com"), new AddressHeaderCollection())); shippingClient.ClientCredentials.ClientCertificate.Certificate = certificate; foreach(OperationDescription od in shippingClient.Endpoint.Contract.Operations) { od.Behaviors.Add(new RoyalMailIEndpointBehavior()); } return shippingClient; }
Now the namespaces should be in the SOAP envelope, and all elements use the correct prefix, which gives us something like:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v2="http://www.royalmailgroup.com/api/ship/V2" xmlns:v1="http://www.royalmailgroup.com/integration/core/V1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <s:Header></s:Header> <s:Body> <v2:createShipmentRequest> <v2:integrationHeader> <v1:dateTime>2015-07-23T20:37:07.937+01:00</v1:dateTime> <v1:version>2</v1:version> <v1:identification> <v1:applicationId>SOME RANDOM ID</v1:applicationId> <v1:transactionId>SOME RANDOM ID</v1:transactionId> </v1:identification> </v2:integrationHeader> </v2:createShipmentRequest> </s:Body> </s:Envelope>