Ringraziamenti

Questo articolo nasce dal lavoro congiunto con i miei due colleghi André Santacroce e Giulio Pansironi, che ringrazio tantissimo per aver condiviso, negli ultimi 4 anni, delle meravigliose esperienze lavorative.

 

Introduzione

Nell’ultimo progetto lavorativo uno dei requisiti che ho dovuto affrontare con i miei colleghi è stato quello di fornire un servizio WCF che permettesse di esporre un qualsiasi servizio Web offerto da un fornitore terza parte (d’ora in poi Terza Parte) con la sicurezza gestita nell’header, personalizzata rispetto alle esigenze del cliente, sostituendo quella originarimente esposta dai servizi di Terza Parte.

Il problema è stato risolto mediante la costruzione di un servizio WCF generico, che riespone il WSDL del servizio originario tranne per la parte di header.

 

Architettura

L’immagine seguente illustra l’architettura scelta:

Architettura WCF multipurpose

Dove GSC è il gestore, fornito dal cliente, attraverso il quale debbono transitare tutte le comunicazioni con Terza Parte, ad eccezione delle richieste dei metadati, che possono invece essere effettuate direttamente dal servizio WCF generico.

Il servizio WCF generico è capace di esporre una qualsiasi interfaccia descritta nel WSDL di uno dei servizi esposti da Terza Parte, togliendo l’header originario e sostituendolo con l’header richiesto da GSC.

Due sono quindi gli aspetti da gestire:

  • L’esposizione di un WSDL personalizzato, tramite mex dinamico;
  • La presa in carico di una richiesta qualsiasi, visto che i WSDL e quindi i metodi esposti variano.

 

Mex Dinamico

Per esporre un WSDL personalizzato è necessario estendere il comportamento predefinito di un WCF il quale, se chiamato con la URL del tipo xxxxx.svc?wsdl, attiva la generazione dinamica dei metadati sulla base dei metodi esposti definiti nel codice.

In questo caso i metodi esposti sono invece definiti nel WSDL del servizio di Terza Parte, pertanto è stata aggiunta una behavior extension, definita nel web.config, che permette di generare il WSDL desiderato utilizzando la sintassi xxx.svc?endpoint=URL WSDL del servizio Terza Parte.

<system.servicemodel>
   <extensions>
      <behaviorextensions>
         <!–estensione per ottenere il mex dinamico–>
         <add name=”DynamicMexBehaviorExtension”
              type=”Customer.Component.Behaviors.DynamicMexBehaviorExtensionElement, Customer.Component, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d803b2e54d29a49e” />
      </behaviorextensions>
   </extensions>

   <behaviors>
      <servicebehaviors>
         <behavior name=”metadataServiceExtension”>
            <servicemetadata httpgetenabled=”true” />
            <servicedebug includeexceptiondetailinfaults=”true” />
            <DynamicMexBehaviorExtension />
         </behavior
>
      </servicebehaviors
>
   </behaviors
>

</system.servicemodel>

La classe DynamicMexBehaviorExtensionElement è la classe che definisce quale comportamento (behavior) aggiungere al nostro WCF. Eredita da BehaviorExtensionElement per essere definita come tag nel web.config.

/// <summary>
/// Elemento di configurazione per agganciare il behavior DynamicMex
/// </summary>
public class DynamicMexBehaviorExtensionElement : BehaviorExtensionElement
{
   /// <summary>
   /// Restituisce il tipo del behavior da agganciare
   /// </summary>
   public override Type BehaviorType
   {
      get { return typeof(DynamicMexBehavior); }
   }

   /// <summary>
   /// Crea il behavior da agganciare
   /// </summary>
   protected override object CreateBehavior()
   {
      return new DynamicMexBehavior();
   }
}

Il comportamento vero e proprio è definito nella classe DynamicMexBehavior, che implementa l’interfaccia IServiceBehavior.

/// <summary>
/// Behavior per ottenere un servizio capace di generare dinamicamente un WSDL specifico
/// </summary>
public class DynamicMexBehavior: IServiceBehavior
{
   #region Implementazione IServiceBehavior
   /// <summary>
   /// Non usato
   /// </summary>
   void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription,
                     System.ServiceModel.
ServiceHostBase serviceHostBase,
                     System.Collections.ObjectModel.
Collection<ServiceEndpoint> endpoints,
                     System.ServiceModel.Channels.
BindingParameterCollection bindingParameters)
   {
   }

   /// <summary>
   /// Aggancia ad ogni dispatcher con endpoint riguardante i metadati,
   /// l’inspector che genera il WSDL custom
   /// </summary>
   void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription,
                                 System.ServiceModel.
ServiceHostBase serviceHostBase)
   {
      foreach (ChannelDispatcher chDisp in serviceHostBase.ChannelDispatchers)
      {
         foreach (EndpointDispatcher epDisp in chDisp.Endpoints)
         if (epDisp.ContractName.Contains(“Metadata”))
            epDisp.DispatchRuntime.MessageInspectors.Add(new DynamicWsdlInspector());
      }
   }

   /// <summary>
   /// Non usato
   /// </summary>
   void IServiceBehavior.Validate(ServiceDescription serviceDescription,
                     System.ServiceModel.
ServiceHostBase serviceHostBase)
   {
   }
   #endregion
}

È nel metodo ApplyDispatcher che si aggancia ad ogni dispatcher in grado di gestire i metadati una classe inspector, che permette di entrare nell’intimo di WCF e gestire qualsiasi aspetto della pipeline di richiesta:

/// <summary>
/// Gestisce la richiesta dei metadati in modo da fornire oltre al WSDL predefinito,
/// quello indicato nel parametro endpoint della querystring
/// </summary>
public class DynamicWsdlInspector : IDispatchMessageInspector
{
   #region Variabili private
   /// <summary>
   /// generatore WSDL
   /// </summary>
   private static WsdlGenerator generator;

   /// <summary>
   /// Parametri presenti nella querystring di richiesta mex
   /// </summary>
   private string mexparam;
   #endregion

   #region Implementazione IDispatchMessageInspector
   /// <summary>
   /// Appena riceve la richiesta prende nota della querystring
   /// </summary>
   object IDispatchMessageInspector.AfterReceiveRequest(
                  ref System.ServiceModel.Channels.Message request,
                  System.ServiceModel.
IClientChannel channel,
                  System.ServiceModel.
InstanceContext instanceContext)
   {
      mexparam = request.Headres.To.Query;
      return null;
   }

   /// <summary>
   /// Prima di restituire il WSDL, stabilisce se restituirlo così com’è o
   /// sostituirlo con quello custom
   /// </summary>
   /// <param name=”reply”></param>
   /// <param name=”correlationState”></param>
   void IDispatchMessageInspector.BeforeSendReply(ref Message reply, object correlationState)
   {
      //Se è stato specificato un WSDL…
      if (!string.IsNullOrEmpty(mexparam) && mexparam.IndexOf(“endpoint=”,
            StringComparison.OrdinalIgnoreCase) > -1)
      {
         try
         {
            //se non istanziato, istanzia il generatore e genera il WSDL sulla base dell’URL
            //indicato in endpoint
            if (generator == null)
               generator = new WsdlGenerator(Settings.Default.WsdlXslt);
            string wsdlUrl = System.Web.HttpUtility.UrlDecode(mexparam.Substring(
               mexparam.IndexOf(
“endpoint=”, StringComparison.OrdinalIgnoreCase) + 9));
            var wsdlGenerato = generator.GenerateWsdl(wsdlUrl,
               OperationContext.Current.Host.BaseAddresses[0].AbsoluteUri);

            //sostituisce al WSDL predefinito quello scaricato e modificato
            reply = Message.CreateMessage(XmlReader.Create(new StringReader(wsdlGenerato)), int.MaxValue,
               reply.Version);
         }
         catch (Exception ex)
         {
            //se non riesce ad importare il WSDL rilancia un’eccezione Not Found
            throw new EndpointNotFoundException(ex.Message);
   
      }
   
   }
   
}
   #endregion
}

DynamicWsdlInspector, che implementa IDispatchMessageInspector, utilizzando gli eventi seguenti per la generazione del WSDL:

  • IDispatchMessageInspector.AfterReceiveRequest, per leggere la querystring passata all’svc;
  • IDispatchMessageInspector.BeforeSendReply, per lasciare il comportamento predefinito riguardo la generazione dei metadati o sostituire il WSDL con quello opportunamente modificato, tramite richiesta http del WSDL e trasformazione, tramite XSLT del WSDL, sostituendo l’header originario con quello richiesto da GSC.

Accettazione di una qualsiasi richiesta esposta tramite WSDL custom

Per ora il servizio è in grado di esporre un WSDL qualsiasi ma per accettare le richieste manca la definizione di un metodo di accettazione di richiesta qualsiasi, proprio perché a priori il servizio non sa che WSDL sta esponendo.

Ci viene in aiuto la classe WCF MatchAllMessageFilter, che permette di istruire i dispatcher WCF ad accettare una qualsiasi richiesta. La classe viene utilizzata tramite una classe personalizzata, che implementa IContractBehavior ed è utilizzabile come attributo:

/// <summary>
/// Behavior per intercettare qualsiasi tipo di richiesta
/// </summary>
public class MatchAllEndpointsAttribute : Attribute, IContractBehavior
{
   /// <summary>
   /// Non usato
   /// </summary>
   public void AddBindingParameters(ContractDescription contractDescription,
         ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
   {
   }

   /// <summary>
   /// Non usato
   /// </summary>
   public void ApplyClientBehavior(ContractDescription contractDescription,
         ServiceEndpoint endpoint, ClientRuntime clientRuntime)
   {
   }

   /// <summary>
   /// Aggancia il filtro per intercettare qualunque tipo di richiesta
   /// </summary>
   public void ApplyDispatchBehavior(ContractDescription contractDescription,
        
ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
   {
      if (dispatchRuntime != null)
         dispatchRuntime.EndpointDispatcher.AddressFilter = new MatchAllMessageFilter();
   }

   /// <summary>
   /// Non usato
   /// </summary>
   public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
   {
   }
}

Grazie al metodo ApplyDispatchBehavior definito nell’interfaccia, istruiamo tutti i dispatcher ad accettare qualsiasi tipo di messaggio in input.

A questo punto non resta che definire l’interfaccia che dovrà esporre il nostro servizio:

/// <summary>
/// Interfaccia di MorphingService
/// </summary>
[MatchAllEndpoints] [ServiceContract(Namespace = “http://xxxxxxx”)] [XmlSerializerFormat(Style = OperationFormatStyle.Document, Use = OperationFormatUse.Literal)] public interface IMorphingService
{
   /// <summary>
   /// Gestore del generico messaggio in input
   /// </summary>
   /// <param name=”request”>richiesta</param>
   [OperationContract(Action = “*”, ReplyAction = “*”)]    [FaultContract(typeof(MyFault))]    Message ProcessMessage(Message request);
}

Notare l’applicazione dell’attributo MatchAllEndpoints, unita alla presenza di un unico metodo per gestire qualsiasi richiesta. Il metodo in questione, ProcessMessage, è corredato dall’attributo

[OperationContract(Action = “*”, ReplyAction = “*”)]

In modo da accettare qualsiasi action e reply action definita nel WSDL.

Osservare che la genericità del comportamento del servizio si paga con la perdita di tipizzazione. Infatti necessariamente il metodo è definito per ricevere e restituire un Message, che è la classe più generica rappresentante una richiesta.

Sta poi nell’implementazione di ProcessMessage estrarre dalla request il corpo del messaggio, elaborarlo e restituire un Message.

Nel nostro caso la soluzione è la seguente:

/// <summary>
/// Implementazione del gestore del generico messaggio in input
/// </summary>
/// <param name=”request”>richiesta</param>
public Message ProcessMessage(Message request)
{
   //Check input
   if (request == null)
      throw new ArgumentNullException(“request”);

   //Elabora il corpo del messaggio estraendolo dall’input sottoforma di XmlDocument
   //e crea un XmlDocument rappresentante una risposta SOAP
   XmlDocument response = …;

   //Restituisce la risposta, trasformandola in Message
   return Utility.ConvertXmlToMessage(response, request.Version);
}

L’estrazione del corpo del messaggio da Message è affidata ad una routine di questo tipo:

/// <summary>
/// Converte un Message in XmlDocument
/// </summary>
/// <param name=”message”>messaggio da convertire</param>
public static XmlDocument ConvertMessageToXml(Message message)
{
   //Check input
   if (message == null)
      throw new ArgumentNullException(“message”);

   //Crea l’XmlDocument scrivendo il message in un MemoryStream tramite un XmlWriter
   var res = new XmlDocument();
   using (var stream = new MemoryStream())
   {
      using (var xmlWriter = XmlWriter.Create(stream))
      {
         message.WriteMessage(xmlWriter);
         xmlWriter.Flush();
         stream.Flush();
         stream.Position = 0;
         res.Load(stream);
      }
   }

   return res;
}

Mentre questa è la routine per creare un Message a partire da un XML:

/// <summary>
/// Converte un XML rappresentante una risposta SOAP in un Message da utilizzare
/// come risposta in un metodo WCF
/// </summary>
/// <param name=”soapDoc”>documento da convertire</param>
/// <param name=”ver”>versione del message (da reperire dalla request)</param>
public static Message ConvertXmlToMessage(XmlDocument soapDoc, MessageVersion ver)
{
   //Check input
   if (soapDoc == null)
      throw new ArgumentNullException(“soapDoc”);

   //Carica l’XML nello stream
   MemoryStream stream = new MemoryStream();soapDoc.Save(stream);stream.Position = 0;

   //Legge lo stream col reader e lo converte in message
   XmlReader xRd = XmlReader.Create(stream);
   return Message.CreateMessage(xRd, int.MaxValue, ver);
}

 

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *