Инспекторы сообщений

В примере MessageInspectors показано, как реализовать и настроить инспекторы сообщений клиента и службы.

Инспектор сообщений - это объект расширяемости, который можно использовать программно или посредством конфигурации в среде выполнения клиента модели служб и среде выполнения распределения для проверки и изменения сообщений перед их получением или отправкой.

В этом образце реализуется базовый механизм проверки сообщений клиента и службы, который проверяет входящие сообщения на основе набора настраиваемых документов XML-схемы. Обратите внимание, что в этом образце сообщения проверяются не для всех операций. Данное упрощение сделано преднамеренно.

Инспектор сообщений

Инспекторы сообщений клиента реализуют интерфейс IClientMessageInspector, а инспекторы сообщений службы - интерфейс IDispatchMessageInspector. Реализации можно объединить в один класс для создания инспектора сообщений, работающего на обеих сторонах. В данном образце реализуется такой комбинированный инспектор сообщений. Инспектор создается, передавая набор схем, на основе которых проверяются входящие и исходящие сообщения, и позволяет разработчику определить, следует ли проверять входящие или исходящие сообщения, а также использовать ли режим диспетчеризации или режим клиента, который влияет на обработку ошибок, как описано далее в этом разделе.

public class SchemaValidationMessageInspector : IClientMessageInspector, IDispatchMessageInspector
{
    XmlSchemaSet schemaSet;
    bool validateRequest;
    bool validateReply;
    bool isClientSide;
    [ThreadStatic]
    bool isRequest;

    public SchemaValidationMessageInspector(XmlSchemaSet schemaSet,
         bool validateRequest, bool validateReply, bool isClientSide)
    {
        this.schemaSet = schemaSet;
        this.validateReply = validateReply;
        this.validateRequest = validateRequest;
        this.isClientSide = isClientSide;
    }

Любой инспектор сообщений службы (диспетчера) должен реализовывать два метода IDispatchMessageInspector: AfterReceiveRequest и BeforeSendReply(Message, Object).

Метод AfterReceiveRequest вызывается диспетчером при получении сообщения, обработке сообщения стеком каналов и назначении сообщения службе, но перед его десериализацией и отправкой операции. Если входящее сообщение было зашифровано, оно поступает в инспектор сообщений в расшифрованном виде. Метод получает сообщение request, переданное в качестве ссылочного параметра, который позволяет проверить, изменить или заменить сообщение при необходимости. Возвращаемое значение может быть любым объектом и используется как объект состояния корреляции, передаваемый методу BeforeSendReply при возврате службой ответа на текущее сообщение. В этом образце метод AfterReceiveRequest делегирует проверку сообщения закрытому локальному методу ValidateMessageBody и не возвращает объект состояния корреляции. Этот метод гарантирует, что службе не будут передаваться недопустимые сообщения.

object IDispatchMessageInspector.AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
{
    if (validateRequest)
    {
        // inspect the message. If a validation error occurs,
        // the thrown fault exception bubbles up.
        ValidateMessageBody(ref request, true);
    }
    return null;
}

Метод BeforeSendReply(Message, Object) вызывается, когда ответ готов для отправки клиенту или, в случае односторонних сообщений, после обработки входящего сообщения. Это позволяет симметрично вызывать необходимые расширения независимо от шаблона обмена сообщениями. Как и в случае метода AfterReceiveRequest, сообщение передается в качестве ссылочного параметра и может быть проверено, изменено или заменено. Проверка сообщения, выполняемая в этом образце, повторно делегируется методу ValidMessageBody, но в данном случае процедура обработки ошибок проверки немного отличается.

При возникновении ошибки проверки метод ValidateMessageBody вызывает исключения, унаследованные от FaultException. В случае метода AfterReceiveRequest эти исключения могут размещаться в инфраструктуре модели служб, где они автоматически преобразуются в ошибки SOAP и передаются клиенту. В случае метода BeforeSendReply исключения FaultException не следует размещать в инфраструктуре, поскольку преобразование исключений ошибок, вызванных службой, происходит перед вызовом инспектора сообщений. Поэтому следующая реализация перехватывает известное исключение ReplyValidationFault и заменяет ответное сообщение явным сообщением об ошибке. Этот метод гарантирует, что реализацией службы не будут возвращаться недопустимые сообщения.

void IDispatchMessageInspector.BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
    if (validateReply)
    {
        // Inspect the reply, catch a possible validation error
        try
        {
            ValidateMessageBody(ref reply, false);
        }
        catch (ReplyValidationFault fault)
        {
            // if a validation error occurred, the message is replaced
            // with the validation fault.
            reply = Message.CreateMessage(reply.Version,
                    fault.CreateMessageFault(), reply.Headers.Action);
        }
    }

Инспектор сообщений клиента очень похож на инспектор сообщений службы. На основе IClientMessageInspector необходимо реализовать два метода: AfterReceiveReply и BeforeSendRequest.

Метод BeforeSendRequest вызывается при создании сообщения клиентским приложением или модулем форматирования операций. Как и в случае инспекторов сообщений диспетчера, сообщение может быть просто проверено или полностью заменено. В этом образце инспектор выполняет делегирование тому же локальному вспомогательному методу ValidateMessageBody, который используется для инспекторов сообщений диспетчера.

Различие в поведении между проверкой клиента и службы (как указано в конструкторе) заключается в том, что проверка клиента вызывает локальные исключения, которые размещаются в пользовательском коде, поскольку они возникают локально и не по причине сбоя службы. Как правило, инспекторы диспетчера службы выдают сообщения об ошибках, а инспекторы клиента - исключения.

Эта реализация BeforeSendRequest гарантирует, что службе не будут передаваться недопустимые сообщения.

object IClientMessageInspector.BeforeSendRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel)
{
    if (validateRequest)
    {
        ValidateMessageBody(ref request, true);
    }
    return null;
}

Реализация AfterReceiveReply гарантирует, что полученные от службы недопустимые сообщения не будут передаваться пользовательскому коду клиента.

void IClientMessageInspector.AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
{
    if (validateReply)
    {
        ValidateMessageBody(ref reply, false);
    }
}

Ключевым элементом данного инспектора сообщений является метод ValidateMessageBody. Для выполнения работы он обертывает проверяющее средство чтения XmlReader вокруг поддерева содержимого тела переданного сообщения. Средство чтения заполняется набором схем, которые содержит инспектор сообщений, и обратный вызов проверки настраивается на делегат, относящийся к обработчику InspectionValidationHandler, который определен наряду с данным методом. Для выполнения проверки сообщение считывается и буферизуется в поддерживаемом потоком памяти объекте XmlDictionaryWriter. При возникновении ошибки или предупреждения в процессе проверки вызывается метод обратного вызова.

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

void ValidateMessageBody(ref System.ServiceModel.Channels.Message message, bool isRequest)
{
    if (!message.IsFault)
    {
        XmlDictionaryReaderQuotas quotas =
                new XmlDictionaryReaderQuotas();
        XmlReader bodyReader =
            message.GetReaderAtBodyContents().ReadSubtree();
        XmlReaderSettings wrapperSettings =
                              new XmlReaderSettings();
        wrapperSettings.CloseInput = true;
        wrapperSettings.Schemas = schemaSet;
        wrapperSettings.ValidationFlags =
                                XmlSchemaValidationFlags.None;
        wrapperSettings.ValidationType = ValidationType.Schema;
        wrapperSettings.ValidationEventHandler += new
           ValidationEventHandler(InspectionValidationHandler);
        XmlReader wrappedReader = XmlReader.Create(bodyReader,
                                            wrapperSettings);

        // pull body into a memory backed writer to validate
        this.isRequest = isRequest;
        MemoryStream memStream = new MemoryStream();
        XmlDictionaryWriter xdw =
              XmlDictionaryWriter.CreateBinaryWriter(memStream);
        xdw.WriteNode(wrappedReader, false);
        xdw.Flush(); memStream.Position = 0;
        XmlDictionaryReader xdr =
        XmlDictionaryReader.CreateBinaryReader(memStream, quotas);

        // reconstruct the message with the validated body
        Message replacedMessage =
            Message.CreateMessage(message.Version, null, xdr);
        replacedMessage.Headers.CopyHeadersFrom(message.Headers);
        replacedMessage.Properties.CopyProperties(message.Properties);
        message = replacedMessage;
    }
}

Метод InspectionValidationHandler вызывается проверяющим объектом XmlReader при каждом возникновении ошибки или предупреждения проверки схемы. Следующая реализация работает только с ошибками и не учитывает все предупреждения.

При первом рассмотрении может показаться возможной вставка проверяющего объекта XmlReader в сообщение с инспектором сообщений, чтобы проверка происходила по мере обработки сообщения без его буферизации. Однако это означает, что по мере обнаружения недопустимых XML-узлов данный обратный вызов создает исключения проверки в некоторой области инфраструктуры модели служб или пользовательского кода, приводящие к непредсказуемому поведению. Подход на основе буферизации полностью ограждает пользовательский код от недопустимых сообщений.

Как отмечалось ранее, исключения, вызываемые обработчиком на стороне клиента и на стороне службы различаются. На стороне службы исключения наследуются от FaultException, на стороне клиента исключения являются регулярными пользовательскими исключениями.

        void InspectionValidationHandler(object sender, ValidationEventArgs e)
{
    if (e.Severity == XmlSeverityType.Error)
    {
        // We are treating client and service side validation errors
        // differently here. Client side errors cause exceptions
        // and are thrown straight up to the user code. Service side
        // validations cause faults.
        if (isClientSide)
        {
            if (isRequest)
            {
                throw new RequestClientValidationException(e.Message);
            }
            else
            {
                throw new ReplyClientValidationException(e.Message);
            }
        }
        else
        {
            if (isRequest)
            {
                // this fault is caught by the ServiceModel
                // infrastructure and turned into a fault reply.
                throw new RequestValidationFault(e.Message);
             }
             else
             {
                // this fault is caught and turned into a fault message
                // in BeforeSendReply in this class
                throw new ReplyValidationFault(e.Message);
              }
          }
      }
    }

Поведение

Инспекторы сообщений являются расширениями среды выполнения клиента или среды выполнения распределения. Такие расширения настраиваются с помощью поведения. Поведение - это класс, изменяющий поведение среды выполнения модели служб путем изменения конфигурации по умолчанию или добавления в нее расширений (например, инспекторов сообщений).

Следующий класс SchemaValidationBehavior является поведением, которое используется для добавления инспектора сообщений, создаваемого в данном образце, в среду выполнения клиента или среду выполнения распределения. В обоих случаях реализация достаточно проста. В случае ApplyClientBehavior и ApplyDispatchBehavior инспектор сообщений создается и добавляется в коллекцию MessageInspectors соответствующей среды выполнения.

public class SchemaValidationBehavior : IEndpointBehavior
{
    XmlSchemaSet schemaSet;
    bool validateRequest;
    bool validateReply;

    public SchemaValidationBehavior(XmlSchemaSet schemaSet, bool
                           inspectRequest, bool inspectReply)
    {
        this.schemaSet = schemaSet;
        this.validateReply = inspectReply;
        this.validateRequest = inspectRequest;
    }
    #region IEndpointBehavior Members

    public void AddBindingParameters(ServiceEndpoint endpoint,
       System.ServiceModel.Channels.BindingParameterCollection
                                            bindingParameters)
    {
    }

    public void ApplyClientBehavior(ServiceEndpoint endpoint,
            System.ServiceModel.Dispatcher.ClientRuntime clientRuntime)
    {
        SchemaValidationMessageInspector inspector =
           new SchemaValidationMessageInspector(schemaSet,
                      validateRequest, validateReply, true);
            clientRuntime.MessageInspectors.Add(inspector);
    }

    public void ApplyDispatchBehavior(ServiceEndpoint endpoint,
         System.ServiceModel.Dispatcher.EndpointDispatcher
                                          endpointDispatcher)
    {
        SchemaValidationMessageInspector inspector =
           new SchemaValidationMessageInspector(schemaSet,
                        validateRequest, validateReply, false);
   endpointDispatcher.DispatchRuntime.MessageInspectors.Add(inspector);
    }

   public void Validate(ServiceEndpoint endpoint)
   {
   }

    #endregion
}

Примечание.

Данное конкретное поведение не дублируется как атрибут, поэтому его нельзя декларативно добавить в тип контракта типа службы. Это решение было принято намеренно, поскольку коллекцию схем нельзя загрузить в объявление атрибута, а обращение к размещению дополнительной конфигурации (например, к параметрам приложения) в этом атрибуте означает создание элемента конфигурации, не согласованного с остальной конфигурацией модели служб. Поэтому данное поведение можно добавить только императивно посредством кода и посредством расширения конфигурации модели служб.

Добавление инспектора сообщений с помощью конфигурации

Для настройки пользовательского поведения в конечной точке в файле конфигурации приложения модель службы требует, чтобы разработчики создали элемент расширения конфигурации, представленный классом, производным от BehaviorExtensionElement. Это расширение затем необходимо добавить в раздел конфигурации модели служб для расширений, как показано для следующего расширения, рассматриваемого в данном разделе.

<system.serviceModel>
…
   <extensions>
      <behaviorExtensions>
        <add name="schemaValidator" type="Microsoft.ServiceModel.Samples.SchemaValidationBehaviorExtensionElement, MessageInspectors, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
      </behaviorExtensions>
    </extensions>
…
</system.serviceModel>

Расширения можно добавить в файл конфигурации приложения или ASP.NET, что является наиболее распространенным вариантом, либо в файл конфигурации компьютера.

При добавлении расширения в область конфигурации можно добавить поведение в конфигурацию поведения, как показано в следующем коде. Конфигурации поведения являются элементами многократного использования, которые можно по мере необходимости применять к нескольким конечным точкам. Поскольку настраиваемое здесь конкретное поведение реализует интерфейс IEndpointBehavior, оно допустимо только в соответствующем разделе файла конфигурации.

<system.serviceModel>
   <behaviors>
      …
     <endpointBehaviors>
        <behavior name="HelloServiceEndpointBehavior">
          <schemaValidator validateRequest="True" validateReply="True">
            <schemas>
              <add location="messages.xsd" />
            </schemas>
          </schemaValidator>
        </behavior>
      </endpointBehaviors>
      …
    </behaviors>
</system.serviceModel>

Элемент <schemaValidator>, настраивающий инспектор сообщений, поддерживается классом SchemaValidationBehaviorExtensionElement. Класс предоставляет два открытых свойства логического типа: ValidateRequest и ValidateReply. Оба свойства отмечены атрибутом ConfigurationPropertyAttribute. Этот атрибут представляет собой связь между свойствами кода и XML-атрибутами, которые можно увидеть заданными для предыдущего XML-элемента конфигурации. Кроме того, в классе имеется свойство Schemas, дополнительно отмеченное атрибутом ConfigurationCollectionAttribute и принадлежащее к типу SchemaCollection, которое также является частью этого образца, но исключено из данного документа для краткости изложения. Это свойство вместе с коллекцией и классом элемента коллекции SchemaConfigElement поддерживает элемент <schemas> в предыдущем фрагменте конфигурации и позволяет добавить коллекцию схем в набор проверки.

Переопределенный метод CreateBehavior преобразует данные конфигурации в объект поведения при их оценке средой выполнения во время построения клиента или конечной точки.

public class SchemaValidationBehaviorExtensionElement : BehaviorExtensionElement
{
    public SchemaValidationBehaviorExtensionElement()
    {
    }

    public override Type BehaviorType
    {
        get
        {
            return typeof(SchemaValidationBehavior);
        }
    }

    protected override object CreateBehavior()
    {
        XmlSchemaSet schemaSet = new XmlSchemaSet();
        foreach (SchemaConfigElement schemaCfg in this.Schemas)
        {
            Uri baseSchema = new
                Uri(AppDomain.CurrentDomain.BaseDirectory);
            string location = new
                Uri(baseSchema,schemaCfg.Location).ToString();
            XmlSchema schema =
                XmlSchema.Read(new XmlTextReader(location), null);
            schemaSet.Add(schema);
        }
     return new
     SchemaValidationBehavior(schemaSet,ValidateRequest,ValidateReply);
    }

[ConfigurationProperty("validateRequest",DefaultValue=false,IsRequired=false)]
public bool ValidateRequest
{
    get { return (bool)base["validateRequest"]; }
    set { base["validateRequest"] = value; }
}

[ConfigurationProperty("validateReply", DefaultValue = false, IsRequired = false)]
        public bool ValidateReply
        {
            get { return (bool)base["validateReply"]; }
            set { base["validateReply"] = value; }
        }

     //Declare the Schema collection property.
     //Note: the "IsDefaultCollection = false" instructs
     //.NET Framework to build a nested section of
     //the kind <Schema> ...</Schema>.
    [ConfigurationProperty("schemas", IsDefaultCollection = true)]
    [ConfigurationCollection(typeof(SchemasCollection),
        AddItemName = "add",
        ClearItemsName = "clear",
        RemoveItemName = "remove")]
    public SchemasCollection Schemas
    {
        get
        {
            SchemasCollection SchemasCollection =
            (SchemasCollection)base["schemas"];
            return SchemasCollection;
        }
    }
}

Императивное добавление инспекторов сообщений

Помимо использования атрибутов (что не поддерживается в данном образце по указанной ранее причине) и конфигурации, поведения можно довольно легко добавить в среду выполнения клиента и службы с помощью императивного кода. В данном образце это выполняется в клиентском приложении для тестирования инспектора сообщений клиента. Класс GenericClient наследуется от класса ClientBase<TChannel>, который предоставляет конфигурацию конечной точки пользовательскому коду. Перед неявным открытием клиента конфигурацию конечной точки можно изменить, например, путем добавления поведений, как показано в следующем коде. Добавление поведения на стороне службы в значительной мере аналогично показанному в данном разделе способу, относящемуся к клиенту, и должно выполняться перед открытием узла службы.

try
{
    Console.WriteLine("*** Call 'Hello' with generic client, with client behavior");
    GenericClient client = new GenericClient();

    // Configure client programmatically, adding behavior
    XmlSchema schema = XmlSchema.Read(new StreamReader("messages.xsd"),
                                                          null);
    XmlSchemaSet schemaSet = new XmlSchemaSet();
    schemaSet.Add(schema);
    client.Endpoint.Behaviors.Add(new
                SchemaValidationBehavior(schemaSet, true, true));

    Console.WriteLine("--- Sending valid client request:");
    GenericCallValid(client, helloAction);
    Console.WriteLine("--- Sending invalid client request:");
    GenericCallInvalid(client, helloAction);

    client.Close();
}
catch (Exception e)
{
    DumpException(e);
}

Настройка, сборка и выполнение образца

  1. Убедитесь, что вы выполнили процедуру однократной установки для примеров Windows Communication Foundation.

  2. Чтобы создать решение, следуйте инструкциям по созданию примеров Windows Communication Foundation.

  3. Чтобы запустить пример в конфигурации с одним или несколькими компьютерами, следуйте инструкциям в разделе "Примеры Windows Communication Foundation".