Partager via


Inspecteurs des messages

L’exemple MessageInspectors montre comment implémenter et configurer des inspecteurs de messages client et de service.

Un inspecteur de message est un objet d’extensibilité qui peut être utilisé dans le runtime client du modèle de service et distribuer le runtime par programme ou par le biais de la configuration et qui peut inspecter et modifier les messages une fois qu’ils sont reçus ou avant leur envoi.

Cet exemple implémente un mécanisme de validation de message client et de service de base qui valide les messages entrants par rapport à un ensemble de documents de schéma XML configurables. Notez que cet exemple ne valide pas les messages pour chaque opération. Il s’agit d’une simplification intentionnelle.

Visionneur de messages

Les inspecteurs de messages clients implémentent l’interface IClientMessageInspector et les inspecteurs de message de service implémentent l’interface IDispatchMessageInspector . Les implémentations peuvent être combinées en une seule classe pour former un inspecteur de message qui fonctionne pour les deux côtés. Cet exemple implémente un tel inspecteur de message combiné. L’inspecteur est construit en passant un ensemble de schémas sur lesquels les messages entrants et sortants sont validés et permet au développeur de spécifier si les messages entrants ou sortants sont validés et si l’inspecteur est en mode dispatch ou client, ce qui affecte la gestion des erreurs comme indiqué plus loin dans cette rubrique.

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

Tout inspecteur de message (répartiteur) de service doit implémenter les deux méthodes IDispatchMessageInspectorAfterReceiveRequest et BeforeSendReply(Message, Object).

AfterReceiveRequest est appelé par le répartiteur lorsqu'un message a été reçu, traité par la pile de canaux et assigné à un service, mais avant qu'il soit désérialisé et distribué à une opération’ Si le message entrant a été chiffré, le message est déjà déchiffré lorsqu’il atteint l’inspecteur de message. La méthode obtient le request message transmis en tant que paramètre de référence, ce qui permet au message d’être inspecté, manipulé ou remplacé en fonction des besoins. La valeur de retour peut être n’importe quel objet et est utilisée comme objet d’état de corrélation qui est passé à BeforeSendReply lorsque le service retourne une réponse au message en cours. Dans cet exemple, AfterReceiveRequest délègue l’inspection (validation) du message à la méthode ValidateMessageBody privée, locale et ne retourne aucun objet d’état de corrélation. Cette méthode garantit qu’aucun message non valide ne passe dans le service.

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) est appelé chaque fois qu’une réponse est prête à être renvoyée à un client, ou dans le cas de messages unidirectionnel, lorsque le message entrant a été traité. Cela permet aux extensions d'être appelées symétriquement, indépendamment du MEP utilisé. Comme avec AfterReceiveRequest, le message est passé en tant que paramètre de référence et peut être inspecté, modifié ou remplacé. La validation du message qui est effectué dans cet exemple est à nouveau déléguée à la ValidMessageBody méthode, mais la gestion des erreurs de validation est légèrement différente dans ce cas.

Si une erreur de validation se produit sur le service, la méthode ValidateMessageBody lève FaultException (exceptions dérivées). Dans AfterReceiveRequest, ces exceptions peuvent être placées dans l’infrastructure de modèle de service où elles sont automatiquement transformées en erreurs SOAP et relayées au client. Dans BeforeSendReply, les exceptions FaultException ne doivent pas être placées dans l'infrastructure, car la transformation des exceptions levées par le service se produit avant l'appel de l'inspecteur de message. Par conséquent, l’implémentation suivante intercepte l’exception connue ReplyValidationFault et remplace le message de réponse par un message d’erreur explicite. Cette méthode garantit qu’aucun message non valide n’est retourné par l’implémentation du service.

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

L’inspecteur de message client est très similaire. Les deux méthodes qui doivent être implémentées à partir de IClientMessageInspector sont AfterReceiveReply et BeforeSendRequest.

BeforeSendRequest est appelé lorsque le message a été composé par l’application cliente ou par le formateur de l’opération. Comme avec les inspecteurs de message de répartiteur, le message peut être simplement inspecté ou être entièrement remplacé. Dans cet exemple, l'inspecteur délègue à la même méthode d'assistance locale ValidateMessageBody qui est également utilisée pour les inspecteurs de message de répartiteur.

La différence comportementale entre le client et la validation du service (comme spécifié dans le constructeur) est que la validation du client lève des exceptions locales qui sont placées dans le code utilisateur, car elles se produisent localement et non en raison d’une défaillance de service. En général, la règle veut que les inspecteurs de répartiteur de service génèrent des erreurs et que les inspecteurs de client lèvent des exceptions.

Cette BeforeSendRequest implémentation garantit qu’aucun message non valide n’est envoyé au service.

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

L’implémentation AfterReceiveReply garantit qu’aucun message non valide reçu du service n’est relayé vers le code utilisateur client.

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

Le cœur de cet inspecteur de message particulier est la ValidateMessageBody méthode. Pour effectuer son travail, elle encapsule un XmlReader de validation autour du sous-arbre du contenu du corps du message passé. Le lecteur est rempli avec le jeu de schémas géré par l'inspecteur de message et le rappel de validation a pour valeur un délégué qui fait référence au InspectionValidationHandler défini avec cette méthode. Pour effectuer la validation, le message est ensuite lu et mis en attente dans un XmlDictionaryWriter reposant sur un flux de mémoire. Si une erreur de validation ou un avertissement se produit dans le processus, la méthode de rappel est appelée.

Si aucune erreur ne se produit, un nouveau message est construit en copiant les propriétés et les en-têtes du message d’origine, et utilise l’ensemble d’informations maintenant validé dans le flux de mémoire. Celui-ci est ensuite encapsulé par un XmlDictionaryReader, puis ajouté au message de remplacement.

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

La méthode InspectionValidationHandler est appelée par l'objet de validation XmlReader chaque fois qu'une erreur ou un avertissement de validation de schéma se produit. L’implémentation suivante fonctionne uniquement avec les erreurs et ignore tous les avertissements.

En premier lieu, il peut sembler possible d’injecter une validation XmlReader dans le message avec l’inspecteur de message et de laisser la validation se produire lorsque le message est traité et sans mettre en mémoire tampon le message. Cela signifie cependant que ce callback lève les exceptions de validation dans l'infrastructure du modèle de service ou le code utilisateur lorsque des nœuds XML non valides sont détectés, entraînant ainsi un comportement imprévisible. L’approche de mise en mémoire tampon protège entièrement le code utilisateur contre les messages non valides.

Comme indiqué précédemment, les exceptions levées par le gestionnaire diffèrent entre le client et le service. Sur le service, les exceptions sont dérivées de FaultException, tandis que sur le client, les exceptions sont des exceptions personnalisées classiques.

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

Comportement

Les inspecteurs de message sont des extensions au runtime du client ou au runtime de distribution. Ces extensions sont configurées par le biais de comportements. Un comportement est une classe qui modifie le comportement du runtime du modèle de service en modifiant la configuration par défaut ou en y ajoutant des extensions (comme des inspecteurs de message).

La classe suivante SchemaValidationBehavior est le comportement utilisé pour ajouter l’inspecteur de message de cet exemple au client ou au runtime de distribution. L’implémentation est plutôt basique dans les deux cas. Dans ApplyClientBehavior et ApplyDispatchBehavior, l’inspecteur de message est créé et ajouté à la MessageInspectors collection du runtime respectif.

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
}

Remarque

Ce comportement particulier ne se comporte pas comme un attribut et ne peut donc pas être ajouté de manière déclarative à un type de service dans un type de contrat. Il s’agit d’une décision de conception prise, car la collection de schémas ne peut pas être chargée dans une déclaration d’attribut et fait référence à un emplacement de configuration supplémentaire (par exemple aux paramètres d’application) dans cet attribut signifie créer un élément de configuration qui n’est pas cohérent avec le reste de la configuration du modèle de service. Par conséquent, ce comportement peut uniquement être ajouté par le biais du code et par le biais d’une extension de configuration de modèle de service.

Ajout de l’inspecteur de message via la configuration

Pour configurer un comportement personnalisé sur un point de terminaison dans le fichier de configuration de l’application, le modèle de service nécessite des implémenteurs pour créer un élément d’extension de configuration représenté par une classe dérivée de BehaviorExtensionElement. Cette extension doit ensuite être ajoutée à la section de configuration du modèle de service pour les extensions, comme indiqué pour l’extension suivante décrite dans cette section.

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

Les extensions peuvent être ajoutées dans l’application ou ASP.NET fichier de configuration, qui est le choix le plus courant ou dans le fichier de configuration de l’ordinateur.

Lorsque l’extension est ajoutée à une étendue de configuration, le comportement peut être ajouté à une configuration de comportement, comme indiqué dans le code suivant. Les configurations de comportement sont des éléments réutilisables qui peuvent être appliqués à plusieurs points de terminaison selon les besoins. Étant donné que le comportement particulier à configurer ici implémente IEndpointBehavior, il est valide uniquement dans la section de configuration correspondante dans le fichier de configuration.

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

L’élément <schemaValidator> qui configure l’inspecteur de message est soutenu par la SchemaValidationBehaviorExtensionElement classe. La classe expose deux propriétés publiques booléennes nommées ValidateRequest et ValidateReply. Ces deux éléments sont marqués avec un ConfigurationPropertyAttribute. Cet attribut constitue le lien entre les propriétés de code et les attributs XML qui peuvent être vus sur l’élément de configuration XML précédent. La classe possède également une propriété Schemas qui est marquée avec ConfigurationCollectionAttribute et est de type SchemaCollection. Cette propriété fait partie de cet exemple, mais elle est omise de ce document pour des raisons de concision. Cette propriété, ainsi que la collection et la classe SchemaConfigElement d’élément de collection, sauvegarde l’élément <schemas> dans l’extrait de code de configuration précédent et permet d’ajouter une collection de schémas au jeu de validation.

La méthode substituée CreateBehavior transforme les données de configuration en objet de comportement lorsque le runtime évalue les données de configuration lorsqu’il génère un client ou un point de terminaison.

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

Ajout d'inspecteurs de message de façon impérative

À l’exception des attributs (qui ne sont pas pris en charge dans cet exemple pour la raison mentionnée précédemment) et de la configuration, les comportements peuvent facilement être ajoutés à un runtime client et de service à l’aide de code impératif. Dans cet exemple, cette opération est effectuée dans l’application cliente pour tester l’inspecteur de message client. La classe GenericClient est dérivée de ClientBase<TChannel>, qui rend accessible la configuration du point de terminaison au code utilisateur. Avant que le client soit implicitement ouvert, la configuration du point de terminaison peut être modifiée, par exemple en ajoutant des comportements comme indiqué dans le code suivant. L'ajout du comportement sur le service est en grande partie équivalent à la technique du client présentée ici et doit être effectué avant que l'hôte de service ne soit ouvert.

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

Pour configurer, générer et exécuter l’exemple

  1. Assurez-vous d’avoir effectué la Procédure d’installation unique pour les exemples Windows Communication Foundation.

  2. Pour générer la solution, suivez les instructions de Création des exemples Windows Communication Foundation.

  3. Pour exécuter l’exemple dans une configuration à un ou plusieurs ordinateurs, conformez-vous aux instructions figurant dans la rubrique Exécution des exemples Windows Communication Foundation.