Compartir a través de


Inspectores de mensajes

En el ejemplo MessageInspectors se muestra cómo implementar y configurar inspectores de mensajes de cliente y servicio.

Un inspector de mensajes es un objeto de extensibilidad que se puede usar en el tiempo de ejecución de cliente y en el tiempo de ejecución de despacho del modelo de servicio, ya sea mediante programación o a través de configuración, y que puede inspeccionar y modificar los mensajes después de que se reciban o antes de que se envíen.

En este ejemplo se implementa un mecanismo básico de validación de mensajes de cliente y servicio que valida los mensajes entrantes en un conjunto de documentos de esquema XML configurables. Tenga en cuenta que este ejemplo no valida los mensajes de cada operación. Se trata de una simplificación intencionada.

Inspector de mensajes

Los inspectores de mensajes de cliente implementan la IClientMessageInspector interfaz y los inspectores de mensajes de servicio implementan la IDispatchMessageInspector interfaz. Las implementaciones se pueden combinar en una sola clase para formar un inspector de mensajes que funcione para ambos lados. En este ejemplo se implementa un inspector de mensajes combinado. El inspector se construye pasando un conjunto de esquemas en los que se validan los mensajes entrantes y salientes y permite al desarrollador especificar si se validan los mensajes entrantes o salientes y si el inspector está en modo de envío o cliente, lo que afecta al control de errores tal y como se describe más adelante en este tema.

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

Todos los inspectores de mensaje de servicio (distribuidor) deben implementar los dos métodos IDispatchMessageInspectorAfterReceiveRequest y BeforeSendReply(Message, Object).

El distribuidor invoca AfterReceiveRequest cuando se ha recibido un mensaje, la pila del canal lo ha procesado y se ha asignado a un servicio, pero antes de que sea deserializado y enviado a una operación. Si el mensaje entrante se cifró, el mensaje ya se descifra cuando llega al inspector de mensajes. El método obtiene el request mensaje pasado como parámetro de referencia, lo que permite inspeccionar, manipular o reemplazar el mensaje según sea necesario. El valor devuelto puede ser cualquier objeto y se usa como un objeto de estado de correlación al que se pasa BeforeSendReply cuando el servicio devuelve una respuesta al mensaje actual. En este ejemplo, AfterReceiveRequest delega la inspección (validación) del mensaje al método ValidateMessageBody privado, local y no devuelve ningún objeto de estado de correlación. Este método garantiza que ningún mensaje no válido pase al servicio.

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) se invoca cada vez que una respuesta está lista para enviarse de vuelta a un cliente o, en el caso de mensajes unidireccionales, cuando se ha procesado el mensaje entrante. Esto permite a las extensiones contar con que se les llame simétricamente, sin tener en cuenta el MEP. Al igual que con AfterReceiveRequest, el mensaje se pasa como parámetro de referencia y se puede inspeccionar, modificar o reemplazar. La validación del mensaje que se realiza en este ejemplo se vuelve a delegar en el ValidMessageBody método , pero el control de errores de validación es ligeramente diferente en este caso.

Si se produce un error de validación en el servicio, el método ValidateMessageBody lanza excepciones derivadas de FaultException. En AfterReceiveRequest, estas excepciones se pueden colocar en la infraestructura del modelo de servicio donde se transforman automáticamente en errores soap y se retransmiten al cliente. En BeforeSendReply, las excepciones FaultException no se deben colocar en la infraestructura, porque la transformación de excepciones de error iniciada por el servicio se produce antes de llamar al inspector de mensaje. Por lo tanto, la siguiente implementación detecta la excepción conocida ReplyValidationFault y reemplaza el mensaje de respuesta por un mensaje de error explícito. Este método garantiza que la implementación del servicio devuelva ningún mensaje no válido.

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

El inspector de mensajes de cliente es muy similar. Los dos métodos que se deben implementar desde IClientMessageInspector son AfterReceiveReply y BeforeSendRequest.

Se invoca BeforeSendRequest cuando el mensaje se ha creado o por la aplicación cliente o por el formateador de la operación. Como con los inspectores de mensaje de distribuidor, el mensaje se puede simplemente inspeccionar o reemplazarlo por completo. En este ejemplo, el inspector delega al mismo método del asistente ValidateMessageBody local que también se utiliza para los inspectores de mensaje de envío.

La diferencia de comportamiento entre el cliente y la validación del servicio (como se especifica en el constructor) es que la validación del cliente produce excepciones locales que se colocan en el código de usuario porque se producen localmente y no debido a un error de servicio. Generalmente, la regla es que los inspectores de distribuidor de servicio inician errores y los inspectores del cliente inician excepciones.

Esta BeforeSendRequest implementación garantiza que no se envíen mensajes no válidos al servicio.

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

La AfterReceiveReply implementación garantiza que no se retransmitan mensajes no válidos recibidos del servicio al código de usuario del cliente.

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

El corazón de este inspector de mensajes concreto es el ValidateMessageBody método . Para realizar su trabajo, ajusta un XmlReader ,que realiza labores de validación, alrededor del subárbol de contenido del cuerpo del mensaje pasado. El lector se rellena con el conjunto de esquemas que el inspector del mensaje contiene y la devolución de llamada de la validación se establece en un delegado que hace referencia a InspectionValidationHandler , el cual se define junto a este método. Para realizar la validación, el mensaje se lee y se almacena en una secuencia de memoria respaldada por XmlDictionaryWriter. Si se produce un error de validación o una advertencia en el proceso, se invoca el método callback.

Si no se produce ningún error, se construye un nuevo mensaje que copia las propiedades y los encabezados del mensaje original y usa el conjunto de información validado ahora en el flujo de memoria, que se ajusta mediante XmlDictionaryReader y se agrega al mensaje de reemplazo.

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

El método InspectionValidationHandler es llamado por XmlReader , que realiza labores de validación, cada vez que se produce un error de validación del esquema o una advertencia. La siguiente implementación solo funciona con errores y omite todas las advertencias.

En primer lugar, podría parecer posible insertar una validación XmlReader en el mensaje con el inspector de mensajes y permitir que la validación se produzca a medida que se procesa el mensaje y sin almacenar en búfer el mensaje. Eso, sin embargo, significa que esta devolución de llamada inicia las excepciones de validación en alguna parte de la infraestructura del modelo del servicio o el código de usuario cuando se detectan nodos XML no válidos, lo que produce un comportamiento imprevisible. El enfoque de almacenamiento en búfer protege el código de usuario de mensajes no válidos, por completo.

Como se explicó anteriormente, las excepciones producidas por el controlador difieren entre el cliente y el servicio. En el servicio, las excepciones se derivan de FaultException, en el cliente, las excepciones son excepciones personalizadas normales.

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

Comportamiento

Los inspectores de mensaje son extensiones del tiempo de ejecución del cliente o de la distribución. Estas extensiones se configuran utilizando comportamientos. Un comportamiento es una clase que cambia el comportamiento del entorno de ejecución del modelo de servicio cambiando la configuración predeterminada o agregando extensiones (como inspectores de mensajes) a él.

La clase SchemaValidationBehavior siguiente es el comportamiento utilizado para agregar el inspector de mensaje de este ejemplo al tiempo de ejecución del cliente o la distribución. La implementación es bastante básica en ambos casos. En ApplyClientBehavior y ApplyDispatchBehavior, el inspector de mensajes se crea y se agrega a la colección MessageInspectors del tiempo de ejecución correspondiente.

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:

Este comportamiento concreto no se duplica como un atributo y, por tanto, no se puede agregar declarativamente a un tipo de contrato de un tipo de servicio. Esta es una decisión por diseño tomada porque la colección de esquemas no se puede cargar en una declaración de atributo y hacer referencia a una ubicación de configuración adicional (por ejemplo, a la configuración de la aplicación) en este atributo significa crear un elemento de configuración que no sea coherente con el resto de la configuración del modelo de servicio. Por lo tanto, este comportamiento solo se puede agregar de forma imperativa a través del código y a través de una extensión de configuración del modelo de servicio.

Agregar el Inspector de mensajes a través de la configuración

Para configurar un comportamiento personalizado en un punto de conexión en el archivo de configuración de la aplicación, el modelo de servicio requiere que los implementadores creen un elemento de extensión de configuración representado por una clase derivada de BehaviorExtensionElement. A continuación, esta extensión debe agregarse a la sección de configuración del modelo de servicio para las extensiones, como se muestra en la siguiente extensión que se describe en esta sección.

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

Las extensiones se pueden agregar en la aplicación o ASP.NET archivo de configuración, que es la opción más común o en el archivo de configuración de la máquina.

Cuando la extensión se agrega a un ámbito de configuración, el comportamiento se puede agregar a una configuración de comportamiento, tal como se muestra en el código siguiente. Las configuraciones de comportamiento son elementos reutilizables que se pueden aplicar a varios puntos de conexión según sea necesario. Dado que el comportamiento concreto que se va a configurar aquí implementa IEndpointBehavior, solo es válido en la sección de configuración correspondiente del archivo de configuración.

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

El <schemaValidator> elemento que configura el inspector de mensajes está respaldado por la SchemaValidationBehaviorExtensionElement clase . La clase expone dos propiedades públicas booleanas denominadas ValidateRequest y ValidateReply. Ambos se marcan con un ConfigurationPropertyAttribute. Este atributo constituye el vínculo entre las propiedades de código y los atributos XML que se pueden ver en el elemento de configuración XML anterior. La clase también tiene una propiedad Schemas que se marca además con la ConfigurationCollectionAttribute y es del tipo SchemaCollection, que también forma parte de este ejemplo, pero se omite de este documento por brevedad. Esta propiedad, junto con la colección y la clase de colección de elemento SchemaConfigElement respalda el elemento <schemas> en el fragmento de código de configuración anterior y permite agregar una colección de esquemas a la validación establecida.

El método invalidado CreateBehavior convierte los datos de configuración en un objeto de comportamiento cuando el tiempo de ejecución evalúa los datos de configuración a medida que compila un cliente o un punto de conexión.

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

Agregar imperiosamente inspectores de mensaje

Excepto a través de atributos (que no se admiten en este ejemplo por el motivo citado anteriormente) y la configuración, los comportamientos se pueden agregar fácilmente a un tiempo de ejecución de cliente y servicio mediante código imperativo. En este ejemplo, esto se hace en la aplicación cliente para probar el inspector de mensajes de cliente. La GenericClient clase se deriva de ClientBase<TChannel>, que expone la configuración del punto de conexión al código de usuario. Antes de que se abra implícitamente el cliente, se puede cambiar la configuración del punto de conexión, por ejemplo agregando comportamientos como se muestra en el código siguiente. Agregar el comportamiento en el servicio es en gran medida equivalente a la técnica de cliente que se muestra aquí y debe realizarse antes de que se abra el host de servicio.

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 y ejecutar el ejemplo

  1. Asegúrese de que ha realizado el procedimiento de instalación única para los ejemplos de Windows Communication Foundation.

  2. Para compilar la solución, siga las instrucciones que se indican en Compilación de los ejemplos de Windows Communication Foundation.

  3. Para ejecutar el ejemplo en una configuración de una máquina única o entre máquinas, siga las instrucciones de Ejecución de los ejemplos de Windows Communication Foundation.