Wednesday, May 14, 2008

What do you mean by "Action Precondition Filter"?

I often find the need to check for the existence of a parameter passed to a web page, such as an ID, and also often need to check if the ID is valid as well (e.g.: a valid primary key).

In the spirit of Design By Contract, I would consider this check to be a sort of "Request By Contract", at least insofar as the concept of a "precondition".

UPDATE: Action Precondition Filter is now part of MVCContrib. Find out more about the newer version of PreconditionFilter.

How do your filters work, a.k.a. ActionFilterAttribute

System.Web.Mvc.ActionFilterAttribute, introduced in the ASP.NET MVC Preview 2 release, exposes an event handler in which to run "pre-Action" code, named OnActionExecuting(). Via this method's FilterExecutingContext parameter, you can get access to the Controller, the ActionMethod, and the RouteData, along with the familiar HttpContext.

My preliminary attempt to design common-use filters resulted in the concepts of a "RegEx Precondition Filter" and a "Predicate Precondition Filter". A RegEx (or Regular Expression), for those that are unfamiliar with the term, is what I would simply call a "text pattern matching language". A Predicate, in this context, is simply a fancy term for a method that returns a boolean.

So, as mentioned above, I often want to declare that a web page (i.e.: MVC Action) have a precondition such that one of its parameters (or RouteData keys, in MVC terms), let's say "id", had to be numeric, non-negative, and non-zero. And if the parameter did not meet this condition (e.g.: a user was trying to hack the URL), I would want to redirect the user to another URL, such as the index page or an error page.

To accomplish this using my "RegEx Precondition Filter", I would create an attribute on the applicable controller's Action method that looked like this:
[RegExPreconditionFilter(RouteParam = "id", RegExPattern = "^[1-9][0-9]*$", ErrorUrl = "/Home/Error")]
To accomplish this using my "Predicate Precondition Filter", I would create an attribute on the applicable controller action method that looked like this...
[PredicatePreconditionFilter(RouteParam = "id", PredicateMethod = "IsGreaterThanZero", ErrorUrl = "/Home/Error")]
...AFTER adding a predicate method to the Controller containing the applicable Action that accepted a single parameter of object and returned a boolean value. In other words, a method of type Predicate<object>.

These parameters are "stackable", meaning you can have more than one attribute (of either type) attached to an Action method.

To make these Precondition Filter attributes work, just add the RegExPreconditionFilter code and the PredicatePreconditionFilter code (both listed below) to your MVC project and compile.

Show Me the Code!

Note: this code has not been tested against the April CodePlex release, though it may work fine without alteration.

Example Controller:

using System;

using System.Web.Mvc;

 

namespace MvcApplication1.Controllers

{

    public class HomeController : Controller

    {

 

        public void Index()

        {

            RenderView("Index");

        }

 

        [RegExPreconditionFilter(RouteParam = "id", RegExPattern = "^[1-9][0-9]*$", ErrorUrl = "/Home/Error")]

        [RegExPreconditionFilter(RouteParam = "id", RegExPattern = "^[2-9][0-9]*$", ErrorUrl = "/Home/Error")]

        public void Company()

        {

            RenderView("Company");

        }

 

        [PredicatePreconditionFilter(RouteParam = "id", PredicateMethod = "IsGreaterThanZero", ErrorUrl = "/Home/Error")]

        [PredicatePreconditionFilter(RouteParam = "id", PredicateMethod = "IsGreaterThanOne", ErrorUrl = "/Home/Error")]

        public void Employee()

        {

            RenderView("Employee");

        }

 

        public void Error()

        {

            RenderView("Error");

        }

 

        protected bool IsGreaterThanZero(object value)

        {

            try

            {

                int id = Convert.ToInt32(value);

                return id > 0;

            }

            catch

            {

                return false;

            }

        }

 

        protected bool IsGreaterThanOne(object value)

        {

            try

            {

                int id = Convert.ToInt32(value);

                return id > 1;

            }

            catch

            {

                return false;

            }

        }

 

    }

 

}

RegExPreconditionFilter.cs:

using System.Web.Mvc;

using System.Text.RegularExpressions;

 

namespace MvcApplication1

{

 

    [System.AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Interface,

        AllowMultiple = true)]

    public class RegExPreconditionFilter : ActionFilterAttribute

    {

 

        public string RouteParam { get; set; }

        public string RegExPattern { get; set; }

        public string ErrorUrl { get; set; }

 

        public override void OnActionExecuting(FilterExecutingContext filterContext)

        {

 

            //redirect if invalid

            if (!filterContext.RouteData.Values.ContainsKey(RouteParam)

                || filterContext.RouteData.Values[RouteParam].ToString().Length == 0

                || !Regex.IsMatch(filterContext.RouteData.Values[RouteParam].ToString(), RegExPattern))

            {

 

                filterContext.HttpContext.Response.Redirect(ErrorUrl);

 

            }

 

        }

 

    }

 

}

PredicatePreconditionFilter.cs:

using System;

using System.Web.Mvc;

 

namespace MvcApplication1

{

 

    [System.AttributeUsage(System.AttributeTargets.Method | System.AttributeTargets.Interface,

        AllowMultiple = true)]

    public class PredicatePreconditionFilter : ActionFilterAttribute

    {

 

        public string RouteParam { get; set; }

        public string PredicateMethod { get; set; }

        public string ErrorUrl { get; set; }

 

        public override void OnActionExecuting(FilterExecutingContext filterContext)

        {

 

            Predicate<object> predicate = (Predicate<object>)Delegate.CreateDelegate(typeof(Predicate<object>), filterContext.Controller, PredicateMethod);

 

            //redirect if invalid

            if (!filterContext.RouteData.Values.ContainsKey(RouteParam)

                || filterContext.RouteData.Values[RouteParam] == null

                || !predicate(filterContext.RouteData.Values[RouteParam]))

            {

 

                filterContext.HttpContext.Response.Redirect(ErrorUrl);

 

            }

 

        }

 

    }

 

}

.NET | ASP.NET | C_Sharp | MVC