URL Rewriting mit ASP.NET

Veröffentlicht: 06. Dez 2002 | Aktualisiert: 21. Jun 2004
Von Patrick A. Lorenz

Kaum eine moderne Web-Applikation kommt ohne Datenbank aus. Dies gilt insbesondere, wenn die Website auf Basis von Technologien wie ASP.NET entwickelt wurde. Viele Seiten werden gar komplett mit Hilfe einer Vorlage und den just anzuzeigenden Inhalten dynamisch generiert. Problematisch ist das im Prinzip nicht, doch fällt es dem geübten Auge schnell auf und verhindert oft die Listung in Suchmaschinen. Warum das so ist und was Sie dagegen tun können, verrät dieser Artikel.

Auf dieser Seite

URL Rewriting URL Rewriting
URL Rewriting mit ASP.NET URL Rewriting mit ASP.NET
Ein allgemein gültiges Rewriting Modul Ein allgemein gültiges Rewriting Modul
Konfiguration mittels web.config Konfiguration mittels web.config
Auswertung der regulären Ausdrücke Auswertung der regulären Ausdrücke
Putting it all together Putting it all together
Fazit Fazit

Bestimmt haben Sie auch schon einmal eine Seite entwickelt, deren Aufgabe die Visualisierung von einzelnen Datensätzen war. Beispiele hierfür gibt es wie Sand am Meer - angefangen von einer einfachen Link-Sammlungen, über eine FAQ, bis hin zu einem komplexen Shop-System, bei dem die einzelnen Artikelinformationen in einer Datenbank hinterlegt sind.
Dynamisch generierte Seiten haben oft eine wesentliche Gemeinsamkeit, die dem geübten Betrachter sofort ins Auge fällt: die URL. Da der anzuzeigende Datenbankinhalt nicht im Namen der Seite stecken kann, wird dafür der Query-String verwendet. Meistens wird hier der Primärschlüssel des Datensatzes übergeben. Dies resultiert in Adressen wie etwa der folgenden: http://www.domain.tld/seite.aspx?id=4174

Das kommt Ihnen bekannt vor, oder? Aber so richtig schön finden Sie es doch eigentlich auch nicht!? Neben einem nicht befriedigten ästhetischen Anspruch bergen derartige Internet-Adresse aber ein echtes Problem. Auch die Betreiber von Suchmaschinen wie Google haben gelernt, dass es sich bei derartigen Adressen meist um dynamisch generierte Seiten handelt. Um einen Missbruch ihrer Maschinen zu verhindern, werden Seiten mit einem Query-String oftmals bei der Indizierung einer Internet-Präsenz ignoriert, oder die so wichtigen Parameter einfach abgeschnitten. Folgerichtig verringert sich die Wahrscheinlichkeit, dass Sie bei Eingabe relevanter Schlagworte tatsächlich gefunden werden.

URL Rewriting

Die Lösung für das offensichtliche Problem lautet URL Rewriting. Dieser imponierende Name bezeichnet aber eine doch recht simple Technik. Dem Benutzer werden zum Beispiel in Form von Links virtuelle Seiten-Adresse zugetragen. Diese enthalten die zur Identifikation benötigte Datensatz-ID direkt im Namen. Aus der oben gezeigten Adresse könnte so folgende werden: http://www.domain.tld/seite4174.aspx
Natürlich existiert nicht für jeden Datensatz eine eigene Seite, denn das würde den Sinn und Zweck der dynamischen Generierung stark aufweichen. Für den Client und somit auch die Spider von Suchmaschinen sieht es jedoch so aus. Auf dem Server wird aus dieser Adresse wieder die oben gezeigte ermittelt und die damit verknüpfte Seite ausgeführt. Die Adresse wird also umgeschrieben - URL Rewriting. Der Client bekommt von diesem Eingriff nichts mit.
Die Technik des URL Rewritings ist nicht wirklich neu, ganz im Gegenteil. Bei Apache-Servern ist sie gerade unter Linux sehr verbreitet. Auch in Verbindung mit den Internet Information Services (IIS) und beispielsweise dem klassischen ASP lässt sich der Ansatz zu Nutze machen. Zum Einsatz kommt hier ein ISAPI Filter, der die Adressen verändert. ASP.NET selbst bietet eine weitere Möglichkeit, die ohne zusätzliche Komponente eines Drittherstellers auskommt.

URL Rewriting mit ASP.NET

Mit ASP.NET ist es ziemlich einfach, eine Adresse umzuleiten - eigentlich kein Wunder. Es existiert eine Methode HttpContext.RewritePath, über die der aktuellen Anforderung eine neue Adresse auf dem lokalen Server zugewiesen werden kann. Es stellt sich nur noch die Frage, wo diese Methode aufgerufen werden kann. Es bietet sich die globale Datei global.asax an. Hier lassen sich auf einfache Weise die verschiedensten Ereignisse der Klasse HttpApplication behandeln. Das Ereignis BeginRequest wird beispielsweise zu Beginn jeder (!) URL-Anforderung ausgeführt - ein optimaler Kandidat für das Rewriting.

Das folgende Beispiel zeigt eine mögliche Implementierung. Innerhalb der Ereignisbehandlung wird die angeforderte URL überprüft. Beginnt diese mit dem Wort "seite", wird die anschließend notierte ID extrahiert und mittels Rewriting an die tatsächliche seite.aspx übergeben:

<% @Application Language="C#" %>
<script runat="server">
void Application_BeginRequest(object sender, EventArgs e) {
 string page = Request.Url.Segments[Request.Url.Segments.Length - 1];
 if(page.ToLower().StartsWith("seite")) {
  string id = page.Substring(5, page.Length - 10);
  Context.RewritePath(string.Format("seite.aspx?id={0}", id));
 }
}
</script>

Einen Schönheitspreis für gelungene Zeichenkettenoperationen kann dieses Beispiel aber mitnichten gewinnen und dennoch: es funktioniert. Ruft man die physikalisch nicht existierende seite4174.aspx auf, so erhält man das Ergebnis der seite.aspx, die zum Test lediglich die im Query-String übergebene ID über ein Label-Control visualisiert. Abbildung 1 zeigt das Beispiel im Einsatz.

Bild01

Abbildung 1: Die angeforderte Adresse wird auf dem Server umgeschrieben.

Ein allgemein gültiges Rewriting Modul

Das zuvor gezeigte Beispiel funktioniert zwar tadellos, aber so richtig schön ist es nun auch wieder nicht. Es bietet sich vielmehr an, eine derartige Aufgabe einem allgemein formulierten Modul zu überlassen, dass sich projektübergreifend wiederverwenden lässt. Die Definition von einer oder mehreren Umleitungen könnte über reguläre Ausdrücke geschehen. Damit diese nicht im Quelltext hart kodiert werden müssen, könnte stattdessen die web.config genutzt werden.

Um die Zielvorstellung umzusetzen, müssen verschiedene Techniken miteinander kombiniert werden:

  • Entwicklung eines HttpModul

  • Verwendung individueller Konfigurationsabschnitte in der web.config

  • Reguläre Ausdrücke

  • Und natürlich das URL Rewriting an sich

Ein HttpModul ist heute das, was früher ein ISAPI Filter war. Das oder die installierten Module laufen parallel zu jeder Client-Anforderung mit und haben die Möglichkeit, in diese einzugreifen, wann immer ihnen dies notwendig erscheint. Ganz konkret macht ein HttpModul in aller Regel nichts anderes, als ausgewählte Ereignisse der Klasse HttpApplication zu überwachen und im Kontext des Moduls zu behandeln.

Ein Modul wird mittels Unterstützung der Schnittstelle IHttpModule realisiert, die zwei Methoden - Init und Dispose - definiert. Während Dispose wie üblich der Freigabe von Ressourcen dient, wird der Methode Init die aktuelle Instanz der Klasse HttpApplication übergeben. Diese Instanz kann beispielsweise genutzt werden, um Ereignisbehandlungen zuzuweisen.

Der Grundaufbau vieler Module sieht in etwa wie nachfolgend gezeigt aus. In diesem Fall wird das beschriebene Ereignis BeginRequest behandelt.

public class RegexRewriteModule : IHttpModule {
 private HttpApplication application;
 public void Init(HttpApplication application) {
  this.application = application;
  this.application.BeginRequest += new EventHandler(this.Application_BeginRequest);
 }
 public void Dispose() {}
 protected void Application_BeginRequest(object sender, EventArgs e) {
  // tu was
 }
}

Die Instanziierung des Http-Moduls sowie Aufruf der Methode Init erfolgen beim Start der Web-Applikationen. Fortan bleibt die Instanz im Speicher erhalten und kann alle eingehenden Anforderungen bearbeiten.
Weitere Informationen zu Http-Modulen finden Sie beispielsweise auch in [1] und [2].

Konfiguration mittels web.config

Die web.config kommt aus zweierlei Gründen zum Einsatz. Zunächst einmal muss hier das neu entwickelte Modul quasi registriert werden. Dies geschieht innerhalb des Abschnitts durch Angabe des vollen Klassennamens samt Namespace und per Komma davon getrennt dem Namen der Assembly, die im bin-Verzeichnis abgelegt ist.

<configuration>
 <system.web>
  <httpModules>
   <add name="RewriteModule" type="PAL.Fever.Web.RegexRewriteModule.RegexRewriteModule, 
   PAL.Fever.Web.RegexRewriteModule" />
  </httpModules>
 </system.web>

</configuration>

Auf diese Weise lässt sich das Modul "scharf" machen. Allerdings fehlen noch die notwendigen Konfigurationsdaten für das Modul selbst und die damit verbundenen Regeln. Hierfür existiert naturgemäß kein vorgegebener Konfigurationsabschnitt und so gilt es, diesen selbst zu realisieren. Mit Hilfe der Schnittstelle IConfigurationSectionHandler und der einzigen definierten Methode Create ist das kein echtes Problem. Der Methode wird im Wesentlichen einer Instanz der Klasse XmlNode übergeben, die Zugriff auf den hinterlegten Konfigurationsabschnitt bietet. Aufgabe des Handlers ist es nun, das XML-Element in ein, wie auch immer geartetes, Objektmodell zu transferieren und dieses als Rückgabewert der Methode zu liefern. Da als Datentyp object angegeben ist, lässt sich der Ansatz universell nutzen.

Im vorliegenden Fall soll der Konfigurationsabschnitt die beschriebenen Regeln in Form von regulären Ausdrücken auslesen. Die Konfiguration sieht beispielsweise so aus:

<fever>
 <regexRewriteModule>
  <rules>
   <add matchPattern="seite(\d+)\.aspx"  rewritePath="seite.aspx?id={1}" 
   rewritePattern="seite(\d+)\.aspx"/>
  </rules>
 </regexRewriteModule>
</fever>

Dieser Aufbau ist durch den Handler vorgegeben und wird von diesem genutzt, um die Daten in ein Objektmodell umzuwandeln. Dieses besteht aus einer von CollectionBase abgeleiteten RewriteRuleCollection sowie den einzelnen Regeln, die von der Klasse RewriteRule repräsentiert werden. Die Klasse erwartet im Konstruktor die Übergabe von drei Zeichenketten: matchPattern, rewritePath und rewritePattern. Diese sind analog in der Konfigurationsdatei hinterlegt und werden von dort übernommen.

public class RewriteRulesConfigurationSectionHandler : IConfigurationSectionHandler {
 public object Create(object parent, object context, XmlNode section) {
  RewriteRuleCollection rules = new RewriteRuleCollection(parent as RewriteRuleCollection);
  XmlNodeList nodes = section.SelectNodes("rules/add");
  foreach(XmlNode node in nodes) {
   rules.Add(new RewriteRule(node.Attributes["matchPattern"].Value, 
   node.Attributes["rewritePath"].Value, node.Attributes["rewritePattern"].Value));
  }
  return(rules);
 }
}

Damit der neue Konfigurationsabschnitt genutzt werden kann, muss auch dieser erst einmal konfiguriert werden. Wieder einmal hilft ein kleiner Eintrag in der web.config.

<configSections>
 <sectionGroup name="fever">
  <section name="regexRewriteModule" 
   type="PAL.Fever.Web.RegexRewriteModule.RewriteRulesConfigurationSectionHandler,
   PAL.Fever.Web.RegexRewriteModule"/>
 </sectionGroup>
</configSections>

Der Zugriff auf das Objektmodell kann prinzipiell an beliebiger Stelle im Quelltext durch Aufruf der Methode ConfigurationSettings.GetConfig("fever/regexRewriteModule") erfolgen. Da der Rückgabewert mit object angegeben ist, muss lediglich eine Typenkonvertierung auf die zurückgegebene RewriteRuleCollection durchgeführt werden. Zur Optimierung werden die Konfigurationsdaten vom Framework gecachet, so dass lediglich beim ersten Aufruf die web.config eingelesen und der RewriteRulesConfigurationSectionHandler ausgewertet werden muss. Im konkreten Fall geschieht dies innerhalb des Http-Moduls, wie weiter unten zu sehen ist.

Auswertung der regulären Ausdrücke

Nachdem alle Daten als Objektmodell zur Verfügung stehen, gilt es diese auch zu benutzen. Die zunächst nur als Datenspeicher benutze Klasse RewriteRule wird dazu mit zwei Methoden ausgerüstet. IsMatch soll überprüfen, ob die zugewiesene Regel auf die aktuelle URL-Anforderung zutrifft. Dies geschieht intern über die gleichnamige statische Methode der Klasse Regex und sieht wie folgt aus:

public bool IsMatch(Uri uri) {
 return Regex.IsMatch(uri.AbsolutePath,
 this.matchPattern, RegexOptions.Compiled | RegexOptions.IgnoreCase);
}

Die als Flag-Enumeration übergebenen Optionen bewirken unter anderem, dass der reguläre Ausdruck kompiliert wird. Dies erfordert beim ersten Aufruf etwas mehr Zeit, dafür sind alle nachfolgenden umso schneller. Das ist wichtig, da später jede einzelne Client-Anforderung diese Stelle durchlaufen wird.

Für den Fall, dass die Regel auf die aktuelle Anforderung zutrifft, der reguläre Ausdruck also passt, existiert eine zweite Methode. Diese ermittelt aus der angeforderten virtuellen Adresse die zugehörige physikalische. Dazu wird ein weiterer regulärer Ausdruck in Verbindung mit einer Formatierungsanweisung genutzt. Letztere wird mittels string.Format ausgewertet und bietet in der üblichen Notation Zugriff auf die vom Ausdruck gelieferten Gruppierungsergebnisse.

public string GetRewriteUrl(Uri uri) {
 if(rewritePattern != null) {
  Match match    = Regex.Match(uri.AbsolutePath, this.rewritePattern, 
  RegexOptions.Compiled | RegexOptions.IgnoreCase);
  Group[] groups = new Group[match.Groups.Count];
  match.Groups.CopyTo(groups, 0);
  return string.Format(this.rewritePath, groups);
 }
 else {
  return this.rewritePath;
 }
}

Die Verwendung von regulären Ausdrücken im Rahmen von .NET ist beispielsweise in [3] beschrieben.

Putting it all together

Die Arbeit ist fast erledigt, lediglich die einzelnen Klassen müssen noch zusammengefasst werden. Dies geschieht innerhalb des RegexRewriteModules, von dem zuvor nur der Rumpf gezeigt wurde. Die Ereignisbehandlung von Application.BeginRequest lädt die konfigurierte RewriteRuleCollection und durchläuft alle hinterlegten Regeln. Trifft einer der Ausdrücke zu, wird der Aufruf umgeleitet.

protected void Application_BeginRequest(object sender, EventArgs e) {
 RewriteRuleCollection rules = (RewriteRuleCollection) 
 ConfigurationSettings.GetConfig("fever/regexRewriteModule");
 foreach(RewriteRule rule in rules) {
  if(rule.IsMatch(application.Request.Url)) {
   application.Context.RewritePath(rule.GetRewriteUrl(application.Request.Url));
   break;
  }
 }
}

Zur Demonstration des neuen Moduls dient erneut die Seite seite.aspx, der eine ID übergeben werden soll. Die passende Regel sieht so aus:

<add matchPattern="seite(\d+)\.aspx"  rewritePath="seite.aspx?id={1}" 
rewritePattern="seite(\d+)\.aspx"/>

Der reguläre Ausdruck überprüft die angeforderte Adresse auf das Vorkommen der Zeichenkette "seite" gefolgt von einer oder mehreren Ziffern gefolgt von der Zeichenkette ".aspx". Trifft der Ausdruck zu, erfolgt eine Umleitung auf seite.aspx. Im Ergebnis unterscheidet sich das Beispiel also nicht von der ersten Implementierung innerhalb der global.asax, lediglich der Ansatz ist etwas universeller geworden.

Die Verwendung von "{1}" als Referenz mag auf den ersten Blick ungewöhnlich anmuten, klärt sich aber schnell auf: Da die implizite Aufnahme von Teilsuchtreffern nicht explizit ausgeschaltet wurde (Option RegexOptions.ExplicitCapture), enthält der erste Eintrag der Collection mit dem Index 0 den vollständigen Suchtreffer, in diesem Fall also die gesamte URL.

Fazit

Mit Konzentration auf das Umleiten von Adressen hat dieser Artikel die praxisgerechte Kombination mehrerer Techniken gezeigt. Jede davon lässt sich auch in anderen Zusammenhängen sinnvoll einsetzen. Das entstandene HttpModul lässt sich projektübergreifend nutzen, um dynamischen Datenbankinhalten virtuelle Dateinamen zuzuweisen. Dies ermöglicht die Verwendung von "schönen" URLs, die es zudem schaffen, ihre dynamische Erzeugungen vor Suchmaschinen zu verbergen.

Ressourcen
[1].NET Framework SDK-Dokumentation Schlagwort "IHttpModule"
[2]Patrick A. Lorenz, ASP.NET Grundlagen und Profiwissen, Hanser, 2002, ISBN 3-446-21943-9
[3]Patrick A. Lorenz, Visual C# .NET, Hanser, 2002, ISBN 3-446-22132-8


Anzeigen: