How to add custom properties to be logged with Policy Injection?

Topics: Logging Application Block, Policy Injection Application Block
Jul 28, 2011 at 8:40 PM

Hi there,

I'm using Policy Injection App Block to do automatic logging in my application.  I'm able to successfully log info before and after method execution.  However, I would like for it to log additional custom data that is not part of the method input params.  Is there a way for me to specify the data I want logged that will be added to the Extended Properties dictionary object and ultimately displayed in the log?

Lets say i have a method ProcessOrder(string OrderID) which is being intercepted and logged.  Lets say I have an available variable LocalStoreID that I would also want to be logged when ProcessOrder is called how would I do that?

Thanks in advance.

Jul 29, 2011 at 6:22 AM

Hi,

What is the scope of this LocalStoredID variable? Where is this variable defined? You can extend the existing LoggingCallHandler to achieve this. You can refer to the Enterprise Library Source Code on how the call handler is implemented. There are methods there named LogPreCall and LogPostCall which writes the log message using LogEntry. There you can add your additional data.

 

Noel Angelo Bolasoc
Global Technologies and Solutions
Avanade, Inc.
Contact Us

 

Jul 29, 2011 at 7:23 PM

Thanks Noel,

I'll check it out.

Sep 29, 2011 at 12:10 PM
creativeone wrote:

Thanks Noel,

I'll check it out.

Hi,

I come across the issue as you had. Just wondering if you've had any luck in resolving this? If so would be great if you can share some sample code? Thanks a lot.

Sep 30, 2011 at 4:10 AM

Hi,

As what I've suggested from the previous posts, you can create a custom call handler by extending the LoggingCallHandler. Just refer to the Entlib Source Code to get you started. Let us know if you have further questions.

 

Noel Angelo Bolasoc
Avanade Software
Avanade, Inc.
Contact Us

Sep 30, 2011 at 2:22 PM

Hi Noel

Thanks very much for your reply. 

I'm trying to apply PIAB on my WCF service layer. In a nutshell, my question would be how to pass some context information (shipped from the service client via message inspector) to my custom call handlers. Reason I'm asking this is because the ICallHandler interface does not seem to allow additional information to be passed in:

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext);

All you get is the IMethodInvocation which initially doesn't populate the InvocationContext property. I thought this particular property would be ideally to carry some additional information from the WCF service context but I could be mistaken.

So what I have came up with is below, although works on a simple proof of concept project I'm not convinced it will perform well in a production environment as I'm not sure if this is the right approach/best practice:

[ConfigurationElementType(typeof(MyCallHandlerData))]
    public class MyCallHandler : ICallHandler
    {
        #region Implementation of ICallHandler
        public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
        {
            if (getNext == null) throw new ArgumentNullException("getNext");
            var slot = Thread.GetNamedDataSlot("foobar");
            var data = Thread.GetData(slot);

            Console.WriteLine("Custom context data: {0}", data);
            var result = getNext()(input, getNext);
            Console.WriteLine("Exiting");
            return result;
        }
        public int Order { get; set; }
        #endregion
    }

And in my service I would add the context information to the TLS like below:

public class CustomerService : ICustomerService
    {
        public CustomerService()
        {
            var slot = Thread.GetNamedDataSlot("foobar");
            Thread.SetData(slot, "Test audit entry!");
        }
        #region ICustomerService Members
        public Customer GetCustomer(string customerID)
        {
            return CustomerDAO.GetCustomer(customerID);
        }
        #endregion
    }

Please let me know if this echoes what you suggested. If not please advise. It'd be really great if you could provide some sample code as to how to pass context data from WCF service to the PIAB custom handlers. What I'd like to achieve is a clean SoC so that on my service method implementation, there's no pollution of PIAB's footprints. Everything is bootstraped by the configuration file. 

Thanks very much in advance.

Oct 4, 2011 at 3:13 AM

Hi wheeljack,

You can actually passed argument data coming from the Method attached to your Custom Call Handler. Parameter argument passed values in your method is accessible in the Call Handler specifically thru the method "public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)" looking up into the IMethodInvocation Arguments property (i.e. input.Arguments) you'll find all the parameter arguments values of your attached method available here.

Is this the same thing you're looking for? HTH.

Gino Terrado
Avanade Software
Avanade, Inc.
Contact Us

Oct 4, 2011 at 11:26 AM

Hi Gino

Thanks for your reply.

But what you suggested isn't quite what I'm looking for. The reason for that is I don't want to pollute my service methods with those audit data. It's a violation of single responsibility principle and also SoC. Imagine a change of audit requirement to add/remove data items, then you end up with a very unstable service which keeps changing all the time.

I think I've got the answer to the problem. I have a wrapper class for CallContext to store a dictionary object for context data:

 

    [Serializable]
    public class ApplicationContext : Dictionary<string, object>
    {
        private const string CALL_CONTEXT_KEY = "__Context";
        public const string ContextHeaderLocalName = "__Context";
        public const string ContextHeaderNamespace = "urn:w3c.org";

        private static void EnsureSerializable(object value)
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }
            if (!value.GetType().IsSerializable)
            {
                throw new ArgumentException(string.Format("The argument of the type \"{0}\" is not serializable!", value.GetType().FullName));
            }
        }

        public new object this[string key]
        {
            get { return base[key]; }
            set
            { EnsureSerializable(value); base[key] = value; }
        }

        public int Counter
        {
            get { return (int)this["__Count"]; }
            set { this["__Count"] = value; }
        }

        public static ApplicationContext Current
        {
            get
            {
                if (CallContext.GetData(CALL_CONTEXT_KEY) == null)
                {
                    CallContext.SetData(CALL_CONTEXT_KEY, new ApplicationContext());
                }

                return CallContext.GetData(CALL_CONTEXT_KEY) as ApplicationContext;
            }
            set
            {
                CallContext.SetData(CALL_CONTEXT_KEY, value);
            }
        }
    }

 

On the service client, this context will be added to the request message header through implementing IClientMessageInspector.

public class ClientAuditInfoInspector : IClientMessageInspector
    {
        #region Implementation of IClientMessageInspector

        public object BeforeSendRequest(ref Message request, IClientChannel channel)
        {
            var contextHeader = new MessageHeader<ApplicationContext>(ApplicationContext.Current);
            request.Headers.Add(contextHeader.GetUntypedHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace));
            return null;
        }

        public void AfterReceiveReply(ref Message reply, object correlationState)
        {
            if (reply.Headers.FindHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace) < 0) { return; }
            var context = reply.Headers.GetHeader<ApplicationContext>(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace);
            if (context == null) { return; }
            ApplicationContext.Current = context;
        }

        #endregion
    }

On the service side, I have a custom implementation of ICallContextInitializer to retrieve the message header from the incoming message and set it back to the outgoing message:

public class AuditInfoCallContextInitializer : ICallContextInitializer
    {
        #region Implementation of ICallContextInitializer
        /// <summary>
        /// Extract context data from message header through local name and namespace,
        /// set the data to ApplicationContext.Current.
        /// </summary>
        /// <param name="instanceContext"></param>
        /// <param name="channel"></param>
        /// <param name="message"></param>
        /// <returns></returns>
        public object BeforeInvoke(InstanceContext instanceContext, IClientChannel channel, Message message)
        {
            var context = message.Headers.GetHeader<ApplicationContext>(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace);
            if (context == null) { return null; }

            ApplicationContext.Current = context;
            return ApplicationContext.Current;

        }

        /// <summary>
        /// Retrieve context from correlationState and store it back to reply message header for client.
        /// </summary>
        /// <param name="correlationState"></param>
        public void AfterInvoke(object correlationState)
        {
            var context = correlationState as ApplicationContext;
            if (context == null)
            {
                return;
            }
            var contextHeader = new MessageHeader<ApplicationContext>(context);
            OperationContext.Current.OutgoingMessageHeaders.Add(contextHeader.GetUntypedHeader(ApplicationContext.ContextHeaderLocalName, ApplicationContext.ContextHeaderNamespace));
            ApplicationContext.Current = null;

        }

        #endregion
    }

This essentially is a round trip for the message header payload to travel. In the AfterInvoke method, the message header could be modified before being sent back.
Finally, I have created an endpoint behaviour to apply the MessageInspector and CallContextInitializer.
public class AuditInfoContextPropagationEndpointBehavior : BehaviorExtensionElement, IEndpointBehavior
    {
        #region Overrides of BehaviorExtensionElement

        protected override object CreateBehavior()
        {
            return new AuditInfoContextPropagationEndpointBehavior();
        }

        public override Type BehaviorType
        {
            get { return typeof(AuditInfoContextPropagationEndpointBehavior); }
        }

        #endregion

        #region Implementation of IEndpointBehavior

        public void Validate(ServiceEndpoint endpoint)
        {
            return;
        }

        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            return;
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            foreach (var operation in endpointDispatcher.DispatchRuntime.Operations)
            {
                operation.CallContextInitializers.Add(new AuditInfoCallContextInitializer());
            }

        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
            clientRuntime.MessageInspectors.Add(new ClientAuditInfoInspector());
        }

        #endregion
    }
You could also write a contract behaviour to achieve the same by decorating your service/contract with the behaviour attribute.
I hope this will help someone with the same problem.

Oct 4, 2011 at 12:35 PM

I have posted a question on stackoverflow to summarize my solution. Please visit How to add custom context data to be logged with Policy Injection in WCF?

Oct 5, 2011 at 7:43 AM

Thanks for sharing this wheeljack! :)

 

Noel Angelo Bolasoc
Avanade Software and Cloud Services
Avanade, Inc.
Contact