Compartilhar via


Inspetores de mensagem

Aamostra 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 runtime do cliente do modelo de serviço e no runtime de expedição de forma programática ou por meio da configuração e que pode inspecionar e alterar mensagens depois de recebidas ou antes de enviadas.

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

Inspetor de Mensagens

Os inspetores de mensagens do cliente implementam a interface IClientMessageInspector e os inspetores de mensagens de serviço implementam a interface IDispatchMessageInspector. 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 um inspetor de mensagens combinado. O inspetor é construído passando um conjunto de esquemas nos quais as mensagens de entrada e saída são validadas e permite que o desenvolvedor especifique se as mensagens de entrada ou saída são validadas e se o inspetor está no modo de expedição ou cliente, o que afeta o tratamento de erros conforme discutido posteriormente 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 (despachador) precisa implementar os dois métodos IDispatchMessageInspector, AfterReceiveRequest e BeforeSendReply(Message, Object).

AfterReceiveRequest é invocado pelo despachador quando uma mensagem é recebida, processada pela pilha de canais e atribuída a um serviço, mas antes de ser desserializada e expedida para uma operação. Se a mensagem de entrada foi criptografada, a mensagem já estará descriptografada quando chegar ao inspetor de mensagens. O método obtém a mensagem request passada como um parâmetro de referência, que permite que a mensagem seja inspecionada, manipulada ou substituída conforme necessário. O valor retornado pode ser qualquer objeto e é usado como um objeto de estado de correlação 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 local e privado 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 de entrada é processada. Isso permite que as extensões contem com a chamada simétrica, independentemente do MEP. Assim como acontece com 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 ao método ValidMessageBody, mas a manipulação de erros de validação é ligeiramente diferente nesse caso.

Se ocorrer um erro de validação no serviço, o método ValidateMessageBody gerará exceções derivadas de FaultException. Em AfterReceiveRequest, essas exceções podem ser colocadas na infraestrutura do modelo de serviço em que são automaticamente transformadas em falhas SOAP e retransmitidas ao cliente. Em BeforeSendReply, as exceções FaultException não podem ser colocadas na infraestrutura, pois a transformação das exceções de falha geradas pelo serviço ocorre antes que o inspetor de mensagens seja chamado. Portanto, a implementação a seguir captura a exceção ReplyValidationFault conhecida 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 precisam ser implementados de IClientMessageInspector são AfterReceiveReply e BeforeSendRequest.

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

A diferença de comportamo entre a validação do cliente e do serviço (conforme especificado no construtor) é que a validação do cliente gera exceções locais colocadas no código do usuário porque ocorrem localmente e não por causa de uma falha de serviço. Em geral, a regra é que os inspetores do despachador de serviço geram falhas e que os inspetores de cliente lançam exceções.

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

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

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

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

O fundamental desse inspetor de mensagens em particular é o método ValidateMessageBody. Para executar seu trabalho, ele encapsula um XmlReader de validação 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 contém e o retorno de chamada de validação é definido como um delegado que se refere ao InspectionValidationHandler definido com esse método. Para executar a validação, a mensagem é lida e colocada em umXmlDictionaryWriter apoiado por fluxo de memória. Se ocorrer um erro de validação ou aviso no processo, o método de retorno de chamada será invocado.

Se nenhum erro ocorrer, será construída uma nova mensagem que copia as propriedades e os cabeçalhos da mensagem original e usa o conjunto de informações agora validado no fluxo de memória, 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 método InspectionValidationHandler é chamado pelo XmlReader de validação sempre que ocorrer 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 um XmlReader de validação na mensagem com o inspetor de mensagens e permitir que a validação aconteça à medida que a mensagem é processada e sem armazenar a mensagem em buffer. No entanto, isso significa que esse retorno de chamada gera as exceções de validação em algum lugar na infraestrutura do modelo de serviço ou no código do usuário, pois são detectados nós XML inválidos, o que resulta em um comportamento imprevisível. A abordagem de buffer protege totalmente o código do usuário de mensagens inválidas.

Conforme discutido anteriormente, as exceções geradas 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 runtime do cliente ou o runtime de despacho. Essas extensões são configuradas usando comportamentos. Um comportamento é uma classe que altera o comportamento do runtime do modelo de serviço, alterando a configuração padrão ou adicionando extensões (como inspetores de mensagens) a ela.

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

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
}

Observação

Esse comportamento específico não dobra 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 planejada tomada porque a coleção de esquemas não pode ser carregada em uma declaração de atributo, e fazer referência a um local de configuração extra (por exemplo, as configurações do aplicativo), nesse atributo, significa criar um elemento de configuração inconsistente 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 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 exige que os implementadores criem um elemento de extensão de configuração representado por uma classe derivada de BehaviorExtensionElement. Essa extensão precisa ser adicionada à seção de configuração do modelo de serviço para extensões, conforme mostrado para a seguinte extensão 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 arquivo de configuração ASP.NET (a opção mais comum) ou no arquivo de configuração do computador.

Quando a extensão é adicionada a um escopo de configuração, o comportamento pode ser adicionado a uma configuração de comportamento, como 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 IEndpointBehavior, ele só é válido na respectiva seção de configuração no arquivo de configuraçã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 elemento <schemaValidator> que configura o inspetor de mensagens é apoiado pela classe SchemaValidationBehaviorExtensionElement. A classe expõe duas propriedades públicas boolianas nomeadas ValidateRequest e ValidateReply. Ambas são marcadas com ConfigurationPropertyAttribute. Esse atributo constitui o vínculo 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 marcada com o tipo ConfigurationCollectionAttribute e é do tipo SchemaCollection, que também faz parte desse exemplo, mas omitida deste documento para brevidade. Essa propriedade, juntamente com a coleção e o SchemaConfigElement de classe de elemento de coleção, apoia o <schemas> elemento no snippet de configuração anterior e permite adicionar uma coleção de esquemas ao conjunto de validação.

O método CreateBehavior substituído transforma os dados de configuração em um objeto de comportamento quando o runtime 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 de forma imperativa

Exceto por meio de atributos (não compatíveis neste exemplo pelo motivo citado anteriormente) e pela configuração, os comportamentos podem ser adicionados facilmente a um cliente e ao runtime de serviço usando o código imperativo. Neste exemplo, isso é feito no aplicativo cliente para testar o inspetor de mensagens do cliente. A classe GenericClient é derivada de ClientBase<TChannel>, o que expõe a configuração do ponto de extremidade ao código do 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 é em grande parte equivalente à técnica do cliente mostrada aqui e precisa ser executada 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. Verifique se você executou o Procedimento de instalação única para os exemplos do Windows Communication Foundation.

  2. Para compilar a solução, siga as instruções contidas em Como compilar as amostras do Windows Communication Foundation.

  3. Para executar a amostra em uma configuração de computador único ou entre computadores, siga as instruções contidas em Como executar as amostras do Windows Communication Foundation.