如何添加要通过 WCF 中的策略注入记录的自定义上下文数据?

发布于 2024-12-07 20:11:00 字数 336 浏览 0 评论 0 原文

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

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

答案(由于 stackoverflow 对低评分会员的奇怪政策,稍后将添加为答案):

We all know it's perfectly ok to marry up WCF with PIAB to address cross cutting concerns like logging, validation, auditing etc (visit http://msdn.microsoft.com/en-us/magazine/cc136759.aspx).

But the bog standard log call handler only support a limited set of "extended properties" for the logs. What if there are requirements for additional information to be logged such as: client Ip address, user id etc?

Answer (will be added as an answer later due to stackoverflow's strange policy on members with low ratings):

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

二手情话 2024-12-14 20:11:00

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

首先,您需要有一个自定义调用处理程序来包含您想要的日志的所有附加数据。您可以参考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
}

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

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

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

After much digging around I came up with this solution which I hope will benefit others with the same query.

First of all, you need to have a custom call handler to include all the additional data you want for your logs. You can refer to the entlib source code and look for LogCallHandler. Add the additional data in GetLogEntry private method:

    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;
    }

After thatn, you have to creat the infrastruture to propagate context data from client to server. 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: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);
        }
    }
}

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.

Now from your service client, you can set all the context data like below:

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 + "]");
        }

This is also posted on the discussion board of entlib.codeplex at: http://entlib.codeplex.com/discussions/266963

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文