Background: I'm maintaining an integration platform that pulls data from various unreliable APIs. Some of those actions generate potentially high costs, so for diagnostic purposes, every outgoing and incoming message is logged to disk to a separate file. For REST-like APIs, I use a simple wrapper on the network stream that also saves data to a file. For .NET Classic SOAP client, I have a helper that dynamically wraps SoapHttpClientProtocol
to use the same network stream logging mechanism.
With .NET Standard 2.0 and .NET Core, the only supported way to write SOAP clients is WCF. How do I programmatically configure WCF SOAP client to log the HTTP incoming/outgoing streams to separate files, preferrably with configurable names?
My current sample client code:
public abstract class ServiceCommunicatorBase<T>
where T : IClientChannel
{
private const int Timeout = 20000;
private static readonly ChannelFactory<T> ChannelFactory = new ChannelFactory<T>(
new BasicHttpBinding(),
new EndpointAddress(new Uri("http://target/endpoint")));
protected T1 ExecuteWithTimeoutBudget<T1>(
Func<T, Task<T1>> serviceCall,
[CallerMemberName] string callerName = "")
{
// TODO: fixme, setup logging
Console.WriteLine(callerName);
using (var service = this.CreateService(Timeout))
{
// this uses 2 threads and is less than ideal, but legacy app can't handle async yet
return Task.Run(() => serviceCall(service)).GetAwaiter().GetResult();
}
}
private T CreateService(int timeout)
{
var clientChannel = ChannelFactory.CreateChannel();
clientChannel.OperationTimeout = TimeSpan.FromMilliseconds(timeout);
return clientChannel;
}
}
public class ConcreteCommunicator
: ServiceCommunicatorBase<IWCFRemoteInterface>
{
public Response SomeRemoteAction(Request request)
{
return this.ExecuteWithTimeoutBudget(
s => s.SomeRemoteAction(request));
}
}
I've managed to log messages using IClientMessageInspector
attached via a configurable behaviour. There's some documentation for Message Inspectors on MSDN but it's still vague (and somewhat out of date, netstandard-2.0
API surface is slightly different.
I had to move from using the ChannelFactory<T>
to using the actual generated proxy class. Working code (cut for brevity):
var clientChannel = new GeneratedProxyClient(
new BasicHttpBinding { SendTimeout = TimeSpan.FromMilliseconds(timeout) },
new EndpointAddress(new Uri("http://actual-service-address"));
clientChannel.Endpoint.EndpointBehaviors.Add(new LoggingBehaviour());
Service classes:
// needed to bind the inspector to the client channel
// other methods are empty
internal class LoggingBehaviour : IEndpointBehavior
{
public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
{
clientRuntime.ClientMessageInspectors.Add(new LoggingClientMessageInspector());
}
}
internal class LoggingClientMessageInspector : IClientMessageInspector
{
public object BeforeSendRequest(ref Message request, IClientChannel channel)
{
var correlationId = Guid.NewGuid();
this.SaveLog(ref request, correlationId, "RQ");
return correlationId;
}
public void AfterReceiveReply(ref Message reply, object correlationState)
{
var correlationId = (Guid)correlationState;
this.SaveLog(ref reply, correlationId, "RS");
}
private void SaveLog(ref Message request, Guid correlationId, string suffix)
{
var outputPath = GetSavePath(suffix, correlationId, someOtherData);
using (var buffer = request.CreateBufferedCopy(int.MaxValue))
{
var directoryName = Path.GetDirectoryName(outputPath);
if (directoryName != null)
{
Directory.CreateDirectory(directoryName);
}
using (var stream = File.OpenWrite(outputPath))
{
using (var message = buffer.CreateMessage())
{
using (var writer = XmlWriter.Create(stream))
{
message.WriteMessage(writer);
}
}
}
request = buffer.CreateMessage();
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With