Sinnvolle Verwendung der global.asax

Veröffentlicht: 21. Jan 2003 | Aktualisiert: 09. Nov 2004

Von Karsten Samaschke

Die global.asax-Datei bietet vielfältige Einsatzmöglichkeiten für Entwickler. Besonders die beiden Events Application_BeginRequest, Application_EndRequest und Application_Error sollen an dieser Stelle im Blickpunkt des Interesses stehen. Einige sinnvolle Ansätze für die Arbeit mit globalen Ereignissen innerhalb Ihrer Applikation sollen im Folgenden kurz vorgestellt werden.

Die global.asax-Datei erlaubt Reaktionen auf applikationsweite Ereignisse. Die Datei liegt stets im Wurzelverzeichnis einer Web-Applikation. Die Ereignisbehandlungsmethoden können dabei in eine CodeBehind-Datei ausgelagert werden, die dann sinnvollerweise den Namen global.asax.vb für VB.NET bzw. global.asax.cs für C# trägt. Innerhalb der global.asax können selbstverständlich auch Handler für Reaktionen auf von eigenen Modulen erzeugte Ereignisse definiert werden, so dass Sie bei sinnvollem Einsatz eine Art Werkzeugkiste für Ihre Applikation bereitstellen können.

Auf dieser Seite

 Fehlerbehandlung
 Wie oft wurde eine Seite bereits aufgerufen?
 Wie viele Benutzer sind online?
 URL-Rewriting
 Statistiktool
 Fazit

Bb979330.8fb4fb58-1ee0-4719-bf87-956618fb212b(de-de,MSDN.10).gif

Diesen Artikel können Sie dank freundlicher Unterstützung von ASP .NET professional auf MSDN Online lesen. ASP .NET professional ist ein Partner von MSDN Online.

Bb979330.f3166d26-b194-40c6-9ef2-d3b3a0c40f1a(de-de,MSDN.10).gif

Fehlerbehandlung

Die global.asax-Datei bietet sich für die Fehlerbehandlung geradezu an, da sie als letzte Instanz vor der Ausgabe einer Fehlermeldung an den Nutzer fungiert. Sie können an dieser Stelle alle noch belegten Ressourcen wieder freigeben, auf eine Fehlerseite weiterleiten und somit dem Benutzer die Ansicht der Fehlermeldungen ersparen.

Die Fehlerbehandlung wird in der global.asax vorgenommen. Zu diesem Zweck könnte eine Email generiert, dem Webmaster zugesendet werden und anschließend auf eine Fehlerseite weitergeleitet werden:

protected void Application_Error(Object sender, EventArgs e) 
{ 
   StringBuilder theErrors = new StringBuilder(); 
   IEnumerator Enum = Context.AllErrors.GetEnumerator(); 
   while(Enum.MoveNext())  
   { 
   Exception current = (Exception)Enum.Current; 
   String theMessage = current.InnerException.Message; 
   if(theMessage != null)  
   theErrors.Append(theMessage + Environment.NewLine); 
   } 
   MailMessage mail = new MailMessage(); 
   mail.To = "<A href="mailto:errors@domain.tld">errors@domain.tld</A>"; 
   mail.From = "<A href="mailto:info@domain.tld">info@domain.tld</A>"; 
   mail.Subject = "Fehlermeldungen"; 
   mail.Body = "Folgende Fehler sind aufgetreten"  
   + Environment.NewLine  
   + theErrors.ToString(); 
   SmtpMail.Send(mail); 
   Context.ClearError(); 
   Server.Transfer("Errorhandler.aspx"); 
}

Beachten Sie, dass für den Text der Fehlermeldung die Message-Eigenschaft der inneren Exception ausgelesen wird. Würden Sie dies nicht tun, wären die in der E-Mail versandten Fehlermeldungen wenig aussagekräftig - es würde lediglich ausgegeben werden, dass eine unbehandelte Exception aufgetreten sei.

 

Wie oft wurde eine Seite bereits aufgerufen?

Bei einigen Webseiten findet man auf jeder Seite eine Übersicht, wie häufig diese aufgerufen worden ist. Selbstverständlich können Sie auf jeder Seite eine statische Variable einbinden, die darüber Auskunft gibt, wie oft bereits ein Abruf erfolgte. Mit Hilfe der global.asax können Sie dieses Problem aber global und eleganter lösen.

Folgendes Listing sorgt dafür, dass für jede Seite, die aufgerufen wird, ein eigener Zähler inkrementiert wird. Anschließend erfolgt ein Eintrag im Context-Objekt, der dann innerhalb der Seite nur noch ausgelesen werden muss:

private Hashtable pageCount = new Hashtable(); 
protected void Application_BeginRequest(Object sender, EventArgs e) 
{ 
   // ----------------------------------------- 
   // Absoluten Pfad bilden 
   // ----------------------------------------- 
   Uri requestUrl = Context.Request.Url; 
   String thePath = requestUrl.AbsolutePath; 
   // ----------------------------------------- 
   // Falls schon besucht, dann aus Pfadliste 
   // auslesen und Erhöhen,  
   // sonst Standardwert verwenden 
   // ----------------------------------------- 
   int theCount = 1; 
   if(pageCount.ContainsKey(thePath))  
   { 
   theCount = (int)pageCount[thePath]; 
   pageCount[thePath] = ++theCount; 
   } 
   else  
   pageCount.Add(thePath, theCount); 
   // ----------------------------------------- 
   // Wert für aktuelle Seite im Context speichern 
   // ----------------------------------------- 
   Context.Items.Add("PageAccessed", theCount); 
}

Im Beispiel wird zunächst der absolute Pfad der Anwendung gebildet. Anschließend wird überprüft, ob dieser Pfad schon in der Liste der abgerufenen Seiten enthalten ist - falls dies der Fall ist, wird der vorhandene Wert erhöht, anderenfalls ein neuer Eintrag in der Liste der abgerufenen Seiten hinzugefügt. Zuletzt wird die Anzahl der Zugriffe auf die aktuelle Seite in der Context.Items-Auflistung hinterlegt und somit der angesprungenen Seite zugänglich gemacht.

Das Auslesen der Information gestaltet sich nun denkbar einfach. Folgendes Codefragment liest den Wert aus dem Context aus und weißt ihn einem Label zu:

if(Context.Items["PageAccessed"] != null)  
{ 
   int theCount = (int)Context.Items["PageAccessed"]; 
   Label1.Text = theCount.ToString(); 
}

Sinnvollerweise sollten Sie diesen Vorgang bei häufiger Anwendung in ein Usercontrol oder ein Customcontrol auslagern.

 

Wie viele Benutzer sind online?

Immer wieder wird danach gefragt, ob und wie man zählen könne, wie viele Benutzer momentan auf einer Webseite surfen. Eigentlich ist die Antwort darauf klar: Es geht nicht, da das HTTP-Protokoll zustandslos ist.

Nun, diese Aussage muss relativiert werden. Mit Hilfe der global.asax ist es sehr wohl möglich, die Anzahl der aktiven Sessions zu zählen - allerdings ist dies im besten Fall nur ein Näherungswert, da der Zähler zwar beim ersten Request eines Users erhöht, aber erst bei Beendigung der Session verringert wird.

Das Beenden einer Session könnte zwar anhand eines Buttons erfolgen, nur werden die wenigsten Benutzer freiwillig auf eine derartige Schaltfläche klicken. Somit bleibt nur, auf den SessionEnd-Event zu warten und dann den Zähler zu verringern. Dieser Event tritt nach Ablauf der vorgegebenen TimeOut-Zeit der Session ein, in der Standardeinstellung also nach zwanzig Minuten.

Folgender Code sorgt dafür, dass bei Beginn einer neuen Session ein Zähler erhöht und bei Ablauf der Session wieder verringert wird:

private int activeUsers = 0; 
protected void Session_Start(Object sender, EventArgs e) 
{ 
   activeUsers++; 
   Context.Items["activeUsers"] = activeUsers; 
} 
protected void Application_BeginRequest(Object sender, EventArgs e) 
{ 
   Context.Items.Add("activeUsers", activeUsers); 
} 
protected void Session_End(Object sender, EventArgs e) 
{ 
   if(activeUsers > 0) 
   activeUsers--; 
} 
protected void Application_End(Object sender, EventArgs e) 
{ 
   activeUsers = 0; 
}

Diesen in der Context-Auflistung enthaltenen Wert können Sie nunmehr auf jeder Seite - beispielsweise in einem Label - ausgeben lassen:

private void Page_Load(object sender, System.EventArgs e) 
{ 
   Label activeUsers = new Label(); 
   int activeUsersCount = (int)Context.Items["activeUsers"]; 
   activeUsers.Text += activeUsersCount.ToString(); 
   Controls.Add(activeUsers); 
}

Sinnvollerweise erledigen Sie diese Ausgabe - gerade wenn Sie einen derartigen Zähler auf mehreren Seiten einblenden lassen wollen - über ein User- oder Customcontrol, jedoch ist auch dort der Zugriff auf die in der Context-Auflistung enthaltenen Informationen wie im Beispiel gezeigt möglich.

Um die Aktualisierungsgeschwindkeit des Zählers zu erhöhen empfiehlt es sich, das Session-TimeOut besonders kurz einzustellen. Allerdings kann dieses Verhalten unangenehme Nebeneffekte haben, wenn Sie Benutzerinformationen - beispielsweise für eine Authentifizierung - in der Session zwischenspeichern: Wenn der Nutzer nicht schnell genug den nächsten Seitenabruf durchführt, verfällt seine Sitzung, und er müsste sich gegebenenfalls erneut an Ihrem System anmelden.

 

URL-Rewriting

Wenn Sie oft mit dynamischem Content arbeiten, werden Sie feststellen, dass einige Suchmaschinen Ihre Webseiten nicht indizieren wollen. Dies hängt primär damit zusammen, dass einige Robots mit den für dynamische Seiten typischen URLs nicht zurechtkommen und Links mit Parametern einfach nicht als Ziele akzeptieren. Derartige URLs sehen meist so aus: http://www.meine-seite.de/Applikation/Default.aspx?id=282\&subid=837 - dies ist auch für Menschen sicherlich nicht wirklich schön anzuschauen und noch schlechter zu merken.

Typische Lösungsansätze sehen in der Regel so aus, dass man bei den Suchmaschinen statisch wirkende URLs anmeldet. Diese beinhalten dann die gleichen Informationen wie die dynamischen URLs, verlinken allerdings nicht auf tatsächlich existierende Seiten. Der obige URL würde als statische Variante beispielsweise so aussehen: http://www.meine-seite.de/Applikation/Default.aspx/id/282/subid/837/. Derartige Adressen könnten von den Robots und Spidern der Suchmaschinen indiziert werden, allerdings hat die Sache einen gewaltigen Pferdefuß: Diese Adresse existiert überhaupt nicht.

Lösungsansätze für dieses Problem wären:

  • einen ISAPI-Filter einsetzen, der den Request umleitet

  • eine benutzerdefinierte Fehlerseite im IIS definieren, die den Request weiterleitet

Jeder dieser Ansätze hat eigene Nachteile aufzuweisen - so sollte man die Programmierung eines ISAPI-Filter tunlichst vermeiden, wenn man keine wirklich fundierten Kenntnisse in C++ oder Delphi hat. Die Variante mit der benutzerdefinierten Fehlerseite im IIS ist zwar vergleichsweise einfach zu programmieren, gehört aber sicherlich zu den langsamsten Lösungen, die man sich für ein derartiges Problem vorstellen kann.

Mit ASP.NET kommt ein weiterer Lösungsansatz für die Problemstellung hinzu: Verwenden Sie doch einfach die global.asax und URL-Rewriting, um die korrekten Parameter an die entsprechende Datei zu übergeben.

Das Charmante an diesem Ansatz ist, dass er applikationsabhängig arbeitet: Jede Applikation kann ihre eigene global.asax-Datei mitbringen, in der die URLs gegebenenfalls ausgewertet und umgeschrieben werden könnten.

Die eigentliche Auswertung der URL ist nicht wirklich aufwändig, allerdings ist an dieser Stelle unter anderem der Einsatz von regulären Ausdrücken und der Uri-Klasse notwendig:

using System; 
using System.Web; 
using System.Text; 
using System.Text.RegularExpressions; 
using System.Collections; 
namespace GobalAsax 
{ 
   public class UrlRewriter 
   { 
   HttpContext Context = null; 
   public UrlRewriter() 
   { 
   // ------------------------------------- 
   // Context-Objekt zuweisen 
   // ------------------------------------- 
   Context = HttpContext.Current; 
   } 
   public void Rewrite()  
   { 
   // ------------------------------------- 
   // URL des Requests auslesen - uns interessiert 
   // aber nur der Pfad 
   // ------------------------------------- 
   Uri theUrl = Context.Request.Url; 
   String absoluteUri = theUrl.AbsolutePath;  
   RegexOptions options = RegexOptions.IgnoreCase; 
   // ------------------------------------- 
   // Scriptnamen und Parameter ermitteln 
   // ------------------------------------- 
   Regex theReg = new Regex(@"([^\.]+\.[^/]+)/(.+)$", options); 
   if(theReg.Match(absoluteUri).Success)  
   { 
   ArrayList theNames = new ArrayList(); 
   ArrayList theValues = new ArrayList(); 
   // ----------------------------------------- 
   // Uri und Parameterliste auslesen 
   // ----------------------------------------- 
   String theUrlToConnect=theReg.Match(absoluteUri).Result("$1"); 
   String theParamList = theReg.Match(absoluteUri).Result("$2");  
   // ----------------------------------------- 
   // Anhand des Schrägstriches zerlegen, 
   // jeder gerade Eintrag im Array ist ein Wert 
   // jeder ungerade Eintrag ist ein Parametername 
   // ------------------------------------------ 
   String[] theParams = theParamList.Split('/'); 
   for(int current=0;current < theParams.Length;current++)  
   { 
   if(current % 2 == 0)  
   theNames.Add(theParams[current]); 
   else 
   theValues.Add(theParams[current]); 
   } 
   StringBuilder theRequestParams = new StringBuilder("?"); 
   for(int i=0;i<theNames.Count;i++)  
   { 
   // ---------------------------------- 
   // Element an Parameterliste anfügen 
   // ---------------------------------- 
   String val = (theValues.Count>i?(String)theValues[i] : ""); 
   theRequestParams.Append(theNames[i] + "="  
   + (Context.Server.UrlEncode(val))); 
   // ---------------------------------- 
   // Falls noch nicht letztes Element: 
   // Ampersand einfügen 
   // ---------------------------------- 
   if(i<theNames.Count - 1) 
   theRequestParams.Append("&"); 
   } 
   // ------------------------------------- 
   // Weiterleiten 
   // ------------------------------------- 
   Context.RewritePath(theUrlToConnect + theRequestParams); 
   } 
   } 
   } 
}

Das Geheimnis der Umwandlung der scheinbar statischen URL liegt darin, den angegebenen Dateinamen zu ermitteln und alle folgenden Elemente als Name-Wert-Paare zu begreifen. Steht der Dateiname als letztes Element in der URL, dann wird keine Umwandlung vorgenommen. Intern wird der Aufruf
http://www.meine-seite.de/Applikation/Default.aspx/id/282/subid/837/
also zu
http://www.meine-seite.de/Applikation/Default.aspx?id=282\&subid=837
umgeschrieben. Somit haben Sie eine einfache Möglichkeit, dynamische URLs bei Suchmaschinen anzumelden und Ihre Seiten dennoch für die Robots und Spiders dynamisch erscheinen zu lassen.

 

Statistiktool

Die global.asax ist ebenfalls für die Generierung von Statistiken einsetzbar. Sie ist in diesem Fall sogar ein mächtiges Werkzeug, da sie auf auftretende Fehler reagieren und entsprechende Meldungen im Logfile hinterlassen kann.

Das folgende Beispiel demonstriert den Einsatz der global.asax als Statistik-Tool. Die Statistik wird dabei sowohl beim Ende eines Requests als auch beim Auftreten eines Fehlers erfasst:

private void DoStats(bool Error)  
{ 
   // ------------------------------------------ 
   // Interessante Werte erfassen 
   // ------------------------------------------ 
   Uri theRequest = Context.Request.Url; 
   String theBrowser = Context.Request.UserAgent; 
   String theAccessedUri = theRequest.AbsoluteUri; 
   String theReferer = (Context.Request.UrlReferrer ==  
 null ? "" : Context.Request.UrlReferrer.ToString()); 
   String theIP = Context.Request.UserHostAddress; 
   String theHostname = Context.Request.UserHostName; 
   String theTime = DateTime.Now.ToString(); 
   String theStatus = (Error == false ? "erfolgreich" : "Fehler"); 
   // --------------------------------------- 
   // Daten in Datenbank erfassen 
   // --------------------------------------- 
   OleDbConnection theConn = new OleDbConnection( 
   "PROVIDER=Microsoft.Jet.OLEDB.4.0;Data Source=" +  
   Server.MapPath("stats.mdb")); 
   OleDbCommand theCommand = new OleDbCommand(); 
   theCommand.Connection = theConn; 
   theCommand.CommandText =  
   "INSERT INTO Statistik (Browser, Uri, Referer, IP, Hostname, Zeit, Status) " + 
   "VALUES (?,?,?,?,?,?,?)"; 
   OleDbParameterCollection theParams = theCommand.Parameters; 
   OleDbParameter param = new OleDbParameter(null, theBrowser); 
   theParams.Add(param); 
   param = new OleDbParameter(null, theAccessedUri); 
   theParams.Add(param); 
   param = new OleDbParameter(null, theReferer); 
   theParams.Add(param); 
   param = new OleDbParameter(null, theIP); 
   theParams.Add(param); 
   param = new OleDbParameter(null, theHostname); 
   theParams.Add(param); 
   param = new OleDbParameter(null, theTime); 
   theParams.Add(param); 
   param = new OleDbParameter(null, theStatus); 
   theParams.Add(param); 
   theCommand.Connection.Open(); 
   theCommand.ExecuteNonQuery(); 
   theCommand.Connection.Close(); 
} 
protected void Application_EndRequest(Object sender, EventArgs e) 
{ 
   DoStats(false); 
} 
protected void Application_Error(Object sender, EventArgs e) 
{ 
   DoStats(true); 
}

Um das Statistiktool zum Laufen zu bekommen, benötigen Sie nun lediglich eine Access-Datenbank namens "stats.mdb" in dem Wurzelverzeichnis der Anwendung. Diese sollte eine Tabelle "Statistik" mit den Feldern "Browser", "Uri", "Referer", "IP", "Hostname", "Zeit" und "Status" enthalten. Passen Sie eventuell noch den Pfad zur Datenbank an und schon haben Sie eine eigene Statistik. Diese können Sie gegebenenfalls noch um weitere für Sie interessante Informationen erweitern und somit aussagekräftiges Zahlenmaterial für die Auswertung der Zugriffe auf Ihre Site erhalten.

 

Fazit

Mit Hilfe der global.asax können Sie viele Aufgaben zentral an einer Stelle lösen. Sie können Fehler behandeln, Statistiken generieren, Benutzer zählen oder auch applikationsweite Werte setzen. Ein intensiverer Blick auf die global.asax lohnt sich also.