Message logging in WCF clients
Having migrated a service client from WSE to WCF one of the things I noticed is that WCF client message logging is certainly very comprehensive but it can be extremely verbose and difficult to just look at the wire packets.
It turns out that WCF client channels can be easily configured to use an existing logging framework. To attach a custom logger we can configure it into the client by adding a behaviourConfiguration into the client endpoint.
<system.serviceModel> <bindings> <customBinding> <binding name="httpsWithCredentialBinding"> <security enableUnsecuredResponse="true" authenticationMode="UserNameOverTransport" includeTimestamp="false" allowInsecureTransport="true"/> <textMessageEncoding messageVersion="Soap11"/> <httpsTransport maxReceivedMessageSize="655360000"/> </binding> </customBinding> </bindings> <client> <endpoint address="https://service" binding="customBinding" bindingConfiguration="httpsWithCredentialBinding" behaviorConfiguration="withMessageLogging" contract="IMyService" name="MyService"/> </client> <behaviors> <endpointBehaviors> <behavior name="withMessageLogging"> <loggingBehavior /> </behavior> </endpointBehaviors> <extensions> <behaviorExtensions> <add name="loggingBehavior" type="LoggingBehaviorExtension, TestWCF" /> </behaviorExtensions> </extensions> </system.serviceModel>
This will attach the LoggingBehaviour to our WCF client. This class derives from the system provided BehaviourExtensionElement it inserts the logger into the channel like this
public class LoggingBehaviorExtension : BehaviorExtensionElement { protected override object CreateBehavior() { return new LoggingBehavior(); } public override Type BehaviorType { get { return typeof(LoggingBehavior); } } } public class LoggingBehavior : IEndpointBehavior { // these are public to enable us to test this class // we use property injection here as we do not create this class public IDebugHelper DebugHelper { get; set; } public IExceptionHandler ExceptionHandler { get; set; } public LoggingBehavior() { // by default we use the real debug helper this.DebugHelper = new DebugHelper(); // by default we use the real exception handler this.ExceptionHandler = new BusinessObjects.Exceptions.ExceptionHandler(); } public void AddBindingParameters( ServiceEndpoint endpoint, BindingParameterCollection bindingParameters) { } public void ApplyClientBehavior( ServiceEndpoint endpoint, ClientRuntime clientRuntime) { // if message logging is disabled then we do not want the overhead of having an inspector if (DebugHelper.MessageLogging) { // for this to work our custom credentials must have been inserted UsernameClientCredentials credentials = endpoint.Behaviors.Find(); ClientLoggingMessageInspector inspector = new ClientLoggingMessageInspector( credentials, this.DebugHelper, this.ExceptionHandler); clientRuntime.MessageInspectors.Add(inspector); } } public void ApplyDispatchBehavior( ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { throw new NotSupportedException("Behavior not supported on the server side!"); } public void Validate(ServiceEndpoint endpoint) { } }
The ClientLoggingMessageInspector is created and attached with everything it needs to work. This is dependency injection without having an IoC container available. The UsernameClientCredentials will only be available if they have been added by the caller, however this is a requirement of the service we are calling.
string username = ConfigurationManager.AppSettings["Username"]; string password = ConfigurationManager.AppSettings["Password"]; UsernameClientCredentials credentials = new UsernameClientCredentials(new UsernameInfo(userName, passWord)); WcfService service = new WcfService("MyService"); service.Endpoint.EndpointBehaviors.Remove(typeof(ClientCredentials)); service.Endpoint.EndpointBehaviors.Add(credentials); ServiceResponse result = service.MethodCall(request);
The actual logging is done in the ClientLoggingMessageInspector
public class ClientLoggingMessageInspector : IClientMessageInspector { private readonly IDebugHelper debugHelper; private readonly IExceptionHandler exceptionHandler; private readonly UsernameClientCredentials credentials; public ClientLoggingMessageInspector( UsernameClientCredentials credentials, IDebugHelper debugHelper, IExceptionHandler exceptionHandler) { this.credentials = credentials; this.debugHelper = debugHelper; this.exceptionHandler = exceptionHandler; } public object BeforeSendRequest(ref Message request, IClientChannel channel) { // careful it is a REF parameter - do not make a mess of the message try { string requestAsString = request.ToString(); string creddentialsDisplay = "NONE"; if (this.credentials != null) { creddentialsDisplay = string.Format( "Username: {0}, Password {1}", this.credentials.UsernameInfo.Username, this.credentials.UsernameInfo.Password); } this.debugHelper.WriteSoapMessage( string.Format( "WCF Request: Credentials: {0}, Message: {1}", creddentialsDisplay, requestAsString ) ); } catch (Exception ex) { this.exceptionHandler.HandleException(ex); // no throw // do not let message logging kill the service // the exception will be logged } return null; } public void AfterReceiveReply(ref Message reply, object correlationState) { // careful it is a REF parameter - do not make a mess of the message try { string replyAsString = reply.ToString(); this.debugHelper.WriteSoapMessage( string.Format( "WCF Reply: Message {0} ", replyAsString ) ); } catch (Exception ex) { this.exceptionHandler.HandleException(ex); // no throw // do not let message logging kill the service // the exception will be logged } } }
The debugHelper is the existing logging framework and the message logging will be stored wherever that framework decides.