ASP.NET MVC: Kleingeschriebene Routen mit Minus trennen statt PascalCase

ASP.NET MVC: Kleingeschriebene Routen mit Minus trennen statt PascalCase

PascalCase und camelCase sind tolle Namenskonventionen, um beispielsweise in .NET zu programmieren. Diese auch bei URLs zu verwenden wie bei ASP.NET MVC standardmäßig der Fall ist allerdings zuviel des Guten: Hier hat sich die Schreibweise in Kleinbuchstaben durchgesetzt. Mehrere Wörter trennt man zur besseren Lesbarkeit meist mit einem Minus, selten auch mit Unterstrichen. Von Haus aus bietet ASP.NET leider nur die Möglichkeit, Controller und Actions klein zu schreiben. Eine Trennung mit Minuszeichen ist nicht vorgesehen, wodurch bei längeren Bezeichnungen die Lesbarkeit leidet. In folgendem Artikel zeigen wir euch nützliche Hilfsklassen, mit denen ihr die URLs nach euren Wünschen anpassen könnt.

Schritt #1: Klasse zur Umwandlung der Namenskonventionen

Vom Konzept her werden wir die generierten URLs in unser eigenes Format umwandeln. Bevor die Anfrage von ASP.NET verarbeitet wird, wandeln wir diese wieder in das ursprüngliche PascalCase-Format um. Denn intern soll ASP.NET schließlich nach wie vor mit den CamelCase-Bezeichnungen arbeiten. Wer selbst dies nicht möchte, kann den letzten Schritt weglassen. Um dies zu bewerkstelligen, benötigen wir eine Klasse zur Umwandlung in beide Richtungen:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

///<summary>
/// Wandelt CamelCase in mit minus-getrennte-urls um und umgekehrt
/// </summary>
public class HyphenatedConverter {
    ///<summary>
    /// Wandelt die Großbuchstaben eines PascalCase-Strings in Kleinbuchstaben um, die mit einem Minus getrennt werden (PascalCase => pascal-case)
    /// </summary>
    public static string FromPascalCase(string inputContent) {
        List<char> convertedContent = inputContent.ToList();
        for (int i = 0; i < convertedContent.Count; i++) { var currentChar = convertedContent[i]; if (currentChar == '?') { break; } if (Char.IsUpper(currentChar)) { convertedContent[i] = convertedContent[i].ToString().ToLower().FirstOrDefault(); if (i > 0) {
                    convertedContent.Insert(i, '-');
                }
            }
        }
        return new string(convertedContent.ToArray());
    }
    ///<summary>
    /// Revidiert die Umwandlung von FromPascalCase: Aus pascal-case wird wieder PascalCase
    /// </summary>
    public static string ToPascalCase(string inputContent) {
        if (inputContent.Length == 0)
            return inputContent;

        var convertedContent = new StringBuilder();
        convertedContent.Append(Char.ToUpperInvariant(inputContent[0]));

        for (int i = 1; i < inputContent.Length; i++) {
            if (inputContent[i] == '-') {
                if (i + 1 < inputContent.Length) {
                    convertedContent.Append(Char.ToUpperInvariant(inputContent[i + 1]));
                    i++;
                }
            } else {
                convertedContent.Append(inputContent[i]);
            }
        }

        return convertedContent.ToString();
    }
}
 

Mit FromPascalCase wird PascalCaseUrl in pascal-case-url umwandelt, wogegen ToPascalCase die umgekehrte Konvertierung durchführt. Diese beiden Funktionen benötigen wir im folgenden Schritt.

Schritt #2: Virtuelle Pfade anpassen

Zuerst widmen wir uns den Links, die über virtuelle Pfade generiert werden. Dies ist beispielsweise der Fall, wenn wir mit Razor-Hilfsklassen wie @Html.ActionLink() einen Link erzeugen. Dazu erzeugen wir eine neue Klasse – hier HyphenatedRoute genannt – die von Route erbt. Dort muss die Methode GetVirtualPath überschrieben werden. Diese ist für die Generierung des Linkes zuständig.

using System.Web.Routing;

///<summary>
/// Erzeugt eine Route, in denen die Wörter nicht durch Großbuchstaben (CamelCase), sondern mithilfe von Bindestrichen getrennt werden (z.B. index-seite)
/// </summary>
public class HyphenatedRoute : Route {
    public HyphenatedRoute(string url, object defaults) : base(url, new RouteValueDictionary(defaults), new HyphenatedRouteHandler()) { }
    public HyphenatedRoute(string url, RouteValueDictionary defaults) : base(url, defaults, new HyphenatedRouteHandler()) { }
    public HyphenatedRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constrains, IRouteHandler routeHandler) : base(url, defaults, constrains, routeHandler) { }
    public HyphenatedRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constrains, RouteValueDictionary dataTokens, IRouteHandler routeHandler) : base(url, defaults, constrains, dataTokens, routeHandler) { }

    ///<summary>
    /// Diese Funktion wird von MVC-Hilfsklassen aufgerufen, um AC-Links zu generieren
    /// </summary>
    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values) {
        values["controller"] = HyphenatedConverter.FromPascalCase(values["controller"] as string);
        values["action"] = HyphenatedConverter.FromPascalCase(values["action"] as string);
        return base.GetVirtualPath(requestContext, values);
    }
}

Der Konstruktor der Route-Klasse besitzt etliche Überladungen, die man leider bei Bedarf alle manuell implementiere muss. In diesem Beispiel sind nur die gängigsten und zwei von mir benötigte mit dabei, dies muss gegebenenfalls angepasst werden.

Schritt 3: Rückumwandlung bei eingehenden Anfragen

Wie zu Beginn schon angekündigt müssen wir unser eigenes URL-Format bei eingehenden Anfragen wieder in das ursprüngliche PascalCase-Format umwandeln, damit die Anfrage korrekt verarbeitet werden kann. Hierzu ist eine Klasse nötig, die von MvcRouteHandlerb erbt. Im Prinzip findet hier der umgekehrte Prozess aus dem letzten Schritt statt:

using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

///<summary>
/// Wandelt durch Bindestriche getrennte Routen intern wieder in ihre PascalCase-Variante um, damit diese vom MVC-Framework korrekt weiterverarbeitet werden können
/// </summary>
public class HyphenatedRouteHandler : MvcRouteHandler {

    protected override IHttpHandler GetHttpHandler(RequestContext requestContext) {
        var values = requestContext.RouteData.Values;

        values["action"] = HyphenatedConverter.ToPascalCase(values["action"].ToString());
        values["controller"] = HyphenatedConverter.ToPascalCase(values["controller"].ToString());

        return base.GetHttpHandler(requestContext);
    }
}

Nun ist unsere Route einsatzbereit und kann in der Routen-Konfiguration verwendet werden. Standardmäßig geschieht dies in der Klasse RouteConfig, die sich im Ordner App_Start befindet. Anstelle der Instanz einer Route-Klasse übergeben wir der RouteCollection einfach unsere selbst definierte HyphenatedConverter-Klasse. Für die standardmäßige Action-Controller-Route mit optionaler Id sieht dies beispielsweise wie folgt aus:

using System.Web.Mvc;
using System.Web.Routing;
public class RouteConfig {
    public static void RegisterRoutes(RouteCollection routes) {
        var acRoute = new HyphenatedRoute(
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
        routes.Add(acRoute);
    }
}

Da unsere eigene Route mit der Route-Klasse kompatibel ist, muss lediglich die Klassenbezeichnung angepasst werden. Eventuell nötige Konstruktoren können identisch mit denen der Route-Klasse bei Bedarf eingefügt werden, wie bereits oben erwähnt. Man muss lediglich darauf achten, als IRouteHandler jeweils den benutzerdefinierten Handler (hier HyphenatedRouteHandler) zu übergeben.

Natürlich kann man dieses Beispiel auch als Grundlage nehmen, um seine komplett eigenen Namenskonventionen für URLs zu entwickeln. Allerdings ist aus verschiedenen Gründen zu empfehlen, hier nicht zu sehr vom gängigen Standard abzuweichen. Beispielsweise sind die Pfade von gesetzten Cookies schreibungsabhängig. Ein Cookie im Pfad /myapp ist daher beim Aufruf von /MyApp oder auch myApp nicht verfügbar.

Data Annotiations als Alternative?

ASP.NET bietet mit dem ActionName-Attribute die Möglichkeit, den Namen der Action in der URL unabhängig von ihrem internen Namen zu verwenden:

[ActionName("register-account")]
public ActionResult RegisterAccount() {
    return View("RegisterAccount");
}

Die Action RegisterAccount würde somit in der URL als register-account bezeichnet. Dieses Vorgehen hat jedoch mehrere Nachteile: Das Attribute muss für jede Action manuell gesetzt werden. Außerdem wird der benutzerdefinierte Name auch für die Ansicht genutzt. Im obigen Beispiel müsste diese also register-account.cshtml heißen statt RegisterAccount.cshtml. Diese Alternative ist daher eher weniger empfehlenswert.

Leave a Reply