29 Jul
2009

Urls zu ASP.NET MVC Controller-Actions außerhalb eines Views erstellen

 

Innerhalb eines ASPX-Views ist das erstellen eines Links bzw. einer Url einfach. Es gibt ja HtmlHelper.ActionLink() und MvcViewPage.Url um diese zu erzeugen.

Nun gibt es Situation wo man keinen direkten Zugriff darauf hat an jedoch die Url zu bestimmten Action inklusive der richtigen Parameter braucht.

So ist es in einer meiner Anwendungen notwendig E-Mails mit Links auf die Anwendung zu haben (Neues Kennwort zuteilen, Benutzer aktiveren etc.)

Dies wird weder beim Controller noch im View veranlasst (und wenn man es doch macht sollte man dringend darüber nachdenken warum man dies macht und es seinlassen). Natürlich möchte man auch das beim Erstellen der Urls auch die verwendeten Routen berücksichtigt werden.

Wenn ich also eine Url brauche, deklariere ich mir erst ein entsprechendes Interfaces.

public interface IPasswordMailUrlBuilder
{
    string GetResetPasswordUrl(string userHash, Guid guid);
}

Die  Anwendung ist sehr einfach. Die Abhängigkeiten werden mittels eines IoC-Containers aufgelöst und bei der Instanziierung über den Construcor übergeben.

private readonly ITemplateEMailSending sending;
private readonly IPasswordMailUrlBuilder urlBuilder;

public void SendWebUserThePasswordResetLink(WebUser webUser)
{
    webUser.EnsureConfirmationKey();
    string url = urlBuilder.GetResetPasswordUrl(webUser.GetWebUserHash(hashing), webUser.ConfirmationKey.Value);
    TemplateItems items = CreateTemplateItems();
    items.Add("url.resetpassword", url);
    sending.Send(MailTemplates.SendWebUserPasswordReset, webUser, items);
}

ITemplateEMailSending ist ein Service der aus einer Template eine E-Mail erstellt. In den TemplateItems stehen die Elemente drin die auf der Template ausgetauscht werden. Aber dies ist nicht das Thema hier.

Die Implementierung von  IPasswordMailUrlBuilder sieht nun folgendermaßen aus.

public class PasswordMailUrlBuilder : IPasswordMailUrlBuilder
{
    private readonly IActionUrlBuilder urlBuilder;

    public PasswordMailUrlBuilder(IActionUrlBuilder urlBuilder)
    {
        this.urlBuilder = urlBuilder;
    }

    public string GetResetPasswordUrl(string userHash, Guid guid)
    {
        return urlBuilder.BuildActionUrl<PasswordController>(c => c.CreateNew(userHash, guid.CleanGuid()));
    }
}

Der PasswordMailUrlBuilder nutzt nun IActionUrlBuilder, in dessen Implementierung findet dann die eigentliche Arbeit statt.

Damit das ganze funktioniert ist es erforderlich auch die ASP.NET MVC v1.0  Futures zu verwenden. In den Futures ist eine einfache Möglichkeit enthalten um Typisiert auf Controller-Actions zu verweisen. ASP.NET MVC v2 wird diese Möglichkeit direkt enthalten haben.

public interface IActionUrlBuilder
{
    string BuildActionUrl<TController>(Expression<Action<TController>> expression)
        where TController : Controller;
}

Wie man sieht ist IActionUrlBuilder wieder sehr kompakt, bietet jedoch alles was man dazu so braucht. Die Implementierung ist ein wenig mehr Aufwand, jedoch alles keine Magie.

Der Trick besteht darin erstmal die Action-Expression in die RouteDataValues zerlegen zu lassen, dazu bieten die MVC Futures die ExpressionHelper-Klasse.

Nun verwendet man die MVC eigene UrlHelper-Klasse um sich daraus eine Url erstellen zu lassen. Da die Angabe http://servername.tld in diese Url fehlt setzt man diese einfach noch davor.

Die UrlHelper-Klasse braucht einen RequestContext, diesen erstellt man einfach. Man solltet nur darauf achten das man die schon vorhandenen RouteDataValues ermittelt und übergibt. Sonst kann es passieren das Teile der Url die weder Controller oder Action sind einfach unter der Tisch fallen gelassen werden (z.B. Angabe einer Culture in der Url). Möchte man die vorhandenen Werte nicht berücksichtige. So ist beim erstellen des RequestContext einfach eine leere Instanz von der RouteData-Klasse zu übergeben.

Und hier der Code.

public class ActionUrlBuilder : IActionUrlBuilder
{
    public string BuildActionUrl<TController>(Expression<Action<TController>> expression)
        where TController : Controller
    {
        var routeValues = GetRouteValues(expression);
        return CreateUrl(routeValues);
    }

    private static RouteValueDictionary GetRouteValues<TController>(Expression<Action<TController>> expression)
        where TController : Controller
    {
        return ExpressionHelper.GetRouteValuesFromExpression(expression);
    }

    private static string CreateUrl(RouteValueDictionary routeValues)
    {
        var urlHelper = new UrlHelper(CreateRequestContext());
        return GetBaseUrl(urlHelper.RequestContext) + urlHelper.RouteUrl(routeValues);
    }

    private static string GetBaseUrl(RequestContext context)
    {
        return string.Format("{0}://{1}", context.HttpContext.Request.Url.Scheme, context.HttpContext.Request.Url.Authority);
    }

    private static RequestContext CreateRequestContext()
    {
        HttpContextBase httpContext = new HttpContextWrapper(HttpContext.Current);
        return new RequestContext(httpContext, RouteTable.Routes.GetRouteData(httpContext));
    }
}

 


Der Eintrag ist mir etwas Wert
 

Feedback

# re: Urls zu ASP.NET MVC Controller-Actions außerhalb eines Views erstellen

left by Markus Zywitza at 7/30/2009 8:24 AM Gravatar
Hallo Albert

das sieht sehr interessant aus, aber mich würde interessieren, wie Du den ActionUrlBuilder testest? Die Verwendung von HttpContext.Current schränkt die Testbarkeit doch sehr ein, oder?

-Markus

# re: Urls zu ASP.NET MVC Controller-Actions außerhalb eines Views erstellen

left by Der Albert at 7/30/2009 1:19 PM Gravatar
Ehrlich gesagt teste ich dies in diesem den ActionUrlBuilder gar nicht. Da das ganze sehr nah am MVC Framework ist.

Aber es sollte kein Problem sein eine HttpContextBase manuell reinzubekommen und dieses im Bedarfsfall zu Mocken.

Jedoch erscheint mir der Aufwand vom Mocken her sehr aufwändig, da sowohl Routen als auch der HttpContext wohl relativ komplett gemockt und mit Daten gefüttert werden müssten.
Comments have been closed on this topic.