Thomas Bandt

Über mich | Kontakt | Archiv

Der beste Weg nach Rom - URL Rewriting mit ASP.NET (2.0)

Update: The solution for URL Rewriting with ASP.NET 2.0 is released: http://www.urlrewriting.net/!

Update: Erweiterungen bzw. Verbesserungen dazu finden sich in den Kommentaren.

Es war ein harter Kampf, aber ich scheine ihn gewonnen zu haben. Ich habe mich in den letzten Monaten immer wieder mal mit URL Rewriting unter ASP.NET 2.0 beschäftigt, und bin jedes Mal an Grenzen gestoßen, die ich dann je nach Anwendungsfall ganz pragmatisch umschifft habe. Jetzt hatte ich aber die Nase voll und wollte endlich "die Richtige" Lösung finden.

Die Anforderungen:

Auf den ersten Blick könnte jetzt die Frage aufkommen, was diese vielen Sachen denn überhaupt miteinander zu tun haben - das wird sich sicherlich gleich aufklären ...

Die Lösungs-Ansätze:

Es gibt prinzipiell wie immer 1000 Wege die nach Rom führen, aber meiner Meinung nach nur zwei wirklich durchdachte bzw. ausgereifte, auf die ich mich konzentriert habe.

ASP.NET HTTP module for URL redirections von Fabrice

Eine bewährte und sehr komfortable Lösung, denn mit ihr kann man einfach beliebige Patterns in der Web.Config definieren und ist so völlig flexibel was das Umschreiben seiner URLs anbelangt:

"Madgeek.Web.ConfigRedirections, Madgeek.RedirectModule">
   "true"
         targetUrl="^~/FalseTarget.aspx"
         destinationUrl="~/RealTarget.aspx" />
   "true"
         targetUrl="^~/2ndFalseTarget.aspx"
         destinationUrl="~/RealTarget.aspx" />
            targetUrl="^~/(Author|Category|Tool)([A-Za-z0\d]{8}-?[A-Za-z\d]{4}-?[A-Za-z\d]{4}-?[A-Za-z\d]{4}-?[A-Za-z\d]{12}).aspx$"
         destinationUrl="~/Pages/$1.aspx?$1=$2" />
            targetUrl="^~/SomeDir/(.*).aspx\??(.*)"
         destinationUrl="~/Pages/$1/Default.aspx?$2" />

Vorteile:

Nachteile:

Rewriting the URL using IHttpHandlerFactory von Jeff

Hier handelt es sich nicht wirklich um eine fertige URL-Rewriting-Implementierung, sondern viel mehr um ein Toolkit, mit dem sich die Unzulänglichkeiten der ersten Lösung umschiffen lassen:

using System;
using System.IO;
using System.Web;
using System.Web.UI;

public class MyPageFactory : IHttpHandlerFactory
{
   public IHttpHandler GetHandler(HttpContext context, string requestType, 
   string url, string pathTranslated)
   {
      context.Items["fileName"] = Path.GetFileNameWithoutExtension(url).ToLower();
      return PageParser.GetCompiledPageInstance(url, 
      context.Server.MapPath("~/Content.aspx"), context);
   }
   public void ReleaseHandler(IHttpHandler handler)
   {
   }
}

Was passiert hier? Eigentlich zu schön und einfach um wahr zu sein: es wird die aktuelle Anfage hergenommen, deren URL auch überall verwendet wird (Action-Attribut im Formtag, Referenzierung für Links, Themes & Co.), aber auf eine andere physische Datei umgeleitet. Die konkrete Implementierung kann man sich dann innerhalb der GetHandler-Methode selbst schaffen, man ist quasi völlig flexibel.

Vorteile:

Nachteile:

Die Lösung

Nachdem ich Stunden meines Lebens damit scheinbar vergeudet habe, eine Lösung für das Forms-Authentication-Problem von Variante 2 zu finden, habe ich dann doch noch einmal die erste Variante ausgepackt und versucht deren Unzulänglichkeiten zu beheben. Mit Erfolg.

Der eigentliche Knackpunkt hier liegt ja in der nicht umgeschriebenen URL im Action-Attribut des Form-Tags sowie der nicht funktionierenden Referenzierung von z.B. Themes. Die Lösung hierfür liefert K. Scott Allen in seinem Blog-Eintrag "The Passion and the Fury of URL Rewriting":

protected override void OnPreInit(EventArgs e)
{
   base.OnPreInit(e);
   HttpContext.Current.RewritePath(HttpContext.Current.Items["VirtualUrl"].ToString(), string.Empty, string.Empty, true);
}

Mittels RewritePath lässt sich die URL für den aktuellen Aufruf wieder so umschreiben, wie man sie haben möchte. Das bedeutet:

Den Aufruf hierfür platziert man am besten in einer gemeinsamen Basisklasse, von der man die Seiten erben lässt, die vom URL-Rewriting in der Applikation betroffen sind. Am liebsten hätte ich das natürlich in einem Aufwasch mit dem Rewriting selbst erledigt, aber das wollte leider nicht so wie ich wollte - und ich denke mit der Basisklassen-Lösung kann man durchaus leben, da man so auch Seiten explizit von dem Prozess ausnehmen kann, für die man beispielsweise kein URL-Rewriting benötigt.

Einen letzten kleinen Wermutstropfen gibt es dennoch, für den ich noch keine Lösung finden konnte:

Platziert man z.B. in einer Umgebung "/de/Default.aspx" ein HyperLink-Control das auf eine Datei im gleichen "Verzeichnis" verweisen soll, dann wird dieser Verweis immer aufs Root abgebildet, also auf "/Seite.aspx", und nicht auf "/de/Seite.aspx". Aber für diesen Fall kann man ja getrost "normale" nicht-serverseitige Links setzen, und bei allen anderen Fällen wo man den Pfad relativ zusammenbauen will kann man wieder auf die serverseitige Variante zurückgreifen. Ich habe leider kein passendes Event gefunden, in das man RewritePath() hätte platzieren können, und mit dem dann sowohl die Links als auch die restlichen Elemente einer Seite funktioniert hätten.

Fazit:

Man kann doch (fast) alles haben, man muss sich nur etwas anstrengen. Ich bin überzeugt, dass die Kombination aus Maurice' URL-Rewriting-Lösung und RewritePath() eine der besten Varianten zum URL Rewriting mit ASP.NET ist, und ich werde sie demnächst häufiger einsetzen und dann auch sehen ob sie sich in der Praxis so bewährt, wie ich es mir vorstelle.

Download:

Anbei ein kleines Demo-Projekt mit dem ich alle möglichen Fälle die mir eingefallen sind ausprobiere und abfrage (.NET 2.0).

UrlRewriteTest.zip (10,33 KB)

Kommentare

  1. Albert Weinert schrieb am Samstag, 7. Januar 2006 18:07:00 Uhr:

    Danke!

    Gerade habe ich nach einer Rewriting Lösung gesucht, ich denke darauf kann ich auch aufbauen.

    Ist der base.OnPreRender() aufruf innerhalb von OnPreInit() beabsichtigt?
  2. Thomas schrieb am Samstag, 7. Januar 2006 18:10:00 Uhr:

    Nein natürlich nicht, ist ein Copy&Paste-Fehler, weil ich das nachträglich noch vom PreRender ins PreInit befördert habe ... sorry.

    Und gib mir Bescheid wenn du deine Lösung fertig hast, würde mich interessieren auf was für evtl. Probleme du so stößt.
  3. Albert Weinert schrieb am Samstag, 7. Januar 2006 18:32:00 Uhr:

    Mache ich, jedoch werden bei mir "nur" optionale Forms Authentication sowie der Rewrite von virtuellen Seiten zwecks einfachen CMS.

    Ausnahmnen über ein rewrite regel sind auch ohne Problem möglich. Scheint wirklich brauchbar zu sein :)

    Schön ist auch das ein Umleitung von .php :) sehr einfach möglich ist. Da ich gerade diese Webseite von PHP auf ASP.NET umstelle. Und ohne dies wäre es nicht so prickelnd.

    Muss nun erstmal weiter an meinen XmlMembershipProvider und XmlRoleProvider, für die kleine Sicherheit für zwischendurch ohne Datenbank, basteln. Keine Lust für 3 Benutzer und 2 Rollen eine Datenbank anzulegen.
  4. Thomas schrieb am Samstag, 7. Januar 2006 18:51:00 Uhr:

    jo, das ist richtig - aber mit dem membershipprovider kann man das ja relativ schnell erledigen, siehe blogeintrag kürzlich. 2.0 bringt schon ein paar wirklich coole sachen mit, die einem das leben leichter machen ;-)
  5. Albert Weinert schrieb am Samstag, 7. Januar 2006 21:21:00 Uhr:

    Das aufwändigste waren nicht die Provider (waren nicht meine ersten) sondern die ConfigSectionen in der web.config, will man es sauber haben tippt man sich den Wolf. Es gibt zwar einen Generator aber das entsprach nicht dem was ich haben wollte.
  6. Albert Weinert schrieb am Samstag, 7. Januar 2006 23:08:00 Uhr:

    So, ein kleines Problem Entdeckt übergibt man der Seite noch einen Querystring als Parameter

    http://localhost/web/test/ding.aspx?sdf=238&hdain

    Dann schläge OnPreInit() fehl. Da versucht wird inkl. des QueryString zu rewriten (Fehlermeldung entfallen).

    Hiermit geht's auch mit Querystring.

    protected override void OnPreInit(EventArgs e)
    {

    base.OnPreInit(e);
    if (HttpContext.Current.Items["VirtualUrl"] != null)
    {
    string queryString = string.Empty;
    string url = HttpContext.Current.Items["VirtualUrl"].ToString();
    if (Request.QueryString.Count > 0)
    {
    url = url.Substring(0, url.IndexOf('?'));
    queryString = Request.QueryString.ToString();
    }
    HttpContext.Current.RewritePath(url, string.Empty, queryString, true);
    }
    }
  7. Thomas schrieb am Sonntag, 8. Januar 2006 00:04:00 Uhr:

    Sehr schön, das hätte man eigentlich schon vorher abfangen können, bereits beim Rewrite-Module. Andererseits hat man so den Querystring schön zusätzlich mit ... also, eigentlich richtig gut :)

    Werde ich erweitern.
  8. Thomas schrieb am Sonntag, 8. Januar 2006 22:44:00 Uhr:

    Update:

    OnPreInit ist Mumpitz, das (soweit getestet) frühest mögliche Event ist OnLoad - andernfalls kommen die QueryStrings "nicht durch".


« Zurück  |  Weiter »