Inspetores de mensagens

O exemplo MessageInspectors demonstra como implementar e configurar inspetores de mensagens de cliente e serviço.

Um inspetor de mensagens é um objeto de extensibilidade que pode ser usado no tempo de execução do cliente e no tempo de execução de despacho do modelo de serviço programaticamente ou por meio da configuração e que pode inspecionar e alterar mensagens depois que elas são recebidas ou antes de serem enviadas.

Este exemplo implementa um mecanismo básico de validação de mensagens de cliente e serviço que valida mensagens de entrada em relação a um conjunto de documentos configuráveis do esquema XML. Observe que este exemplo não valida mensagens para cada operação. Trata-se de uma simplificação intencional.

Inspetor de Mensagens

Os inspetores de mensagens do cliente implementam a interface e os IClientMessageInspector inspetores de mensagens de serviço implementam a IDispatchMessageInspector interface. As implementações podem ser combinadas em uma única classe para formar um inspetor de mensagens que funcione para ambos os lados. Este exemplo implementa esse inspetor de mensagens combinado. O inspetor é construído passando em um conjunto de esquemas em relação aos quais as mensagens de entrada e saída são validadas e permite que o desenvolvedor especifique se as mensagens de entrada ou de saída são validadas e se o inspetor está no modo de despacho ou cliente, o que afeta o tratamento de erros, conforme discutido mais adiante neste tópico.

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

Qualquer inspetor de mensagens de serviço (despachante) deve implementar os dois IDispatchMessageInspector métodos AfterReceiveRequest e BeforeSendReply(Message, Object).

AfterReceiveRequest é invocado pelo dispatcher quando uma mensagem é recebida, processada pela pilha de canais e atribuída a um serviço, mas antes de ser desserializada e enviada para uma operação. Se a mensagem recebida foi encriptada, a mensagem já está desencriptada quando chega ao inspetor de mensagens. O método obtém a request mensagem passada como um parâmetro de referência, o que permite que a mensagem seja inspecionada, manipulada ou substituída conforme necessário. O valor de retorno pode ser qualquer objeto e é usado como um objeto de estado de correlação que é passado para BeforeSendReply quando o serviço retorna uma resposta à mensagem atual. Neste exemplo, AfterReceiveRequest delega a inspeção (validação) da mensagem ao método ValidateMessageBody privado e local e não retorna nenhum objeto de estado de correlação. Esse método garante que nenhuma mensagem inválida passe para o serviço.

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) é invocado sempre que uma resposta está pronta para ser enviada de volta a um cliente ou, no caso de mensagens unidirecionais, quando a mensagem recebida foi processada. Isso permite que as extensões contem com a possibilidade de serem chamadas simetricamente, independentemente do eurodeputado. Assim como no AfterReceiveRequest, a mensagem é passada como um parâmetro de referência e pode ser inspecionada, modificada ou substituída. A validação da mensagem executada neste exemplo é novamente delegada ValidMessageBody ao método, mas o tratamento de erros de validação é ligeiramente diferente neste caso.

Se ocorrer um erro de validação no serviço, o método lançará ValidateMessageBodyFaultExceptionexceções derivadas de -. Na AfterReceiveRequest, essas exceções podem ser colocadas na infraestrutura do modelo de serviço, onde são automaticamente transformadas em falhas SOAP e retransmitidas ao cliente. No BeforeSendReply, FaultException as exceções não devem ser colocadas na infraestrutura, porque a transformação de exceções de falha lançadas pelo serviço ocorre antes que o inspetor de mensagens seja chamado. Portanto, a implementação a seguir captura a exceção conhecida ReplyValidationFault e substitui a mensagem de resposta por uma mensagem de falha explícita. Esse método garante que nenhuma mensagem inválida seja retornada pela implementação do serviço.

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

O inspetor de mensagens do cliente é muito semelhante. Os dois métodos que devem ser implementados a partir de IClientMessageInspector são AfterReceiveReply e BeforeSendRequest.

BeforeSendRequest é invocado quando a mensagem foi composta pelo aplicativo cliente ou pelo formatador de operação. Tal como acontece com os inspetores de mensagens do despachante, a mensagem pode apenas ser inspecionada ou totalmente substituída. Neste exemplo, o inspetor delega ao mesmo método auxiliar local ValidateMessageBody que também é usado para os inspetores de mensagens de envio.

A diferença comportamental entre a validação do cliente e do serviço (conforme especificado no construtor) é que a validação do cliente lança exceções locais que são colocadas no código do usuário porque ocorrem localmente e não devido a uma falha de serviço. Geralmente, a regra é que os inspetores de despachante de serviço lancem falhas e que os inspetores de cliente lancem exceções.

Essa BeforeSendRequest implementação garante que nenhuma mensagem inválida seja enviada para o serviço.

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

A AfterReceiveReply implementação garante que nenhuma mensagem inválida recebida do serviço seja retransmitida para o código de usuário do cliente.

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

O coração deste inspetor de mensagens em particular é o ValidateMessageBody método. Para executar seu trabalho, ele envolve uma validação XmlReader em torno da subárvore de conteúdo do corpo da mensagem passada. O leitor é preenchido com o conjunto de esquemas que o inspetor de mensagens mantém e o retorno de chamada de validação é definido como um delegado referente ao InspectionValidationHandler que é definido ao lado desse método. Para executar a validação, a mensagem é lida e inserida em um fluxo de memória .XmlDictionaryWriter Se ocorrer um erro de validação ou aviso no processo, o método de retorno de chamada será invocado.

Se nenhum erro ocorrer, uma nova mensagem será construída copiando as propriedades e cabeçalhos da mensagem original e usando o infoset agora validado no fluxo de memória, que é encapsulado por um XmlDictionaryReader e adicionado à mensagem de substituição.

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

O InspectionValidationHandler método é chamado pela validação XmlReader sempre que ocorre um erro ou aviso de validação de esquema. A implementação a seguir só funciona com erros e ignora todos os avisos.

Em primeira consideração, pode parecer possível injetar XmlReader uma validação na mensagem com o inspetor de mensagens e deixar a validação acontecer à medida que a mensagem é processada e sem armazenar a mensagem em buffer. Isso, no entanto, significa que esse retorno de chamada lança as exceções de validação em algum lugar na infraestrutura do modelo de serviço ou no código do usuário à medida que nós XML inválidos são detetados, resultando em comportamento imprevisível. A abordagem de buffer protege o código do usuário de mensagens inválidas, inteiramente.

Como discutido anteriormente, as exceções lançadas pelo manipulador diferem entre o cliente e o serviço. No serviço, as exceções são derivadas de FaultException, no cliente as exceções são exceções personalizadas regulares.

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

Comportamento

Os inspetores de mensagens são extensões para o tempo de execução do cliente ou o tempo de execução do dispatch. Tais extensões são configuradas usando comportamentos. Um comportamento é uma classe que altera o comportamento do tempo de execução do modelo de serviço alterando a configuração padrão ou adicionando extensões (como inspetores de mensagens) a ele.

A classe a seguir SchemaValidationBehavior é o comportamento usado para adicionar o inspetor de mensagens deste exemplo ao cliente ou ao tempo de execução de expedição. A implementação é bastante básica em ambos os casos. Em ApplyClientBehavior e ApplyDispatchBehavior, o inspetor de mensagens é criado e adicionado à MessageInspectors coleção do respetivo tempo de execução.

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
}

Nota

Esse comportamento específico não funciona como um atributo e, portanto, não pode ser adicionado declarativamente a um tipo de contrato de um tipo de serviço. Essa é uma decisão por design tomada porque a coleção de esquema não pode ser carregada em uma declaração de atributo e referir-se a um local de configuração extra (por exemplo, para as configurações do aplicativo) nesse atributo significa criar um elemento de configuração que não é consistente com o restante da configuração do modelo de serviço. Portanto, esse comportamento só pode ser adicionado imperativamente por meio de código e por meio de uma extensão de configuração de modelo de serviço.

Adicionando o Inspetor de Mensagens por meio da Configuração

Para configurar um comportamento personalizado em um ponto de extremidade no arquivo de configuração do aplicativo, o modelo de serviço requer que os implementadores criem um elemento de extensão de configuração representado por uma classe derivada de BehaviorExtensionElement. Essa extensão deve ser adicionada à seção de configuração do modelo de serviço para extensões, conforme mostrado para a extensão a seguir discutida nesta seção.

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

As extensões podem ser adicionadas no aplicativo ou ASP.NET arquivo de configuração, que é a escolha mais comum, ou no arquivo de configuração da máquina.

Quando a extensão é adicionada a um escopo de configuração, o comportamento pode ser adicionado a uma configuração de comportamento, conforme mostrado no código a seguir. As configurações de comportamento são elementos reutilizáveis que podem ser aplicados a vários pontos de extremidade, conforme necessário. Como o comportamento específico a ser configurado aqui implementa , ele só é válido na respetiva seção de configuração no arquivo de IEndpointBehaviorconfiguração.

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

O <schemaValidator> elemento que configura o inspetor de mensagens é apoiado pela SchemaValidationBehaviorExtensionElement classe. A classe expõe duas propriedades públicas booleanas denominadas ValidateRequest e ValidateReply. Ambos estão marcados com um ConfigurationPropertyAttributearquivo . Este atributo constitui o link entre as propriedades de código e os atributos XML que podem ser vistos no elemento de configuração XML anterior. A classe também tem uma propriedade Schemas que é adicionalmente marcada com o ConfigurationCollectionAttribute e é do tipo SchemaCollection, que também faz parte deste exemplo, mas omitida deste documento para brevidade. Essa propriedade, juntamente com a coleção e a classe SchemaConfigElement do elemento collection, reverte o <schemas> elemento no trecho de configuração anterior e permite adicionar uma coleção de esquemas ao conjunto de validação.

O método substituído CreateBehavior transforma os dados de configuração em um objeto de comportamento quando o tempo de execução avalia os dados de configuração à medida que cria um cliente ou um ponto de extremidade.

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

Adicionando inspetores de mensagens imperativamente

Exceto por meio de atributos (que não são suportados neste exemplo pelo motivo citado anteriormente) e configuração, os comportamentos podem ser facilmente adicionados a um tempo de execução de cliente e serviço usando código imperativo. Neste exemplo, isso é feito no aplicativo cliente para testar o inspetor de mensagens do cliente. A GenericClient classe é derivada de , que expõe a configuração do ponto de extremidade ao código do ClientBase<TChannel>usuário. Antes que o cliente seja aberto implicitamente, a configuração do ponto de extremidade pode ser alterada, por exemplo, adicionando comportamentos, conforme mostrado no código a seguir. Adicionar o comportamento no serviço é amplamente equivalente à técnica de cliente mostrada aqui e deve ser executado antes que o host de serviço seja aberto.

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

Para configurar, compilar e executar o exemplo

  1. Certifique-se de ter executado o procedimento de instalação única para os exemplos do Windows Communication Foundation.

  2. Para criar a solução, siga as instruções em Criando os exemplos do Windows Communication Foundation.

  3. Para executar o exemplo em uma configuração de máquina única ou cruzada, siga as instruções em Executando os exemplos do Windows Communication Foundation.