Tutoriel : Écrire un module d'Url Rewriting pour asp.net en C#

Nico-pyright(c)

Cet article a pour but de présenter les bases d'écriture d'un module d'url-rewriting.

N'hésitez pas à commenter cet article ! Commentez ♪

- Titre : Tutoriel : Écrire un module d'Url Rewriting pour asp.net en C#

- Auteur : Nico-pyright(c)

- Parution : 10 janvier 2008

- Mise à jour : 10 janvier 2008

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2007 Nico-pyright(c). Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- 1.Introduction

- 2.L'Url rewriting, qu'est-ce que c'est ?

- 3.Un exemple d'url rewriting simple

- 3.1.Rappels sur le global.asax

- 3.2.Réécriture

- 3.3.Exemple

- 4.Écrire un module d'url rewriting

- 4.1.Rappels sur un module Http

- 4.2.Une implémentation de module d'url rewriting

- 4.3.Exemple d'un module url rewriting

- 5.Amélioration, utiliser le web.config pour paramétrer le module

- 5.1.Créer sa section personnalisée

- 5.2.Gestion de l'url rewriting simple dans le module

- 5.3.Amélioration, utilisation d'expressions régulières

- 5.4.Un peu plus loin avec les expressions régulières

- 5.5.Finalisation du module

- 6.Url rewriting et postback

- 6.1.Surcharge du contrôle HtmlForm

- 6.2.Utiliser un contrôle adapté

- 7.Une méthode de pseudoréécriture d'url, utilisation de la propriété PathInfo

- 8.Configurer IIS pour l'url mapping

- 9.Conclusion

- Remerciements

- Contact

1.Introduction

Vous êtes un développeur Asp.Net utilisant le C# et vous souhaitez avoir des URL agréables à lire et faciles à utiliser ? Ou bien améliorer votre référencement ?
Alors ce tutoriel est pour vous.

Vous apprendrez dans ce cours comment utiliser l'Url-Rewriting en C# pour un site web en asp.net. Vous apprendrez également à écrire un module paramétrable de réécriture d'url dans le web.config. Vous saurez également comment gérer les postbacks avec l'url rewriting.

2.L'Url rewriting, qu'est-ce que c'est ?

L'url rewriting (réécriture d'url) est la capacité d'intercepter une url qui n'existe pas physiquement sur le serveur pour la transformer en une url existante pour notre site afin que celle-ci soit plus facile à lire ou à écrire pour les utilisateurs.
Exemple : Imaginons que nous ayons un site web d'e-commerce et que nous pouvions accéder à la fiche technique d'un disque dur de 200 Go qui a pour identifiant : XB45BTO de cette façon :

http://monsite/ficheProduit.aspx?id=XB45BTO

Cette url n'est pas très explicite en soi, il aurait surement été préférable de pouvoir y accéder ainsi :

http://monsite/Produits/Disque-dur/Marque-X-200GO.html

Cela a en effet plusieurs avantages. Dans un premier temps, c'est un confort pour l'utilisateur qui est mieux averti de la fiche produit qu'il va visiter. C'est ensuite plus facile à repérer sur les moteurs de recherche.
Techniquement, une url réécrite permet de masquer le fonctionnement interne du site web. Dans notre exemple, on apprend que le site est fait en asp.net et qu'on passe des identifiants de produit dans l'url.
Avoir une url plus explicite et plus descriptive permet également d'améliorer son référencement.
Cette technique permet aussi d'éviter les liens morts. Un utilisateur peut par exemple avoir bookmarké le lien. En cas de changement d'url (par exemple changement de la structure interne du site), le lien bookmarké renverra vers une page inexistante. L'url rewriting aurait pu nous éviter ce souci.

L'url rewriting c'est donc la capacité d'un site à réécrire l'url :

http://monsite/Produits/Disque-dur/Marque-X-200GO.html

en

http://monsite/ficheProduit.aspx?id=XB45BTO

3.Un exemple d'url rewriting simple

Le but est d'intercepter chaque url pour savoir s'il y a besoin de la réécrire pour qu'elle soit comprise par notre application. Nous devons donc être en mesure d'intercepter chaque demande de requête de notre application. C'est un des domaines que peut couvrir le global.asax.

3.1.Rappels sur le global.asax

Comme précisé dans la FAQ asp.net, la classe Global est la classe capable de gérer des événements du niveau application. Elle se trouve dans un fichier appelé Global.asax (plus précisément Global.asax.cs pour C#).
Msdn nous dit que l'événement BeginRequest signale la création de toute nouvelle demande donnée. Cet événement est toujours déclenché et constitue systématiquement le premier événement à se produire au cours du traitement d'une demande.
C'est donc dans cet événement que devra intervenir notre réécriture d'url.

Ajoutons un fichier global.asax à notre application et surchargeons cet événement : clic droit sur le projet -> add -> new item -> global application class

Il génère par défaut les événements Application_Start et Application_End. Nous pouvons les supprimer et ajouter l'événement Application_BeginRequest :

global.asax.cs

protected void Application_BeginRequest(object sender, EventArgs e)
{
}

3.2.Réécriture

Toute la mécanique de réécriture d'url d'asp.net repose sur la méthode : Context.RewritePath.

3.3.Exemple

Dans cet exemple, nous allons simplement chercher à transformer une url de ce type :

http://monsite/nico.html

en

http://monsite/Default.aspx?name=nico

La page Default.aspx récupérera le paramètre dans l'url et l'affichera dans un label.
Nous allons donc ajouter un label dans le fichier Default.aspx :

Default.aspx

<asp:Label ID="leLabel" runat="server" />

Et on aura dans le code-behind le code pour récupérer le paramètre name et l'afficher dans le label :

Default.aspx.cs

protected override void OnInit(EventArgs e)
{
string aValue = Request.QueryString["name"];
if (!string.IsNullOrEmpty(aValue))
leLabel.Text = string.Format("Le nom passé est : {0}", aValue);
base.OnInit(e);
}

Notre réécriture, qui se passe dans l'événement Application_BeginRequest, va consister en :

- vérifier que l'url se termine bien par .html ;

- récupérer ce qu'il y a avant le .html ;

- et réécrire l'url en Default.aspx et lui passer la chaine récupérée en paramètre name.

Soit :

global.asax.cs

protected void Application_BeginRequest(object sender, EventArgs e)
{
string urlPath = Request.Url.ToString();
if (urlPath.EndsWith(".html"))
{
int index = urlPath.LastIndexOf('/');
string name = urlPath.Substring(index + 1, urlPath.LastIndexOf(".html") - index - 1);
Context.RewritePath(string.Format("/Default.aspx?name={0}", name));
}
}

Maintenant, quand vous taperez par exemple les URL suivantes :

url saisie dans le navigateur

http://monsite/nico.html

url saisie dans le navigateur

http://monsite/nico-pyright.html

Le site affichera :

affichée par la page

Le nom passé est : nico

affichée par la page

Le nom passé est : nico-pyright

Télécharger le source exemple (12 ko).

4.Écrire un module d'url rewriting

Pour l'instant, ce qu'on a fait, c'est utiliser l'événement d'application BeginRequest pour faire notre réécriture d'url.
Nous allons désormais écrire une classe module qui va se substituer à la classe d'application en la complétant pour gérer notamment les requêtes entrantes. Cette classe va implémenter l'interface IHttpModule et s'abonner à l'événement BeginRequest

4.1.Rappels sur un module Http

Comme décrit dans MSDN, un module HTTP est un assembly appelé sur chaque demande effectuée sur votre application. Les modules HTTP sont appelés dans le cadre du pipeline de demande ASP.NET et ont accès aux événements du cycle de vie de toute la demande. Par conséquent, les modules HTTP vous donnent la possibilité d'examiner les demandes entrantes et d'agir en fonction de la demande.
On a besoin de les déclarer dans le web.config de cette façon, à la section <system.web> :

web.config

<httpModules>
<add name="le nom de mon module" type="namespace.classe, assembly"/>
</httpModules>

Comme d'habitude, il s'agit de déclarer notre module en précisant son namespace, sa classe et son assembly.

4.2.Une implémentation de module d'url rewriting

Pour écrire un module, il faut implémenter IHttpModule. Dans notre cas, il faudra également s'abonner à BeginRequest. Nous devrons ensuite récupérer le context pour procéder à la réécriture d'url.
Le minimum à écrire pour une telle classe est :

MonUrlRewriter.cs

public class MonUrlRewriter : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += context_BeginRequest;
}

private void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = sender as HttpApplication;
HttpContext context = (application == null) ? null : application.Context;
if (context != null)
{
HttpRequest request = context.Request;
// ici faire l'url rewriting
}
}

public void Dispose()
{
}
}

Pour que cette classe soit prise en compte par notre application, on va la définir dans le web.config à la section <system.web> :

web.config

<httpModules>
<add name="MonUrlRewriter" type="demoModuleUrlRewriting.MonUrlRewriter, demoModuleUrlRewriting"/>
</httpModules>

4.3.Exemple d'un module url rewriting

pour faire l'équivalent de notre premier exemple, dans la méthode context_BeginRequest, on fera :

MonUrlRewriter.cs

private void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = sender as HttpApplication;
HttpContext context = (application == null) ? null : application.Context;
if (context != null)
{
HttpRequest request = context.Request;
// ici faire l'url rewriting
string urlPath = request.Url.ToString();
if (urlPath.EndsWith(".html"))
{
int index = urlPath.LastIndexOf('/');
string name = urlPath.Substring(index + 1, urlPath.LastIndexOf(".html") - index - 1);
context.RewritePath(string.Format("/Default.aspx?name={0}", name));
}
}
}

Une fois le contexte récupéré, il s'agit du même code qui va récupérer la chaine représentant la page html et la passer en paramètre à Default.aspx.
Ainsi, notre application saura à nouveau rediriger une url de ce type :

http://monsite/nico.html

en

http://monsite/Default.aspx?name=nico

D'où, le site affichera :

Le nom passé est : nico

Télécharger le source exemple (12 ko).

5.Amélioration, utiliser le web.config pour paramétrer le module

C'est bien beau tout ça, mais on se rend compte qu'on va vite se retrouver avec une usine à gaz si on a plusieurs réécritures à faire, plus ou moins complexe. On va vite se retrouver avec des if dans tous les sens, démultiplier les index of …
Ça va vite devenir ingérable, difficile à maintenir et augmenter grandement les risques d'erreurs !

Une idée intéressante serait de généraliser le traitement et de déplacer le paramétrage dans le web.config histoire d'avoir juste à écrire un truc du genre dans le web.config pour paramétrer l'url rewriting :

web.config

<UrlRewriterConfigurationSection>
<rewriting map="/home.html" to="~/Default.aspx" />
</UrlRewriterConfigurationSection>

qui permettrait la transformation de l'url home.html en Default.aspx.
Ce qui permettrait de rajouter facilement une autre transformation si besoin, sans rajouter de ligne de code. Par exemple, si j'ai besoin de mapper une url contact.html, j'aurai juste à rajouter (par exemple) :

web.config

<rewriting map="/contact.html" to="~/Default.aspx?action=contact" />

5.1.Créer sa section personnalisée

Pour avoir une telle section dans son web.config, il faut créer une section personnalisée. Implémenter une section de configuration personnalisée n'est pas si complexe que ça pourrait en avoir l'air.
Je vous renvoie en préambule à mon tutoriel sur les sections personnalisées pour savoir comment implémenter une telle section.

Dans notre cas, cela pourrait être :

fichiers ListeElement.cs, ListesElementCollection.cs et UrlRewriterConfigurationSection.cs

using System;
using System.Configuration;

namespace demoModuleSectionUrlRewriting
{
public class UrlRewriterConfigurationSection : ConfigurationSection
{
private static readonly ConfigurationPropertyCollection _proprietes;
private static readonly ConfigurationProperty _listes;

static UrlRewriterConfigurationSection()
{
_listes = new ConfigurationProperty("", typeof(ListesElementCollection),
null, ConfigurationPropertyOptions.IsRequired | ConfigurationPropertyOptions.IsDefaultCollection);
_proprietes = new ConfigurationPropertyCollection();
_proprietes.Add(_listes);
}

public ListesElementCollection Listes
{
get { return (ListesElementCollection)base[_listes]; }
}

public new ListeElement this[string nom]
{
get { return Listes[nom]; }
}

protected override ConfigurationPropertyCollection Properties
{
get { return _proprietes; }
}
}

public class ListeElement : ConfigurationElement
{
private static readonly ConfigurationPropertyCollection _proprietes;
private static readonly ConfigurationProperty _map;
private static readonly ConfigurationProperty _to;

static ListeElement()
{
_map = new ConfigurationProperty("map", typeof(string), null, ConfigurationPropertyOptions.IsKey);
_to = new ConfigurationProperty("to", typeof(string), null, ConfigurationPropertyOptions.IsRequired);
_proprietes = new ConfigurationPropertyCollection();
_proprietes.Add(_map);
_proprietes.Add(_to);
}

public string Map
{
get { return (string)base[_map]; }
set { base[_map] = value; }
}

public string To
{
get { return (string)base[_to]; }
set { base[_to] = value; }
}

protected override ConfigurationPropertyCollection Properties
{
get { return _proprietes; }
}
}

public class ListesElementCollection : ConfigurationElementCollection
{
public override ConfigurationElementCollectionType CollectionType
{
get { return ConfigurationElementCollectionType.BasicMap; }
}
protected override string ElementName
{
get { return "rewriting"; }
}

protected override ConfigurationPropertyCollection Properties
{
get { return new ConfigurationPropertyCollection(); }
}

public ListeElement this[int index]
{
get { return (ListeElement)BaseGet(index); }
set
{
if (BaseGet(index) != null)
{
BaseRemoveAt(index);
}
base.BaseAdd(index, value);
}
}

public new ListeElement this[string map]
{
get { return (ListeElement)BaseGet(map); }
}

public void Add(ListeElement item)
{
base.BaseAdd(item);
}

public void Remove(ListeElement item)
{
BaseRemove(item);
}

public void RemoveAt(int index)
{
BaseRemoveAt(index);
}

public void Clear()
{
BaseClear();
}

protected override ConfigurationElement CreateNewElement()
{
return new ListeElement();
}

protected override object GetElementKey(ConfigurationElement element)
{
if (element != null)
return ((ListeElement)element).Map;
else
return null;
}
}
}

5.2.Gestion de l'url rewriting simple dans le module

Maintenant que la section personnalisée est créée, il faut être capable de faire la réécriture d'url en allant lire notre section personnalisée dans l'événement BeginRequest, une fois le contexte récupéré comme d'habitude :

MonUrlRewriter.cs

string urlAbsolutePath = request.Url.AbsolutePath;
if (urlAbsolutePath.EndsWith(".html"))
{
UrlRewriterConfigurationSection section = (UrlRewriterConfigurationSection)ConfigurationManager.GetSection("UrlRewriterConfigurationSection");
foreach (ListeElement list in section.Listes)
{
if (urlAbsolutePath == list.Map)
{
context.RewritePath(list.To);
break;
}
}
}

On récupère dans un premier temps l'url depuis le contexte. Et on boucle sur toutes sections. Si l'url courante correspond au mapping défini dans une section, alors on réécrit l'url avec la valeur correspondante de la section.

N'oubliez pas de déclarer la section dans le web.config, à savoir :

web.config

<configSections>
<section name="UrlRewriterConfigurationSection"
type="demoModuleSectionUrlRewriting.UrlRewriterConfigurationSection, demoModuleSectionUrlRewriting" />
</configSections>
<UrlRewriterConfigurationSection>
<rewriting map="/home.html" to="~/Default.aspx" />
</UrlRewriterConfigurationSection>

Ainsi, quand on utilise l'url suivante dans le site web :

http://monsite/home.html

il la transforme en :

http://monsite/Default.aspx

Rajoutons désormais dans le web.config la ligne suivante :

web.config

<rewriting map="/contact.html" to="~/Default.aspx?action=contact" />

et traitons dans Default.aspx l'action contact en affichant dans un label que nous sommes dans la page contact :

Default.aspx.cs

protected override void OnInit(EventArgs e)
{
string aValue = Request.QueryString["action"];
if (!string.IsNullOrEmpty(aValue) && aValue == "contact")
leLabel.Text = "Nous sommes dans la page de contact";
base.OnInit(e);
}

Ainsi, quand on utilise l'url suivante dans le site web :

http://monsite/contact.html

le site nous affiche :

Nous sommes dans la page de contact

Télécharger le source exemple (18 ko).

5.3.Amélioration, utilisation d'expressions régulières

Dans le cas précédent, on fait un simple mapping. Pour améliorer et faire quelque chose d'un peu plus complexe, on peut utiliser la capacité du framework.net à gérer les expressions régulières.
Exemple
Imaginons que j'ai une base de tutoriel qui soit accessible par l'url

~/Default.aspx?tutoriel=urlRewriting

Il serait plus pratique et meilleur pour le référencement d'avoir un lien :

/tutoriel/urlRewriting.html

On pourrait imaginer vouloir paramétrer notre webconfig ainsi :

web.config

<rewriting map="/tutoriel/.+.html" to="~/Default.aspx?tutoriel={0}"/>

Pour rappel, l'expression régulière .+ permet d'avoir tout ce qui contient plus d'une lettre.
Voici à quoi pourrait ressembler la partie url rewriting :

MonUrlRewriter.cs

string urlAbsolutePath = request.Url.AbsolutePath;
if (urlAbsolutePath.EndsWith(".html"))
{
UrlRewriterConfigurationSection section = (UrlRewriterConfigurationSection)ConfigurationManager.GetSection("UrlRewriterConfigurationSection");
foreach (ListeElement list in section.Listes)
{
Regex reg = new Regex(list.Map);
if (reg.IsMatch(urlAbsolutePath))
{
string urlRewrited = string.Format(list.To,
System.IO.Path.GetFileNameWithoutExtension(VirtualPathUtility.GetFileName(request.Url.LocalPath)));
context.RewritePath(urlRewrited);
break;
}
}
}

La méthode isMatch nous permet de traiter uniquement les URL qui répondent aux critères définis.
On extrait ensuite la chaine saisie juste avant le .html (ceci est fait grâce aux méthodes de la classe path, en tant que fichier sans extension).
Et on la remplace dans l'url à mapper, grâce à {0} et à string.format.

Pour simuler l'affichage du tutoriel, dans le OnInit, on fera :

Default.aspx.cs

string aValue = Request.QueryString["tutoriel"];
if (!string.IsNullOrEmpty(aValue))
leLabel.Text = string.Format("Voici le tutoriel {0}", aValue);
base.OnInit(e);

Ce qui fait que les URL suivantes :

http://monsite/tutoriel/urlRewriting.html
http://monsite/tutoriel/asp.net.html

Afficherons :

Voici le tutoriel urlRewriting
Voici le tutoriel asp.net

Télécharger le source exemple (17 ko).

5.4.Un peu plus loin avec les expressions régulières

Une fonctionnalité bien pratique des expressions régulières est la méthode Replace. Pour rester simple, cette méthode nous permet de remplacer une valeur d'une expression régulière par une autre valeur.
Imaginons que je dispose d'une fonctionnalité de recherche implémentée sur mon site, accessible par :

~/Default.aspx?search=maRecherche

On voudrait avoir une url plus digeste du genre :

/search/maRecherche.html

on va paramétrer notre web.config de la sorte :

web.config

<rewriting map="/search/(.+).html" to="~/Default.aspx?search=$1"/>

et traiter le cas de cette manière :

MonUrlRewriter.cs

UrlRewriterConfigurationSection section = (UrlRewriterConfigurationSection)ConfigurationManager.GetSection("UrlRewriterConfigurationSection");
foreach (ListeElement list in section.Listes)
{
Regex reg = new Regex(list.Map);
if (reg.IsMatch(urlAbsolutePath))
{
context.RewritePath(reg.Replace(urlAbsolutePath, list.To));
break;
}
}

On boucle sur tous les éléments de notre section et si l'url matche, alors on remplace la valeur trouvée dans la chaine de destination. Ici, $1 sera remplacé par le contenu de (.+).

Ainsi, pour les URL suivantes :

http://monsite/search/nico.html
http://monsite/search/un truc avec des espaces.html

Le site affichera, pour un traitement dans le OnInit du genre :

Default.aspx.cs

string aValue = Request.QueryString["search"];
if (!string.IsNullOrEmpty(aValue))
leLabel.Text = string.Format("Résultat de la recherche pour {0} : ....", aValue);

Affichage de la page

Résultat de la recherche pour nico : ....
Résultat de la recherche pour un truc avec des espaces : ....

On peut bien sûr cumuler les paramètres de remplacement…

Les expressions régulières sont tellement puissantes qu'on pourrait imaginer rapidement une petite utilisation :

web.config

<rewriting map="/add/(.+)\+(.+).html" to="~/Default.aspx?value1=$1&amp;value2=$2" />

combiné à un traitement dans Default.aspx de ce genre :

Default.aspx.cs

string value1 = Request.QueryString["value1"];
string value2 = Request.QueryString["value2"];
if (!string.IsNullOrEmpty(value1) && !string.IsNullOrEmpty(value2))
{
int intValue1;
if (int.TryParse(value1, out intValue1))
{
int intValue2;
if (int.TryParse(value2, out intValue2))
leLabel.Text = string.Format("Résultat de {0} + {1} = {2}", intValue1, intValue2, intValue1 + intValue2);
else
leLabel.Text = string.Format("Paramètre incorrect, {0} doit être numérique", intValue2);
}
else
leLabel.Text = string.Format("Paramètre incorrect, {0} doit être numérique", intValue1);
}

Nous aurons donc pour les URL suivantes :

http://monsite/add/15+18.html
http://monsite/add/15+0.html

les affichages suivants :

Résultat de 15 + 18 = 33
Résultat de 15 + 0 = 15

Télécharger le source exemple (18 ko).

5.5.Finalisation du module

Les méthodes présentées ci-dessus pourraient avantageusement être combinées pour couvrir un paramétrage complexe.
Il pourrait d'ailleurs être intéressant d'utiliser des groupes de sections et des sous-groupes pour faciliter le paramétrage.

Exemple pour un web.config de ce genre :

web.config

<configSections>
<sectionGroup name="UrlRewriterConfigurationSection">
<section name="Mapping" type="urlRewritingAll.UrlRewriterConfigurationSection, urlRewritingAll" />
<section name="RegEx" type="urlRewritingAll.UrlRewriterConfigurationSection, urlRewritingAll" />
</sectionGroup>
</configSections>
<UrlRewriterConfigurationSection>
<Mapping>
<rewriting map="/home.html" to="~/Default.aspx" />
<rewriting map="/contact.html" to="~/Default.aspx?action=contact" />
</Mapping>
<RegEx>
<rewriting map="/search/(.+).html" to="~/Default.aspx?search=$1"/>
<rewriting map="/add/(.+)\+(.+).html" to="~/Default.aspx?value1=$1&amp;value2=$2" />
</RegEx>
</UrlRewriterConfigurationSection>

On pourrait avoir notre module d'url rewriting ainsi :

MonUrlRewriter.cs

private void context_BeginRequest(object sender, EventArgs e)
{
HttpApplication application = sender as HttpApplication;
HttpContext context = (application == null) ? null : application.Context;
if (context != null)
{
HttpRequest request = context.Request;
string urlAbsolutePath = request.Url.AbsolutePath;
if (urlAbsolutePath.EndsWith(".html"))
{
TraiteSectionMapping(urlAbsolutePath, context);
TraiteSectionExpressionReguliere(urlAbsolutePath, context);
}
}
}

private void TraiteSectionMapping(string urlAbsolutePath, HttpContext context)
{
UrlRewriterConfigurationSection section =
(UrlRewriterConfigurationSection)ConfigurationManager.GetSection("UrlRewriterConfigurationSection/Mapping");
foreach (ListeElement list in section.Listes)
{
if (urlAbsolutePath == list.Map)
{
context.RewritePath(list.To);
break;
}
}
}
private void TraiteSectionExpressionReguliere(string urlAbsolutePath, HttpContext context)
{
UrlRewriterConfigurationSection section =
(UrlRewriterConfigurationSection)ConfigurationManager.GetSection("UrlRewriterConfigurationSection/RegEx");
foreach (ListeElement list in section.Listes)
{
Regex reg = new Regex(list.Map);
if (reg.IsMatch(urlAbsolutePath))
{
context.RewritePath(reg.Replace(urlAbsolutePath, list.To));
break;
}
}
}

Notez que l'on récupère les sous-sections avec GetSection et l'url Groupe/SousGroupe. Ensuite, pour chaque section, on boucle sur toutes les valeurs et on applique la règle de transformation pour réécrire l'url, s'il y a concordance.

Télécharger le source exemple (18 ko).

6.Url rewriting et postback

Le problème d'un postback combiné à l'url rewriting c'est qu'il utilise l'attribut action de la balise form d'une page pour afficher la page après l'envoi du formulaire.
Ainsi, une page dont l'url aura été réécrite comme home.html (à la place de Default.aspx) qui enverrait un postback au serveur serait affichée après ce postback avec l'url Default.aspx puisque celle-ci est celle qui est enregistrée dans l'attribut action.
C'est tout à fait fonctionnel dans les cas simples, mais disgracieux pour l'utilisateur qui ne comprend pas d'où vient cette url qu'il ne connait pas, alors qu'il en utilisait une autre au départ.
Nous allons donc tacher de résoudre ce problème, en étudiant deux possibilités qui sont basées sur le même principe, à savoir : la modification du comportement de la balise Form pour ne pas réécrire l'attribut action, comme ça, elle conserverait l'url courante et pourrait afficher l'url correcte lors d'un postback.

6.1.Surcharge du contrôle HtmlForm

Une première solution serait d'écrire un contrôle personnalisé qui va surcharger le contrôle HtmlFormet qui filtrera l'attribut action.
Une technique pour le faire est de créer une classe qui dérive de HtmlTextWriter et qui surcharge la méthode WriteAttribute pour filtrer l'attribut action. Nous utiliserons cette classe comme writer de notre HtmlForm.

Form.cs

public class Form : HtmlForm
{
protected override void RenderAttributes(HtmlTextWriter writer)
{
NoActionWriter customWriter = new NoActionWriter(writer);
base.RenderAttributes(customWriter);
}

private class NoActionWriter : HtmlTextWriter
{
public NoActionWriter(HtmlTextWriter writer) : base(writer.InnerWriter)
{
}

public override void WriteAttribute(string name, string value, bool fEncode)
{
if (name != "action")
base.WriteAttribute(name, value, fEncode);
}
}
}

Pour utiliser ce contrôle personnalisé, dans notre page aspx il faudra enregistrer le contrôle de cette façon :

Default.aspx

<%@ Register TagPrefix="MaForm" Namespace="demoUrlRewritingPostBack" Assembly="demoUrlRewritingPostBack" %>

Et entourer nos contrôles dans la page par la nouvelle balise Form (tout en ayant supprimé l'ancienne balise <form>) :

Default.aspx

<MaForm:Form runat="server">
<a href="/home.html">Aller sur /home.html</a>
<div>
<asp:TextBox ID="TextBox1" runat="server"/>
<asp:Button ID="Button1" runat="server" Text="Envoyer" />
<asp:Label ID="leLabel" runat="server"/>
</div>
</MaForm:Form>

Cette solution montre dès à présent un inconvénient. Il faut remplacer toutes les balises form de notre application par notre contrôle personnalisé. Ce qui peut être très laborieux si la solution comporte beaucoup de pages, sauf si l'on dispose d'une master page, dans ce cas, il ne faudra le faire qu'une unique fois.

Télécharger le source exemple (20 ko).

6.2.Utiliser un contrôle adapté

Une autre solution qui ne fonctionne qu'avec asp.net 2.0 est d'utiliser un fichier de définition de navigateur pour personnaliser le rendu de la balise form. (Plus de précisions sur les fichiers de définition de navigateur dans MSDN).

Ajouter le dossier spécial App_Browsers : clic droit sur le projet -> add -> add asp.net folder -> App_Browsers.

Et ajouter un nouveau fichier BrowserFile que nous allons appeler FormUrlRewriting.browser

Dedans, nous allons y mettre le type de l'adapter que nous allons utiliser pour le contrôle System.Web.UI.HtmlControls.HtmlForm.

FormUrlRewriting.browser

<browsers>
<browser refID="Default">
<controlAdapters>
<adapter controlType="System.Web.UI.HtmlControls.HtmlForm" adapterType="demoUrlRewritingPostBackAdapter.FormAdapter, demoUrlRewritingPostBackAdapter" />
</controlAdapters>
</browser>
</browsers>

L'attribut refID est utilisé pour associer de nouvelles fonctions à une définition de navigateur existante.
Il ne nous reste plus qu'à créer la classe FormAdapter qui devra hériter de System.Web.UI.Adapters.ControlAdapter. Cette classe permet de personnaliser le rendu pour le contrôle dérivé auquel l'adaptateur est joint, afin de modifier le balisage par défaut ou le comportement de navigateurs spécifiques, et constitue la classe de base à partir de laquelle sont hérités tous les adaptateurs de contrôles.

FormAdapter.cs

public class FormAdapter : ControlAdapter
{
protected override void Render(HtmlTextWriter writer)
{
NoActionWriter customWriter = new NoActionWriter(writer);
base.Render(customWriter);
}

private class NoActionWriter : HtmlTextWriter
{
public NoActionWriter(HtmlTextWriter writer): base(writer.InnerWriter)
{
}

public override void WriteAttribute(string name, string value, bool fEncode)
{
if (name != "action")
base.WriteAttribute(name, value, fEncode);
}
}
}

Le principe est de surcharger la méthode Render et d'utiliser un writer personnalisé qui n'affiche pas l'attribut, de la même façon que dans le paragraphe précédent.
Ainsi, grâce à cette méthode, nous n'avons plus à remplacer toutes les balises form de notre application.

Télécharger le source exemple (21 ko).

7.Une méthode de pseudoréécriture d'url, utilisation de la propriété PathInfo

Il existe une autre méthode très simple, que je n'apprécie pas spécialement, car je trouve qu'elle dénature l'url. Ce n'est pas une technique de réécriture d'url à proprement parler, mais elle peut valoir le coup d'être connue.
Il s'agit d'utiliser la propriété PathInfo de l'objet Request. Cela permet de connaitre un paramètre d'url qui serait passé sous cette forme :

http://monsite/Default.aspx/nico-pyright

Et qu'on récupérerait comme ça :

Default.aspx.cs

string param = Request.PathInfo;
if (!string.IsNullOrEmpty(param))
leLabel.Text = string.Format("Paramètre passé : {0}", param);

Télécharger le source exemple (10 ko).

8.Configurer IIS pour l'url mapping

Si vous utilisez le serveur web inclus dans visual studio 2005 (Webdev.Webserver), alors vous avez pu tester ces méthodes d'url mapping sans problèmes.
Par contre, si vous avez essayé de le faire avec IIS, vous allez me dire que rien ne fonctionne. Et vous aurez raison.

Il faut en effet configurer IIS pour qu'il puisse interpréter les pages réécrites (qui ont ici une extension .html) comme des pages aspx.

Il faut créer un mapping pour que les fichiers d'extensions .html soient passés dans l'extension ISAPI d'asp.net.
N. B. Une fois l'installation d'IIS terminée, n'oubliez pas d'exécuter la commande suivante pour enregistrer les extensions asp.net :

aspnet_regiis -i

Aspnet_regiis.exe fait partie du framework.net et est situé dans le répertoire du framework utilisé. (chez moi : C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727).

Ensuite, ouvrez le panneau de configuration d'IIS (panneau de configuration -> outils d'administration -> service Internet (IIS)).
Si vous n'avez pas encore créé de site web, faites-le.
Affichez les Propriétés de votre site web et cliquez sur configuration dans l'onglet Répertoire virtuel :

Dans l'onglet Mappages, cliquez sur Ajouter

Dans Exécutable, allez chercher la dll aspnet_isapi.dll (C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727).
Saisissez l'extension .html, pour tous les verbes.
La checkbox Moteur de script doit être cochée, celle pour vérifier l'existence du fichier doit être décochée.

Votre IIS est désormais configuré pour interpréter les pages .html comme des pages aspx.

9.Conclusion

Voilà pour cet article. Nous avons donc vu le principe de l'url rewriting et avons développé un exemple de module paramétrable par le web.config. Nous avons également étudié deux solutions pour faire cohabiter l'url-rewriting et les postbacks. Nous avons aussi constaté la puissance des expressions régulières qui peuvent nous être d'un grand secours dans les situations de réécriture d'url.
J'espère que cet article vous aura aidé à construire votre propre solution d'url rewriting. Notez qu'il est très simple d'enrichir la section personnalisée du web.config pour l'étendre à vos besoins.

Une erreur courante associée à la réécriture d'url est qu'il peut arriver qu'on « perde » les images ou les références javascript/css. Cela arrive quand on utilise des URL relatives et que la réécriture ne correspond pas à ce qui est attendu. N'hésitez pas à utiliser la syntaxe ~ pour faire référence à la racine de l'application.

Il faut également faire attention quand on utilise le postback et des répertoires réécrit différents de ceux où sont stockées les pages aspx. En effet, celles-ci pourraient ne pas être retrouvées à cause de la réécriture d'un répertoire qui n'existe pas.
Pour palier cet inconvénient, au lieu d'utiliser une comparaison de chaine d'url avec un ==, on utilisera plutôt un Contains ou une expression régulière.

Remerciements

Je remercie l'équipe Dotnet pour ses relectures attentives du document.

Contact

Si vous constatez une erreur dans le tutoriel, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.