После долгих размышлений я придумал это решение, которое, я надеюсь, принесет пользу другим тем же запросом.
Прежде всего, вам нужно иметь собственный обработчик вызовов, чтобы включить все дополнительные данные, которые вы хотите для своих журналов. Вы можете обратиться к исходному коду 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