Parsing of URL path into action object parameter in ASP.NET MVC

5 Jul 2012

ASP.NET MVC developers can leverage built-in functionality of parsing HTML form data from strongly typed view into controller HTTP POST action which takes view model class as a parameter, for example:

[HttpPost]
public ActionResult Foo(FooViewModel viewModel)
{
    // ... code magic here ...

    return View(viewModel);
}

In above example POST request data, in the form of key-value pairs, are parsed (or mapped) into corresponding properties of FooViewModel class. This technique is very handy since developers don’t have to do this parsing/mapping manually from collection of form values. Similar technique can be also applied to parsing/mapping of HTTP GET request URL path into strongly typed class or dynamic object.

Rationale

Routing of URLs to controller actions in ASP.NET MVC web applications is usually “strictly” mapped within Global.asax file which has it’s benefits but also disadvantage which requires to define these routes with parameters in exact order, for example like this well known default route:

routes.MapRoute(
    "Default", // Route name
    "{controller}/{action}/{id}", // URL with parameters
    new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);

When client send GET request to http://localhost/Home/Index/34, web application would fire Index action of Home controller with id parameter containing value 34. However there might be a case where I would rather define a strongly typed class, similar to view model one, which would contain properties that represent parsed/mapped URL path.

For example when client send GET request in the form of http://localhost/Home/Index/Foo/34/Bar/whoa/, I would like to fire Index action of Home controller which receives object containing properties Foo and Bar with numeric value 34 and “whoa” string. Or perhaps I don’t want it strongly typed, but I would rather receive it as a ExpandoObject with dynamic properties. Given URL represents a form of convention where after controller and action follows a path with key-value (or property-value) pairs.

Parsing/mapping of URL path into strongly typed class or dynamic object

In order to achieve above scenario, there have to be a “catch all” route, added below the default one, which doesn’t explicitely define any parameters, but rather pass everything after controller and action as url parameter:

routes.MapRoute(
    "CatchAll",
    "{controller}/{action}/{*url}",
    new { controller = "Home", action = "SomeAction" }
);

Then I create a custom action filter which will abstract parsing/mapping of URL path into strongly typed class or dynamic object.

Strongly typed version

Custom action filter attribute code with strongly typed parsing/mapping:

public class ParsePathAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.RouteData.Values["url"] != null)
        {
            string[] split = filterContext.RouteData.Values["url"].ToString().Split('/');
            var actionModel = filterContext.ActionParameters["actionModel"];
            Type type = actionModel.GetType();

            for (int i = 0; i < split.Count(); i = i + 2)
            {
                if (((i + 1) < split.Count()) && !string.IsNullOrEmpty(split[i]) && !string.IsNullOrEmpty(split[i + 1]))
                {
                    string s = split[i][0].ToString().ToUpper() + split[i].Substring(1);
                    PropertyInfo property = type.GetProperty(s);

                    if (property != null)
                    {
                        bool tryBool;
                        float tryFloat;
                        int tryInt;

                        if (Boolean.TryParse(split[i + 1], out tryBool))
                        {
                            property.SetValue(actionModel, tryBool, null);
                        }
                        else if (Int32.TryParse(split[i + 1], out tryInt))
                        {
                            property.SetValue(actionModel, tryInt, null);
                        }
                        else if (float.TryParse(split[i + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out tryFloat))
                        {
                            property.SetValue(actionModel, tryFloat, null);
                        }
                        else
                        {
                            property.SetValue(actionModel, split[i + 1], null);
                        }
                    }
                }
            }

            filterContext.ActionParameters["actionModel"] = actionModel;
        }

        base.OnActionExecuting(filterContext);
    }
}

Action model class example:

public class TestActionModel
{
    public string Foo { get; set; }
    public int Bar { get; set; }
    public bool IsBaz { get; set; }
    public float Whoa { get; set; }
}

Usage example (when requesting http://localhost/Home/Test/Foo/string/Bar/1/IsBaz/true/Whoa/5.66/):

[ParsePath]
public ActionResult Test(TestActionModel actionModel)
{
    // actionModel.Foo == "string"
    // actionModel.Bar == 1
    // actionModel.IsBaz == true
    // actionModel.Whoa == 5.66

    return View();
}

Dynamic object version

Custom action filter attribute code with dynamic parsing/mapping:

public class ParsePathAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.RouteData.Values["url"] != null)
        {
            dynamic actionModel = new ExpandoObject() as IDictionary<string, Object>;
            string[] split = filterContext.RouteData.Values["url"].ToString().Split('/');

            for (int i = 0; i < split.Count(); i = i + 2)
            {
                if (((i + 1) < split.Count()) && !string.IsNullOrEmpty(split[i]) && !string.IsNullOrEmpty(split[i + 1]))
                {
                    bool tryBool;
                    float tryFloat;
                    int tryInt;

                    if (Boolean.TryParse(split[i + 1], out tryBool))
                    {
                        ((IDictionary<string, Object>)actionModel).Add(split[i], tryBool);
                    }
                    else if (Int32.TryParse(split[i + 1], out tryInt))
                    {
                        ((IDictionary<string, Object>)actionModel).Add(split[i], tryInt);
                    }
                    else if (float.TryParse(split[i + 1], NumberStyles.Any, CultureInfo.InvariantCulture, out tryFloat))
                    {
                        ((IDictionary<string, Object>)actionModel).Add(split[i], tryFloat);
                    }
                    else
                    {
                        ((IDictionary<string, Object>)actionModel).Add(split[i], split[i + 1]);
                    }
                }
            }

            filterContext.ActionParameters["actionModel"] = actionModel;
        }

        base.OnActionExecuting(filterContext);
    }
}

Usage example (when requesting http://localhost/Home/Test/Foo/string/Bar/1/IsBaz/true/Whoa/5.66/):

[ParsePath]
public ActionResult Test(dynamic actionModel)
{
    // actionModel.Foo == "string"
    // actionModel.Bar == 1
    // actionModel.IsBaz == true
    // actionModel.Whoa == 5.66

    return View();
}

Conclusion

Both methods of URL path parsing/mapping offers flexible solution of receiving data sent through URL as a strongly typed or dynamic object action parameter. This might come handy especially in scenarios where URL path key-value pairs are constructed dynamically without strict ordering. It also simplify routing, manipulation and validation of received data.

blog comments powered by Disqus