Inspektorzy komunikatów

Przykład messageInspectors pokazuje, jak zaimplementować i skonfigurować inspektorów komunikatów klienta i usługi.

Inspektor komunikatów jest obiektem rozszerzalności, który może być używany w środowisku uruchomieniowym klienta modelu usługi i wysyłać środowisko uruchomieniowe programowo lub za pośrednictwem konfiguracji oraz może sprawdzać i zmieniać komunikaty po ich odebraniu lub przed ich wysłaniem.

Ten przykład implementuje podstawowy mechanizm weryfikacji komunikatów klienta i usługi, który weryfikuje przychodzące komunikaty względem zestawu konfigurowalnych dokumentów schematu XML. Należy pamiętać, że ten przykład nie weryfikuje komunikatów dla każdej operacji. Jest to celowe uproszczenie.

Inspektor komunikatów

Inspektorzy komunikatów klienta implementują IClientMessageInspector interfejs i inspektorzy komunikatów usługi implementują IDispatchMessageInspector interfejs. Implementacje można połączyć w jedną klasę, aby utworzyć inspektora komunikatów, który działa po obu stronach. Ten przykład implementuje taki połączony inspektor komunikatów. Inspektor jest konstruowany przekazujący zestaw schematów, względem których są weryfikowane komunikaty przychodzące i wychodzące, i umożliwia deweloperowi określenie, czy komunikaty przychodzące lub wychodzące są weryfikowane i czy inspektor jest w trybie wysyłania lub klienta, co wpływa na obsługę błędów, jak opisano w dalszej części tego tematu.

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

Każdy inspektor komunikatów usługi (dyspozytora) musi zaimplementować dwie IDispatchMessageInspector metody AfterReceiveRequest i BeforeSendReply(Message, Object).

AfterReceiveRequest jest wywoływany przez dyspozytor po odebraniu komunikatu, przetworzony przez stos kanału i przypisany do usługi, ale przed jego deserializacji i wysłania do operacji. Jeśli wiadomość przychodząca została zaszyfrowana, wiadomość jest już odszyfrowywane po dotarciu do inspektora komunikatów. Metoda pobiera request komunikat przekazywany jako parametr referencyjny, który umożliwia inspekcję, manipulowanie lub zastępowanie komunikatu zgodnie z potrzebami. Wartość zwracana może być dowolnym obiektem i jest używana jako obiekt stanu korelacji przekazywany do BeforeSendReply , gdy usługa zwraca odpowiedź do bieżącego komunikatu. W tym przykładzie AfterReceiveRequest deleguje inspekcję (walidację) komunikatu do prywatnej, lokalnej metody ValidateMessageBody i nie zwraca obiektu stanu korelacji. Ta metoda gwarantuje, że żadne nieprawidłowe komunikaty nie są przekazywane do usługi.

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) jest wywoływany za każdym razem, gdy odpowiedź jest gotowa do wysłania z powrotem do klienta lub w przypadku jednokierunkowych komunikatów, gdy wiadomość przychodząca została przetworzona. Dzięki temu rozszerzenia mogą liczyć na wywoływanie symetrycznie, niezależnie od mep. Podobnie jak w przypadku AfterReceiveRequest, komunikat jest przekazywany jako parametr referencyjny i można go sprawdzić, zmodyfikować lub zamienić. Walidacja komunikatu wykonywanego w tym przykładzie jest ponownie delegowana do ValidMessageBody metody, ale obsługa błędów walidacji jest nieco inna w tym przypadku.

Jeśli w usłudze FaultExceptionwystąpi błąd weryfikacji, ValidateMessageBody metoda zgłasza wyjątki pochodne. W AfterReceiveRequestsystemie te wyjątki można umieścić w infrastrukturze modelu usług, w której są one automatycznie przekształcane w błędy protokołu SOAP i przekazywane do klienta. W BeforeSendReplysystemie FaultException wyjątki nie mogą być umieszczane w infrastrukturze, ponieważ przekształcenie wyjątków błędów zgłaszanych przez usługę ma miejsce przed wywołaniem inspektora komunikatów. W związku z tym poniższa implementacja przechwytuje znany ReplyValidationFault wyjątek i zastępuje komunikat odpowiedzi jawnym komunikatem o błędzie. Ta metoda gwarantuje, że implementacja usługi nie zwraca nieprawidłowych komunikatów.

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

Inspektor komunikatów klienta jest bardzo podobny. Dwie metody, z których należy zaimplementować IClientMessageInspector , to AfterReceiveReply i BeforeSendRequest.

BeforeSendRequest jest wywoływany, gdy komunikat został skomponowany przez aplikację kliencą lub przez program formatujący operację. Podobnie jak w przypadku inspektorów komunikatów dyspozytora, wiadomość może być po prostu sprawdzona lub całkowicie zastąpiona. W tym przykładzie inspektor deleguje do tej samej lokalnej ValidateMessageBody metody pomocniczej, która jest również używana dla inspektorów komunikatów wysyłania.

Różnica behawioralna między weryfikacją klienta i usługi (jak określono w konstruktorze) polega na tym, że walidacja klienta zgłasza lokalne wyjątki wprowadzone w kodzie użytkownika, ponieważ występują lokalnie, a nie z powodu awarii usługi. Ogólnie rzecz biorąc, regułą jest to, że inspektorzy dyspozytora usług zgłaszają błędy i że inspektorzy klienta zgłaszają wyjątki.

Ta implementacja BeforeSendRequest gwarantuje, że do usługi nie są wysyłane żadne nieprawidłowe komunikaty.

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

Implementacja AfterReceiveReply gwarantuje, że żadne nieprawidłowe komunikaty odebrane z usługi nie są przekazywane do kodu użytkownika klienta.

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

Sercem tego konkretnego inspektora ValidateMessageBody komunikatów jest metoda. Aby wykonać swoją pracę, opakowuje walidację XmlReader wokół pod drzewa zawartości treści przekazanego komunikatu. Czytelnik jest wypełniany zestawem schematów, które inspektor komunikatów przechowuje, a wywołanie zwrotne weryfikacji jest ustawione na delegata odwołującego się do zdefiniowanego InspectionValidationHandler razem z tą metodą. Aby przeprowadzić walidację, komunikat jest następnie odczytywany i buforowany do strumienia pamięci opartego XmlDictionaryWriterna strumieniu. Jeśli w procesie wystąpi błąd weryfikacji lub ostrzeżenie, wywoływana jest metoda wywołania zwrotnego.

Jeśli wystąpi błąd, zostanie utworzony nowy komunikat, który kopiuje właściwości i nagłówki z oryginalnego komunikatu i używa teraz zweryfikowanego zestawu informacji w strumieniu pamięci, który jest opakowany przez XmlDictionaryReader element i dodany do komunikatu zastępczego.

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

Metoda jest wywoływana InspectionValidationHandler przez walidację XmlReader za każdym razem, gdy wystąpi błąd weryfikacji schematu lub ostrzeżenie. Poniższa implementacja działa tylko z błędami i ignoruje wszystkie ostrzeżenia.

Po pierwsze, może się wydawać, że można wstrzyknąć walidację XmlReader do komunikatu za pomocą inspektora komunikatu i pozwolić, aby walidacja miała miejsce w miarę przetwarzania komunikatu i bez buforowania komunikatu. Oznacza to jednak, że to wywołanie zwrotne zgłasza wyjątki weryfikacji gdzieś w infrastrukturze modelu usługi lub kod użytkownika jako wykryto nieprawidłowe węzły XML, co powoduje nieprzewidywalne zachowanie. Metoda buforowania chroni kod użytkownika przed nieprawidłowymi komunikatami, całkowicie.

Jak wspomniano wcześniej, wyjątki zgłaszane przez program obsługi różnią się między klientem a usługą. W usłudze wyjątki pochodzą z FaultExceptionprogramu na kliencie, wyjątki są zwykłymi wyjątkami niestandardowymi.

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

Zachowanie

Inspektorzy komunikatów są rozszerzeniami środowiska uruchomieniowego klienta lub środowiska uruchomieniowego wysyłania. Takie rozszerzenia są konfigurowane przy użyciu zachowań. Zachowanie to klasa, która zmienia zachowanie środowiska uruchomieniowego modelu usługi przez zmianę domyślnej konfiguracji lub dodawanie do niego rozszerzeń (takich jak inspektorzy komunikatów).

Poniższa SchemaValidationBehavior klasa to zachowanie używane do dodawania inspektora komunikatów tego przykładu do klienta lub środowiska uruchomieniowego wysyłania. Implementacja jest raczej podstawowa w obu przypadkach. W ApplyClientBehavior systemach i ApplyDispatchBehaviorinspektor komunikatów jest tworzony i dodawany do MessageInspectors kolekcji odpowiedniego środowiska uruchomieniowego.

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
}

Uwaga

To konkretne zachowanie nie podwaja się jako atrybut i dlatego nie można dodać deklaratywnie do typu kontraktu typu usługi. Jest to decyzja projektowa, ponieważ nie można załadować kolekcji schematów w deklaracji atrybutu i odwoływania się do dodatkowej lokalizacji konfiguracji (na przykład do ustawień aplikacji) w tym atrybucie oznacza utworzenie elementu konfiguracji, który nie jest zgodny z pozostałą konfiguracją modelu usługi. W związku z tym to zachowanie można dodawać tylko za pośrednictwem kodu i rozszerzenia konfiguracji modelu usługi.

Dodawanie inspektora komunikatów za pomocą konfiguracji

Aby skonfigurować niestandardowe zachowanie w punkcie końcowym w pliku konfiguracji aplikacji, model usługi wymaga implementacji, aby utworzyć element rozszerzenia konfiguracji reprezentowany przez klasę pochodną .BehaviorExtensionElement To rozszerzenie należy następnie dodać do sekcji konfiguracji modelu usługi dla rozszerzeń, jak pokazano w poniższej sekcji.

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

Rozszerzenia można dodać w pliku konfiguracji aplikacji lub ASP.NET, który jest najbardziej typowym wyborem lub w pliku konfiguracji maszyny.

Po dodaniu rozszerzenia do zakresu konfiguracji zachowanie można dodać do konfiguracji zachowania, jak pokazano w poniższym kodzie. Konfiguracje zachowania to elementy wielokrotnego użytku, które można zastosować do wielu punktów końcowych zgodnie z potrzebami. Ponieważ określone zachowanie, które ma zostać skonfigurowane w tym miejscu, implementuje IEndpointBehaviorelement , jest on prawidłowy tylko w odpowiedniej sekcji konfiguracji w pliku konfiguracji.

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

Element <schemaValidator> , który konfiguruje inspektora komunikatów, jest wspierany przez klasę SchemaValidationBehaviorExtensionElement . Klasa uwidacznia dwie właściwości publiczne wartości logiczne o nazwie ValidateRequest i ValidateReply. Oba te elementy są oznaczone znakiem ConfigurationPropertyAttribute. Ten atrybut stanowi połączenie między właściwościami kodu i atrybutami XML, które można zobaczyć w poprzednim elemefikatorze konfiguracji XML. Klasa ma również właściwość Schemas , która jest dodatkowo oznaczona znakiem ConfigurationCollectionAttribute i jest typu SchemaCollection, który jest również częścią tego przykładu, ale pominięty w tym dokumencie w celu zwięzłości. Ta właściwość wraz z kolekcją i klasą SchemaConfigElement elementu kolekcji tworzy kopię zapasową <schemas> elementu w poprzednim fragmencie kodu konfiguracji i umożliwia dodanie kolekcji schematów do zestawu weryfikacji.

Metoda zastępowana CreateBehavior zamienia dane konfiguracji w obiekt zachowania, gdy środowisko uruchomieniowe ocenia dane konfiguracji podczas kompilowania klienta lub punktu końcowego.

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

Dodawanie inspektorów komunikatów w sposób imperatywny

Z wyjątkiem atrybutów (które nie są obsługiwane w tym przykładzie z przyczyn cytowanych wcześniej) i konfiguracji, zachowania można łatwo dodać do środowiska uruchomieniowego klienta i usługi przy użyciu kodu imperatywnego. W tym przykładzie jest to wykonywane w aplikacji klienckiej w celu przetestowania inspektora komunikatów klienta. Klasa GenericClient pochodzi z ClientBase<TChannel>klasy , która uwidacznia konfigurację punktu końcowego w kodzie użytkownika. Zanim klient zostanie niejawnie otwarty, można zmienić konfigurację punktu końcowego, na przykład dodając zachowania, jak pokazano w poniższym kodzie. Dodanie zachowania w usłudze jest w dużej mierze równoważne z techniką klienta pokazaną tutaj i należy wykonać przed otwarciem hosta usługi.

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

Aby skonfigurować, skompilować i uruchomić przykład

  1. Upewnij się, że wykonano procedurę instalacji jednorazowej dla przykładów programu Windows Communication Foundation.

  2. Aby skompilować rozwiązanie, postępuj zgodnie z instrukcjami w temacie Building the Windows Communication Foundation Samples (Tworzenie przykładów programu Windows Communication Foundation).

  3. Aby uruchomić przykład w konfiguracji pojedynczej lub między maszynami, postępuj zgodnie z instrukcjami w temacie Uruchamianie przykładów programu Windows Communication Foundation.