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:
- Es sollte möglich sein, beliebige URLs zu simulieren, insbesondere aber welche mit Verzeichnissen. Zum Beispiel sollte man Default.aspx?Id=1 in /Id/1/Default.aspx umschreiben können.
- Postbacks müssen funktionieren und dabei muss die umgeschriebene URL erhalten bleiben. Das heißt aus /Id/1/Default.aspx darf nach einem Postback kein /Default.aspx?Id=1 werden.
- Annehmlichkeiten wie Themes und Lokale Ressourcen müssen weiterhin ohne selbstgestrickte Bastellösungen, die nachher kein Mensch mehr warten kann, funktionieren.
- Forms Authentication muss funktionieren.
- Es sollte alles mit ASP.NET-Bordmitteln funktionieren. Das heißt, ich möchte keine externe Komponente zukaufen und ich möchte auch am Webserver nichts separat konfigurieren müssen.
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:
- Quellcode liegt vor
- Sehr flexibel
- Sehr schnell
Nachteile:
- ASP.NET 2.0 Themes usw. funktionieren nicht mehr, sobald man "virtuelle Ordner" verwendet. Denn die werden immer vom aktuellen Punkt aus referenziert. Beispiel: Detail.aspx wird zu /Detail/Default.aspx -> Dann wird App_Themes in /Detail/App_Themes gesucht, anstatt im Root.
- Postback-Problem 1: selbst wenn man im Root bleibt, hat man das Problem, dass das URL-Rewriting nach einem Postback nicht beibehalten wird, wie oben beschrieben wird dann aus Default_1.aspx wieder /Default.aspx?Id=1 usw.
- Postback-Problem 2: Verwendet man Ordner in der URL, gehen Postbacks gar nicht mehr. Das liegt am gleichen Problem wie es schon bei Themes auftritt: aus /FakeDirectory/Default.aspx wird dann /FakeDirectory/Default.aspx?Parameter=FakeDirectory ...
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:
- Quellcode liegt halbwegs gut dokumentiert vor.
- Postbacks funktionieren.
Nachteile:
- Nicht die schnellste Lösung (Gedenksekunde beim ersten Aufruf ...)
- Forms Authentication funktioniert nicht.
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:
- Funktionierende Postbacks auch mit Verzeichnissen in der umgeschrieben URL
- Ebenfalls funktionierende Themes, lokale Ressourcen usw.
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)