Hey people!
After trying CodeGeneration which didn't really work out, i had a
brain fart and thought about using linq expressions to perform
strongly typed redirection. The result is below, it is really basic
and still lacks important stuff but it's about the idea :)
The idea is this:
Suppose you have controller LoginController with an action defined
with the signature Login(string username, string password)
You basically feed the RedirectToActionTyped extension method a lamda
in the form of c => c.Login("henk", "pass"). Then the extension dives
deep into the bowels of System.Linq and it invokes
LoginController.RedirectToAction("Login", {username="henk",
password="pass"})
If someone has any suggestions/comments on this technique, i'm very
interested!
To make this more useful i thought of these things:
- support [DataBind], [ARDataBind] and [ARFetch] attributes on the
parameters, perhaps with some help of SmartDispatcherController. For
example action Edit([ARDataBind("id")] Car car) now just comes up with
Edit.rails?car={car.ToString()}
- Enable redirection to any controller using the type of the target
controller and a IEngineRequest/IResponse, perhaps extend IReponse
instead. This requires the extension to find out the controller area
and name.
Oh just found out that Controller has AreaName and Name properties...
- How do i test this? With unit tests for example, is mocking needed?
Any thoughts?
Here's the code:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Castle.MonoRail.Framework;
public static class RedirectExtensions
{
private static readonly Assembly linqAssembly =
typeof(Expression).Assembly;
private const BindingFlags methodFlags = BindingFlags.Instance |
BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
/// <summary>
/// Performs a strongly typed redirect to an action of the <paramref
name="controller"/>.
/// </summary>
/// <typeparam name="TController">The type of the controller that
has the actions.</typeparam>
/// <param name="controller">The controller that has the actions.</
param>
/// <param name="redirect">
/// A redirect expression. This must be a lamda in the form of
/// <c>c => c.Action(optional, action, parameters)</c>,
/// where the action parameters can be anything you want (field,
property, method call etc.)
/// as long as they return the right parameter and the lambda
function will compile.
/// </param>
public static void RedirectToActionTyped<TController>(
this TController controller,
Expression<Action<TController>> redirect)
where TController : Controller
{
Argument.NotNull(controller, "controller");
Argument.NotNull(redirect, "redirect");
var methodCall = redirect.Body as MethodCallExpression;
if (methodCall == null)
{
Argument.Throw("redirect",
"Redirect expression must be a MethodCallExpression, " +
"that is, a lamda in the form of 'c => c.Action(optional, action,
parameters)', " +
"where the action parameters can be anything you want (field,
property, method call etc.) " +
"as long as they return the right parameter and the function will
compile.");
}
string action =
methodCall.Method.Name;
var queryParams = GetQueryParameters(methodCall);
DoTheRedirect(controller, action, queryParams);
}
private static Dictionary<string, object>
GetQueryParameters(MethodCallExpression methodCall)
{
var name2value = new Dictionary<string, object>();
if (methodCall.Arguments == null || methodCall.Arguments.Count ==
0)
{
return name2value;
}
var parameters = methodCall.Method.GetParameters();
for (int i = 0; i < parameters.Length; i++)
{
Expression argExp = methodCall.Arguments[i];
object argValue = GetParameterValue(argExp);
name2value.Add(parameters[i].Name, argValue);
}
return name2value;
}
private static object GetParameterValue(Expression param)
{
Type executorType =
linqAssembly.GetType("System.Linq.EnumerableExecutor");
MethodInfo createMethod = executorType.GetMethod("Create",
methodFlags);
object executor = createMethod.Invoke(null, new object[]
{ param });
MethodInfo executeMethod =
executor.GetType().GetMethod("ExecuteBoxed", methodFlags);
object value = executeMethod.Invoke(executor, null);
return value;
}
private static void DoTheRedirect(Controller controller, string
action, IDictionary queryStringParameters)
{
//Console.WriteLine("Invoking action {0}/{1} with {2} parameters.",
// controller.GetType().Name, action,
queryStringParameters.Count);
controller.RedirectToAction(action, queryStringParameters);
}
}