我们都知道将 WCF 与 PIAB 结合起来解决诸如日志记录、验证、审计等交叉问题是完全可以的(访问http://msdn.microsoft.com/en-us/magazine/cc136759.aspx)。
但是沼泽标准日志调用处理程序仅支持日志的一组有限的“扩展属性”。如果需要记录其他信息,例如:客户端 IP 地址、用户 ID 等,该怎么办?
答案(由于stackoverflow对低评级成员的奇怪政策,稍后将作为答案添加):
我们都知道将 WCF 与 PIAB 结合起来解决诸如日志记录、验证、审计等交叉问题是完全可以的(访问http://msdn.microsoft.com/en-us/magazine/cc136759.aspx)。
但是沼泽标准日志调用处理程序仅支持日志的一组有限的“扩展属性”。如果需要记录其他信息,例如:客户端 IP 地址、用户 ID 等,该怎么办?
答案(由于stackoverflow对低评级成员的奇怪政策,稍后将作为答案添加):
经过大量挖掘后,我想出了这个解决方案,我希望它可以使其他人受益于相同的查询。
首先,您需要有一个自定义调用处理程序来包含您想要用于日志的所有其他数据。可以参考 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