July 2009 Entries

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));
    }
}

 

Prüfen ob alle Post-Controller-Actions dass ValidateAntiForgeryTokenAttribute haben

Ein möglicher Angriff auf Web-Anwendungen ist Cross-Site Request Forgery (CSRF). Mit dem ASP.NET MVC Framework gibt es eine Möglichkeit solche Angriffe zu verhindern.

Siehe dazu auch diesen Blog-Eintrag: Prevent Cross-Site Request Forgery (CSRF) using ASP.NET MVC’s AntiForgeryToken() helper.

Jedoch passiert es mir des Öfteren dass ich vergesse daran zu denken die Actions entsprechend zu attributieren.

Deshalb habe ich mir einen Unit-Test geschrieben der alle Controller-Action die auf POST reagieren überprüft ob sie dass ValidateAntiForgeryTokenAttribute haben.

Damit er nicht bei mir versauert, hier der Test zur allgemeinen Verwendung.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web.Mvc;

using DerAlbert.Community.Web.Controllers;

using MbUnit.Framework;

namespace DerAlbert.Community.Web.Tests.Controllers
{
    [TestFixture]
    public class ControllerTests
    {
        [Test]
        public void Controller_AllePostActions_NutzenValidateAntiForgeryTokenAttribute()
        {
            foreach (var controllerType in AllControllerTypes())
            {
                foreach (var methodInfo in GetActionMethods(controllerType))
                {
                    if (MethodAcceptsPost(methodInfo))
                    {
                        Assert.IsTrue(HasValidateAntiForgeryTokenAttribute(methodInfo),
                                      string.Format("{0}.{1} nutzt nicht das ValidateAntiForgeryTokenAttribute",
                                                    controllerType.FullName, methodInfo.Name));
                    }
                }
            }
        }

        private bool HasValidateAntiForgeryTokenAttribute(MethodInfo methodInfo)
        {
            return null != (from a in methodInfo.GetCustomAttributes(false)
                            where a is ValidateAntiForgeryTokenAttribute
                            select a).SingleOrDefault();
        }

        private bool MethodAcceptsPost(MethodInfo methodInfo)
        {
            var attribute = (AcceptVerbsAttribute) (from a in methodInfo.GetCustomAttributes(false)
                                                    where a is AcceptVerbsAttribute
                                                    select a).SingleOrDefault();

            return attribute != null && attribute.Verbs.Contains("POST");
        }

        private IEnumerable<Type> AllControllerTypes()
        {
            return from t in typeof (HomeController).Assembly.GetExportedTypes()
                   where typeof (Controller).IsAssignableFrom(t)
                   select t;
        }

        private IEnumerable<MethodInfo> GetActionMethods(Type controllerType)
        {
            return from mi in controllerType.GetMethods()
                   where typeof (ActionResult).IsAssignableFrom(mi.ReturnType)
                   select mi;
        }
    }
}
Technorati-Tags: ,,,

Der .NET Open Space 2009 in Blaustein/Ulm

Nur noch sieben Tage und der er geht los, der erste .NET Open Space für 2009 in Blaustein bei Ulm.

Bis jetzt haben sich 65 Personen angemeldet. Somit ist viel Know-How in den unterschiedlichsten Bereichen vor Ort. Wenn ich mir die Teilnehmerliste ansehe verspricht es ein sehr interessantes Wochenende werden bei der Menge an Wissen und Interessen.

Ich selbst werde wohl mindestens eine Session zu ASP.NET MVC vorschlagen, auf der User Group Tour wurde reges Interesse daran bekundet.

Wer sich also nur Ansatzweise für Software-Entwicklung mit .NET Interessiert der sollte es nicht versäumen auch vom 11. bis zum 12. Juli in Blaustein zu sein! Es sind noch wenige freie Plätze vorhanden.

Da so ein Open Space am besten funktioniert wenn man vorher wenigstens schon mal das eine oder andere Wort gewechselt hat, gibt es nun auch ein spontanes Treffen zum kennenlernen am Freitag den 10. Juli 2009. Dies sollte man sich auch nicht entgehen lassen.

Also, wir sehen uns in einer Woche in Blaustein!