eWorld.UI - Matt Hawley

Ramblings of Matt

ASP.NET MVC: Expression Based RedirectToAction Method

May 14, 2008 21:29 by matthaw

Update: I've updated the source to be compatible with the Preview 3 bits. Download and execution is the same, but the sample code within the post is no longer valid.

Since the introduction of lambda expressions within the .NET framework, and it's extensive use of them within ASP.NET MVC, I've grown extremely fond of working with compile time errors that lambda expressions gives us. You've seen the ASP.NET MVC team build out a set of ActionLink<T> methods that enable you to specify an expression that will be compiled like the following

<%= Html.ActionLink<UserController>(c => c.Login(), "Login") %>

As the team released their April push and introduced us to ActionResult return types, I've totally fell in love with using these, mainly from a testability standpoint, and it also makes your code extremely readable to determine exactly what is going to be happening at what point. However, what I've found is that when using RedirectToAction, I would constantly be writing code like

   1:  public class UserController : Controller
   2:  {
   3:     public ActionResult Login() { ... }
   4:     public ActionResult ProcessLogin()
   5:     {
   6:        // ... determine if error'd
   7:        return RedirectToAction("Login");
   8:     }
   9:  }

Yup, nothing fancy and pretty straightforward. However, what if I needed to refactor my codebase and change my action method names... the task becomes straight forward when using ActionLink<T> in my views, but all of my controllers continue to compile even when they shouldn't be! Yes, if you're doing true TDD, this task is easy to spot because after refactoring, you can run all of your test cases and see which ones failed.

 

Great. That's a lot of manual work...mmmmkay. In the age of having refactoring shortcuts built right into the IDE, why couldn't I change my method name using refactoring, and have IT do all the work for me? Enter, the expression based RedirectToAction method. I know, you saw it coming, right? Here's how I want to write my above code

   1:  public class UserController : Controller
   2:  {
   3:     public ActionResult Login() { ... }
   4:     public ActionResult ProcessLogin()
   5:     {
   6:        // ... determine if error'd
   7:        return this.RedirectToAction(c => c.Login());
   8:     }
   9:  }

Ohh, pretty - and hey look, refactoring now works! It even supports parameters, route value dictionaries, anonymous types. Even better, you can specify a completely different controller to route to!

   1:  // parameters, route dictionaries, anonmyous types
   2:  this.RedirectToAction(c => c.Login("matt"));
   3:  this.RedirectToAction(c => c.Login(), new RouteValueDictionary(new { userName = "matt" }));
   4:  this.RedirectToAction(c => c.Login(), new { userName = "matt" }));
   5:   
   6:  // different controller
   7:  this.RedirectToAction<ProductsController>(c => c.View(101));

All this is super powerful, and I'm sure your dying to get your hands on the source...okay :)

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq.Expressions;
   4:  using System.Web.Mvc;
   5:  using System.Web.Routing;
   6:   
   7:  public static class ControllerExtensions
   8:  {
   9:      public static ActionRedirectResult RedirectToAction<T>(this T controller, 
  10:          Expression<Action<T>> action) where T : Controller
  11:      {
  12:          return RedirectToAction<T>(controller, action, null);
  13:      }
  14:   
  15:      public static ActionRedirectResult RedirectToAction<T>(this T controller, 
  16:          Expression<Action<T>> action, object values) where T : Controller
  17:      {
  18:          return RedirectToAction<T>(controller, action, new RouteValueDictionary(values));
  19:      }
  20:   
  21:      public static ActionRedirectResult RedirectToAction<T>(this T controller, 
  22:          Expression<Action<T>> action, RouteValueDictionary values) where T : Controller
  23:      {
  24:          MethodCallExpression body = action.Body as MethodCallExpression;
  25:          if (body == null)
  26:              throw new InvalidOperationException("Expression must be a method call.");
  27:   
  28:          if (body.Object != action.Parameters[0])
  29:              throw new InvalidOperationException("Method call must target lambda argument.");
  30:   
  31:          string actionName = body.Method.Name;
  32:          string controllerName = typeof(T).Name;
  33:   
  34:          if (controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
  35:              controllerName = controllerName.Remove(controllerName.Length - 10, 10);
  36:   
  37:          RouteValueDictionary parameters = LinkBuilder.BuildParameterValuesFromExpression(body);
  38:   
  39:          values = values ?? new RouteValueDictionary();
  40:          values.Add("controller", controllerName);
  41:          values.Add("action", actionName);
  42:   
  43:          if (parameters != null)
  44:          {
  45:              foreach (KeyValuePair<string, object> parameter in parameters)
  46:              {
  47:                  values.Add(parameter.Key, parameter.Value);
  48:              }
  49:          }
  50:   
  51:          return new ActionRedirectResult(values);
  52:      }
  53:  }

As you can see, it's fairly straight forward. This is something that I think is very useful, and gets more in line with how code should be written. Since this example is a bit lengthy, I've provided the source in downloadable format, so click here to get it. Hope you enjoy and use this in your applications!

kick it on DotNetKicks.com

Comments

May 22. 2008 22:58

Legend,
I had something similar, extending the RequestContext and returning:

VirtualPathData vpd = req.RouteData.Route.GetVirtualPath(req, values);
return (vpd == null) ? null : vpd.VirtualPath;

Much like the original BuildUrlFromExpression method, but for some reason it was returning null and I've been looking for a solution since. Thanks!

Harry

May 23. 2008 00:03

Pingback from weblogs.asp.net

ASP.NET MVC: Expression Based RedirectToAction Method - eWorld.UI - Matt Hawley

weblogs.asp.net

May 23. 2008 00:10

Is there any reason you need these methods to be extensions on the controller? I removed the controller parameter in the methods so I could use your code to redirect from an ActionFilterAttribute, which has no Controller reference, but allows me to do the following:

UrlHelper.RedirectToAction<FooController>(s => s.Bar()).ExecuteResult(filterContext);
//I added your methods as statics to a custom UrlHelper btw.

Out of Curiosity, do you know how to obtain the virtual path for this redirect? I mentioned some code earlier:

VirtualPathData vpd = req.RouteData.Route.GetVirtualPath(req, values);
return (vpd == null) ? null : vpd.VirtualPath;

which would replace your

return new ActionRedirectResult(values);

Where req is the RequestContext (which ControllerContext extends). For some reason the virtual path approach works when using a controllers context but returns null when using the filterContext (which extends ControllerContext) of an ActionFilterAttribute .

Any ideas why?

(P.S. I think your approach is neater, but there may be cases where you need to create an absolute path for use with assets outside the domain)

Harry

May 23. 2008 01:19

@Harry - The reason I took this approach was to extend what was already there for the April bits of MVC for RedirectToAction. A action filter attribute, while could be used to do this, doesn't make any sense because the purpose of ActionResult's are to do things like redirect to actions, render views, redirect to url's or whatever else.

And when you say virtual path, do you mean ~/views/foo/bar.aspx or "/foo/bar" ? The latter of the two you can get with what your doing, but the former requires you to get the ViewEngine, and get it's associated ViewLocator to map the controller & action to the actual file.

matthaw

May 23. 2008 01:45

@Matt - The action filter redirected to a virtual path, this is why I needed the method. I agree your approach is better as I only need to redirect, not get an actual virtual path, I was just curious why it didn't work. I dug a little deeper and found that when I used my approach from the controllers context it returns a valid virtual path (of the latter type) in actions that have at least one parameter, but returns null in actions with no parameters. So my approach fails at the controller level were it would be useful to get this information and to me this seems like a bug in MVC... To clarify:

In regards to this line: string q = this.BuildUrlFromExpression<FooController>(s => s.Detail(2));

calling it from *inside* a controller action with no params returns null, calling it from *inside* a controller action with one param returns the correct path. My filter that was not working was on a controller method that had no params, which I believe is why it broke.

Does this seem like a bug to you?

Harry

May 23. 2008 06:44

It's quite possible that it didn't have enough information to match the route, copy down the source from CodePlex and debug in to see what's happening. I couldn't tell you why.

matthaw

May 26. 2008 08:32

your download url is broken

bobo

May 26. 2008 08:33

using my powers of deduction the correct url should be www.eworldui.net/.../...rectToActionExtensions.zip

bobo

May 26. 2008 10:04

@bobo - thanks, link is fixed.

matthaw

May 28. 2008 00:19

Hi Matt,
Thanks for this code. It's exactly what I was looking for. I fixed a small bug that occured when I was redirecting from controller A to  controller B and took the liberty to merge you class with mine that had exactly the same name and added download ActionResult methods.
I put the code on my new blog (old blog is in french and I doubt it helps a lot of people !) :
weblogs.asp.net/.../MVControllerExtensions.aspx

Damien

July 13. 2008 17:03

Pingback from jason.whitehorn.ws

Jason Whitehorn - ASP.NET MVC - Decoupling RedirectToAction

jason.whitehorn.ws

March 23. 2009 01:10

Nice one actually

Bayram Çelik

March 26. 2009 09:38

Funny, I wrote almost the exact some code today in my project. Smile  Glad to see that someone else thought the same way.

Jon Kruger

July 12. 2013 05:22

Pingback from dharaa.biz

.NET MVC 3: How to hide parameters in a RedirectToAction GET Action | Dharaa

dharaa.biz

November 24. 2014 23:40

Pingback from ecanswers.org

Multiple Possible RedirectToAction | Zerby

ecanswers.org

Comments are closed

Copyright © 2000 - 2024 , Excentrics World