Vytvoření nového http modulu v ASP.NET

10. února 2004

Chtěli jste někdy na různých webových formulářích či dokonce v různých ASP.NET aplikacích provádět stejné nebo podobné činnosti? Tento článek vás přesvědčí, že s vestavěnou podporou pro http moduly v ASP.NET je zvládnutí těchto úloh příjemně jednoduché a navíc z hlediska žádoucí čistoty aplikačního kódu brilantní.

Http moduly slouží k vyčlenění kódu sdíleného více WWW aplikacemi, které jsou napsány v ASP.NET. S ASP.NET jsou dodávány moduly, které se v aplikacích starají například o cachování dat nebo o autentizaci uživatele. Ve svých aplikacích se ale nemusíme spoléhat jen na dodané moduly a pasivně obdivovat existující separaci obecné infrastrukturní logiky do samostatných funkčních celků, ale sami bychom měli vydělit vlastní opakovaně využitelný kód do http modulů šitých na míru řešené problematice. V článku bude předvedeno vytvoření http modulu, který přidá dokumentaci k WWW metodám bez rekompilace WWW služby.

Postup při psaní http modulů

Http modul musí implementovat rozhraní IHttpModule ze jmenného prostoru System.Web.

interface IHttpModule
{
  void Init (HttpApplication context);
  void Dispose();
}

V rozhraní IHttpModule jsou deklarovány dvě metody. Metoda Init je volána běhovým prostředím ASP.NET jednou po startu WWW aplikace a v argumentu context je jí předán odkaz na objekt aktuální WWW aplikace. Třída HttpApplication zveřejňuje události, na které budeme reagovat. Když například chceme být notifikováni o počátku a konci každého požadavku, přihlásíme si v metodě Init odběr událostí BeginRequest a EndRequest. V obslužných metodách událostí se nachází kód, který souvisí s činností modulu a který například na konci každého požadavku zapíše serverem vygenerovanou odpověď do souboru.

Metoda Dispose slouží k úklidu použitých neřízených prostředků a je volána při ukončení WWW aplikace. Tělo této metody bývá v http modulu často prázdné, protože se s neřízenými prostředky nepracuje, ale kdybyste ve svém modulu používali třeba P/Invoke a měli pomocí něj otevřen handle na soubor, tak v této metodě musíte handle uzavřít. Ještě lépe, metoda Dispose by se měla řídit takzvaným „Dispose“ návrhovým vzorem z .Net Frameworku.

Http modul musí také obsahovat bezparametrický veřejný konstruktor, aby jeho instance mohlo běhové prostředí vytvářet pomocí reflexe.

Pro http moduly je užitečná vlastnost Filter třídy HttpResponse. Do vlastnosti Filter je možné přiřadit libovolného potomka třídy Stream, přičemž je garantováno, že veškerý výstup bude zapsán přes tento Stream. Když budete například chtít cenzurovat obsah pro určité uživatele vašeho webu, vytvoříte potomka třídy Stream, který před zápisem do původního Streamu odstraní všechna závadná slova. Je ale třeba dodržovat pravidlo, že Stream ve vlastnosti Filter provede zápis do objektu Stream, který byl původně ve vlastnosti Filter. Zvláště důležité je dodržování tohoto pravidla při použití více http modulů, kdy každý z nich do vlastnosti Filter přiřadí svůj Stream, protože jinak bude korektně pracovat pouze poslední z nich. Jak jistě tušíte, možnost řetězení objektů Stream je naším spojencem v boji s nežádoucí funkční obezitou jednoho http modulu.

Po napsání musíme modul zaregistrovat u WWW aplikace. Registrace se provádí v souboru web.config v sekci configuration/system.web/httpModules.

<?xml version=“1.0″ encoding=“utf-8″ ?>
<configuration>
  …
   <system.web>
     <httpModules>
       <add name=“NewModule“ type=“NewModuleNamespace.NewModuleClass, NewModuleAssembly“/>
     </httpModules>
  …
</configuration>

Do sekce httpModules přidáme značku add, u níž hodnota atributu name určuje libovolné unikátní jméno modulu (NewModule)a v atributu type je v první části zadána úplná cesta přes jmenné prostory (NewModuleNamespace) ke třídě implementující rozhraní IHttpModule (NewModuleClass) a ve druhé části je jméno assembly (NewModuleAssembly). Assembly s http modulem by měla být umístěna do adresáře bin www služby.

Kdybychom chtěli náš http modul použít pro všechny WWW aplikace na jednom počítači, jak je tomu u většiny standardně dodávaných http modulů, registrace by musela být provedena v souboru machine.config a assembly bychom museli umístit do sdíleného úložiště (GAC). Když je asssembly s http modulem digitálně podepsána a umístěna v GAC, musí být v konfiguračním souboru zadáno plně kvalifikující jméno assembly, to znamená včetně čísla verze, kultury a hashe veřejného klíče (Public Key Token).

Vytvoření http modulu pro přidání dokumentace k metodám WWW služby

Nyní vytvoříme http modul, který přidá dokumentaci ke zvolené metodě WWW služby. Dokumentaci u WWW metody běžně přidáváme v kódu pomocí metaatributu WebMethod a jeho vlastnosti Description.

[WebMethod(Description=“Metoda vrátí nějaká data“)]

Ve vygenerovaném WSDL dokumentu s popisem WWW služby se dokumentace k metodě zobrazuje ve značce documentation pod značkami portType\operation. Pokud se ve WSDL specifikaci neorientujete, je to vše, co potřebujete vědět.


<portType name=“Service1Soap“>
   <operation name=“HelloWorld“>
     <documentation> Metoda vrátí nějaká data </documentation>
     <input message=“s0:HelloWorldSoapIn“ />
     <output message=“s0:HelloWorldSoapOut“ />
   </operation>
</portType name=“Service1Soap“>

Co ale dělat , když jsme dokumentaci k metodě nedodali a služba je již nasazena v produkčním prostředí? Místo přidání dokumentace do kódu, nové kompilace a nasazení služby, vytvoříme http modul, který při požadavku na WSDL dokument dokumentaci k operacím přidá. Požadavek na zobrazení WSDL poznáme tak, že cílové URL příchozího požadavku je zakončeno sekvencí znaků wsdl. Text umisťovaný do značek documentation k jednotlivým metodám bude uložen v souboru web.config v sekci appSettings. Kompletní kód http modulu si můžete stáhnout.

Nejprve vytvoříme nového potomka třídy Stream s názvem FilterStream, který uloží serverem generovaný WSDL popis služby do objektu MemoryStream a teprve poté jej zapíše do streamu, který byl původně ve vlastnosti Filter objektu HttpResponse. Původní Stream ve vlastnosti Filter je totiž určen pouze pro zápis a my potřebujeme později WSDL popis služby přečíst a upravit.

public class FilterStream : Stream
{
   private MemoryStream m_innerStream;
   private Stream m_originalStream;
   public FilterStream(Stream originalStream)
   {
     m_innerStream = new MemoryStream();
     m_originalStream = originalStream;
   }
  public MemoryStream InnerStream
  {
    get
    {
      return m_innerStream;
    }
  }
  …
}

Fragment kódu třídy FilterStream ukazuje, že v konstruktoru přijímáme odkaz na původní stream ve vlastnosti Filter a ukládáme jej do privátní proměnné m_originalStream. Privátní proměnná m_innerStream je inicializována novou instancí třídy MemoryStream. Tento stream, určený pro čtení i zápis, je zveřejněn ve vlastnosti InnerStream. Zbytek kódu třídy FilterStream není příliš zajímavý a ukazuje implementaci abstraktních vlastností a metod třídy Stream pomocí delegace na streamy v obou zmiňovaných proměnných.

Třída DocumentModule představuje samotný http modul pro přidání dokumentace k operacím ve WSDL dokumentu.

public class DocumentationModule : IHttpModule
{
   private const string WSDL_REQUEST = „wsdl“;
   private const string WSDL_NAMESPACE = „http://schemas.xmlsoap.org/wsdl/“;
   private const string WSDL_PREFIX = „wsdl“;
   private const string OPERATION_TAG = „operation“;
   private const string PORT_TYPE_TAG = „portType“;
   private const string DOCUMENTATION_TAG = „documentation“;
   private const string XPATH_GET_OPERATIONS = @“//“ + WSDL_PREFIX + „:“ + PORT_TYPE_TAG + @“/“ + WSDL_PREFIX + „:“ + OPERATION_TAG;
   private const int EMPTY_DOC = 0;
   private const string METHOD_KEY_SUFFIX = „Method“;
   private Hashtable m_operationsDoc;
  public DocumentationModule()
   {
     readConfig();
   }
   public void Init(HttpApplication context)
  {
    if (m_operationsDoc.Count != EMPTY_DOC)
    {
       context.BeginRequest +=new EventHandler(context_BeginRequest);
       context.EndRequest +=new EventHandler(context_EndRequest);
     }
   }
  public void Dispose()
  {
  }
   private void context_BeginRequest(object sender, EventArgs e)
   {
     if (isWSDLRequest())
       HttpContext.Current.Response.Filter = new FilterStream(HttpContext.Current.Response.Filter);
   }
  private void context_EndRequest(object sender, EventArgs e)
   {
     if (isWSDLRequest())
       addDocumentation();
   }
  >private void addDocumentation()
   {
     HttpResponse response = HttpContext.Current.Response;
     XmlDocument doc = new XmlDocument();
     MemoryStream originalStream = ((FilterStream)response.Filter).InnerStream;
     originalStream.Position = 0;
     try
     {
       XmlTextReader reader = new XmlTextReader(originalStream);
       doc.Load(reader);
     }
     catch (Exception e)
     {
       Trace.WriteLine(„Invalid response“);
       return;
     }
     XmlNamespaceManager namManager = new XmlNamespaceManager(doc.NameTable);
     namManager.AddNamespace(WSDL_PREFIX, WSDL_NAMESPACE);
     XmlNodeList operationsNodeList = getOperations(doc, namManager);
     writeDoc(operationsNodeList);
     response.Clear();
     doc.Save(response.Output);
   }
  private XmlNodeList getOperations(XmlDocument doc, XmlNamespaceManager namManager)
   {
     XmlNodeList operationsNodeList = doc.SelectNodes(XPATH_GET_OPERATIONS, namManager);
     return operationsNodeList;
  }
  private bool isWSDLRequest()
   {
     string path = HttpContext.Current.Request.RawUrl.ToLower();
     if (path.EndsWith(WSDL_REQUEST))
       return true;
     return false;
   }
  private void readConfig()
   {
     m_operationsDoc = new Hashtable();
     string[]keys = ConfigurationSettings.AppSettings.AllKeys;
     foreach (string key in keys)
     {
      if (key.EndsWith(METHOD_KEY_SUFFIX))
       {
         string hashKey = key.Substring(0, key.LastIndexOf(METHOD_KEY_SUFFIX));
         string hashValue = ConfigurationSettings.AppSettings[key];
         m_operationsDoc.Add(hashKey, hashValue);
       }
     }
   }
  private void writeDoc(XmlNodeList nodeList)
   {
     foreach(XmlNode node in nodeList)
     {
       string operationName = node.Attributes.GetNamedItem(„name“).Value;
       if (m_operationsDoc.ContainsKey(operationName))
       {
         XmlNode docNode = createDocumentationNode(node.OwnerDocument, (string) m_operationsDoc[operationName]);
         node.InsertBefore(docNode, node.FirstChild);
       }
     }
   }
   private XmlNode createDocumentationNode (XmlDocument parentDocument, string nodeValue)
  {
    XmlElement docElement = parentDocument.CreateElement(DOCUMENTATION_TAG, WSDL_NAMESPACE);
    docElement.InnerText = nodeValue;
    return docElement;
  }
}

Třída DocumentationModule implementuje rozhraní IHttpModule. Na začátku třídy jsou deklarovány konstanty. Konstanta WSDL_REQUEST říká, že požadavek musí končit znaky wsdl. Konstanta WSDL_NAMESPACE slouží k deklaraci základního jmenného prostoru WSDL dokumentu a konstanta WSDL_PREFIX určuje jeho prefix. Konstanty OPERATION_TAG, PORT_TYPE_TAG a DOCUMENTATION_TAG reprezentují názvy elementů ve WSDL dokumentu, se kterými budeme pracovat. Složená konstanta XPATH_GET_OPERATIONS vyjadřuje xpath dotaz pro výběr všech elementů operations (//wsdl:portType/wsdl:operation). Konstanta EMPTY_DOC reprezentuje stav, kdy v konfiguračním souboru v sekci appSettings nebyla nalezena žádná dokumentace pro metody. Konstanta METHOD_KEY_SUFFIX vyjadřuje konvenci, že atribut key v elementu add pod sekcí appSettings musí končit slovem Method, aby byla hodnota atributu value považována za dokumentaci k metodě WWW služby.

V privátní kolekci m_operationsDoc je udržována dokumentace k jednotlivým metodám. Klíčem pro každou položku dokumentace je název www metody.

V povinném bezparametrickém konstruktoru zavoláme metodu readConfig, která načte dokumentaci metod z konfiguračního souboru a uloží ji do kolekce m_operationsDoc.

Metoda Init z rozhraní IHttpModule nejdříve zkontroluje, zda v souboru web.config byla nalezena dokumentace k alespoň jedné metodě. Pokud ano, přihlásí si odběr událostí BeginRequest (začátek požadavku) a EndRequest (konec požadavku). V opačném případě nemá smysl brzdit provádění požadavků a modul nebude po dobu současného životního cyklu aplikace již volán.

Další povinná metoda z rozhraní IHttpModule s názvem Dispose má prázdné tělo, protože nepotřebujeme uvolňovat žádné neřízené zdroje.

Privátní metoda context_BeginRequest je vyvolána při události BeginRequest. Metoda ověřuje voláním pomocné metody isWsdlRequest, jestli má klient služby zájem o WDL dokument. Metoda isWSDLRequest vrací true, když URL požadavku končí řetězcem wsdl. Když má být vrácen WSDL dokument, je do vlastnosti Filter objektu HttpResponse uložen odkaz na novou instanci naší třídy FilterStream. Konstruktor třídy FilterStream dostane odkaz na původní stream ve vlastnosti Filter.

Po skončení požadavku a vygenerování WSDL dokumentu je vyvolána metoda context_EndRequest pro obsluhu události EndRequest. Opět dojde ke kontrole, zda aktuální požadavek uživatele měl za cíl vytvoření WSDL dokumentu a pokud ano, je volána metoda addDocumentation.

Metoda addDocumentation nejdříve získá odkaz na aktuální objekt HttpResponse a vytvoří novou instanci třídy XmlDocument s názvem doc. Poté si uloží odkaz na MemoryStream s WSDL dokumentem, který se nachází v našem objektu FilterStream nasazeném ve vlastnosti Filter objektu HttpResponse. MemoryStream je po nastavení pozice na začátek streamu nahrán do objektu XmlTextWriter, který je použit k inicializaci objektu doc. Dále je vytvořen XmlNamespaceManager a je do něj uložen název a prefix výchozího jmenného prostoru WSDL dokumentů, aby bylo možné provádět xpath dotazy pro výběr elementů v tomto jmenném prostoru. K vybrání všech operací ve WSDL dokumentu je použita metoda getOperations. XmlNodeList s operacemi je předán metodě writeDoc.Ta se pokusí nalézt pro každou operaci odpovídající popisek v hashovací kolekci m_operationsDoc a když je úspěšná, zavolá metodu createDocumentationNode a požádá ji o vytvoření nového elementu documentation, jehož obsah bude tvořen nalezeným popiskem. Nový element documentation je přidán pod značku operation tak, aby byl jejím prvním vnořeným uzlem.

Modifikovaný WSDL dokument metoda addDocumentation zapíše na výstup. Nejprve je původní výstup smazán voláním metody Clear objektu HttpResponse a poté je výsledek volání metody Save objektu doc s modifikovaným xml dokumentem poslán klientovi služby metodou HttpResponse.Write.

Nyní můžeme vytvořený http modul zkompilovat a zkopírovat do adresáře bin WWW služby. Nový http modul musíme u www služby zaregistrovat a také zadat popisky, které budou zobrazeny u metod (operací) WWW služby.

<configuration>
   <appSettings>
     <add key=“HelloWorldMethod“ value=“Dokumentace k metodě HelloWorld“/>
     <add key=“HelloWorld2Method“ value=“Dokumentace k metodě HelloWorld2″/>
   </appSettings>
  <system.web>
     <httpModules>
       <add name=“AddDocumentation“ type=“Interval.Examples.HttpModules.DocumentationModule, Interval.Examples.AddDocumentationModule“/>
     </httpModules>
   </system.web>
</configuration>

V sekci appSettings jsou uloženy dva popisky, jeden je pro metodu HelloWorld a druhý pro metodu HelloWorld2. Všimněte si, že jejich názvy v atributu key musí mít sufix Method, jinak je bude http modul ignorovat.

V některém z příštích článků upozorním na úskalí při psaní http modulů a budu demonstrovat zřetězení více http modulů.

Odkazy a zdroje

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Předchozí článek Jak začít s WiFi
Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *