Как добавить пользовательские данные контекста для регистрации с помощью внедрения политики в WCF? - PullRequest
0 голосов
/ 04 октября 2011

Мы все знаем, что совершенно нормально объединить WCF с PIAB для решения таких сквозных задач, как ведение журнала, проверка, аудит и т. Д. (Посетите страницу http://msdn.microsoft.com/en-us/magazine/cc136759.aspx).

Но стандартный обработчик вызовов журнала bog поддерживает толькоограниченный набор «расширенных свойств» для журналов. Что делать, если есть требования к дополнительной информации, которая будет регистрироваться, например: IP-адрес клиента, идентификатор пользователя и т. д.?

Ответ (будет добавлен как ответ позже из-зана странную политику stackoverflow для пользователей с низким рейтингом):

1 Ответ

0 голосов
/ 05 октября 2011

После долгих размышлений я придумал это решение, которое, я надеюсь, принесет пользу другим тем же запросом.

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

...