如何在WCF中添加要使用Policy Injection记录的自定义上下文数据?

时间:2011-10-04 11:33:16

标签: wcf enterprise-library policy-injection

我们都知道将WCF与PIAB结合起来解决诸如日志记录,验证,审计等交叉问题(访问http://msdn.microsoft.com/en-us/magazine/cc136759.aspx)是完全可以的。

但是沼泽标准日志调用处理程序仅支持日志的一组有限的“扩展属性”。如果需要记录其他信息,例如:客户端IP地址,用户ID等,该怎么办?

答案(由于stackoverflow对收视率较低的成员的奇怪政策,将在稍后添加作为答案):

1 个答案:

答案 0 :(得分:0)

经过多次挖掘后,我想出了这个解决方案,希望通过相同的查询让其他人受益。

首先,您需要拥有一个自定义调用处理程序,以包含日志所需的所有其他数据。您可以参考entlib源代码并查找LogCallHandler。在GetLogEntry私有方法中添加其他数据:

    private TraceLogEntry GetLogEntry(IMethodInvocation input)
    {
        var logEntry = new CustomLogEntry();
        var formatter = new CategoryFormatter(input.MethodBase);
        foreach (string category in categories)
        {
            logEntry.Categories.Add(formatter.FormatCategory(category));
        }

        //slot = Thread.GetNamedDataSlot("PatientId");
        //logEntry.PatientId = Thread.GetData(slot).ToString();
        //logEntry.PatientId = CallContext.GetData("__PatientId").ToString();
        logEntry.AppName = ApplicationContext.Current["AppName"].ToString();
        logEntry.ClientIp = ApplicationContext.Current["ClientIp"].ToString();
        logEntry.UserId = ApplicationContext.Current["UserId"].ToString();
        logEntry.PatientId = ApplicationContext.Current["PatientId"].ToString();
        logEntry.EventId = eventId;
        logEntry.Priority = priority;
        logEntry.Severity = severity;
        logEntry.Title = LogCallHandlerDefaults.Title;

        if (includeParameters)
        {
            Dictionary<string, object> parameters = new Dictionary<string, object>();
            for (int i = 0; i < input.Arguments.Count; ++i)
            {
                parameters[input.Arguments.GetParameterInfo(i).Name] = input.Arguments[i];
            }

            logEntry.ExtendedProperties = parameters;
        }

        if (includeCallStack)
        {
            logEntry.CallStack = Environment.StackTrace;
        }

        logEntry.TypeName = input.Target.GetType().FullName;
        logEntry.MethodName = input.MethodBase.Name;
        return logEntry;
    }

之后,您必须创建基础结构以将客户端的上下文数据传播到服务器。我有一个CallContext的包装类来存储上下文数据的字典对象:

[Serializable]
public class ApplicationContext : Dictionary<string, object>
{
    private const string CALL_CONTEXT_KEY = "__Context";
    public const string ContextHeaderLocalName = "__Context";
    public const string ContextHeaderNamespace = "urn:tempuri.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);
        }
    }
}

在服务客户端上,此上下文将通过实现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
}

在服务方面,我有一个ICallContextInitializer的自定义实现来从传入消息中检索消息头并将其设置回传出消息:

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
}

这实质上是邮件头有效负载的往返行程。在AfterInvoke方法中,可以在发送回邮件之前修改邮件头。 最后,我创建了一个端点行为来应用MessageInspector和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
}

您还可以通过使用behavior属性修改服务/合同来编写合同行为以实现相同目的。

现在,您可以从服务客户端设置所有上下文数据,如下所示:

using (var channelFactory = new ChannelFactory<ICustomerService>("WSHttpBinding_ICustomerService"))
        {
            var client = channelFactory.CreateChannel();
            ApplicationContext.Current["AppName"] = "Test application";
            ApplicationContext.Current["ClientIp"] = @"1.1.0.1";
            ApplicationContext.Current["UserId"] = "foo";
            ApplicationContext.Current["PatientId"] = "bar123";

            Console.WriteLine("Retreiving Customer 1");
            Customer cust = client.GetCustomer("1");
            Console.WriteLine("Retreived Customer, Name: [" + cust.Name + "]");
        }

这也发布在entlib.codeplex的讨论区:http://entlib.codeplex.com/discussions/266963