Strongly typed url action

I read several posts and blogs similar to

Delegation-based URL Generation in ASP.NET MVC

But none of them really do what I would like to do. I currently have a hybrid approach, for example:

// shortened for Brevity public static Exts { public string Action(this UrlHelper url, Expression<Func<T, ActionResult>> expression) where T : ControllerBase { return Exts.Action(url, expression, null); } public string Action(this UrlHelper url, Expression<Func<T, ActionResult>> expression, object routeValues) where T : ControllerBase { string controller; string action; // extension method expression.GetControllerAndAction(out controller, out action); var result = url.Action(action, controller, routeValues); return result; } } 

Works great if you have controller methods, no parameters:

 public class MyController : Controller { public ActionResult MyMethod() { return null; } public ActionResult MyMethod2(int id) { return null; } } 

Then I can:

 Url.Action<MyController>(c => c.MyMethod()) 

But if my method accepts a parameter, then I need to pass a value (which I never used):

 Url.Action<MyController>(c => c.MyMethod2(-1), new { id = 99 }) 

So the question is, is there a way to change the extension method to still require that the first parameter be a method defined in type T , which checks that the returned parameter is an ActionResult without actually specifying the parameter, something like:

 Url.Action<MyController>(c => c.MyMethod2, new { id = 99 }) 

This way it will pass a pointer to a method (for example, a reflection of MethodInfo ) instead of Func<> , so it does not care about the parameters. What would this signature look like if it were possible?

+8
c # expression asp.net-mvc func
source share
2 answers

You cannot do this:

 c => c.MyMethod2 

Because it is a group of methods. Any method in a method group can return void or something else, so the compiler will not allow this:

 Error CS0428 Cannot convert method group '...' to non-delegate type '...' 

The group may have a method that returns ActionMethod , or none. You need to solve this.

But you still don't need to provide a group of methods. You can simply use the existing signature minus object routeValues , and name it like this:

 Url.Action<MyController>(c => c.MyMethod(99)) 

Then in your method, you can use MethodInfo methodCallExpression.Method to get the names of the method parameters and methodCallExpression.Arguments to get the arguments.

Then your next problem is creating an anonymous object at runtime. Fortunately, you do not need to, because Url.Action() also has an overload that accepts a RouteValueDictionary .

Replace the parameters and arguments together in the dictionary, create a RouteValueDictionary from it RouteValueDictionary and go to Url.Action() :

 var methodCallExpression = expression.Body as MethodCallExpression; if (methodCallExpression == null) { throw new ArgumentException("Not a MethodCallExpression", "expression"); } var methodParameters = methodCallExpression.Method.GetParameters(); var routeValueArguments = methodCallExpression.Arguments.Select(EvaluateExpression); var rawRouteValueDictionary = methodParameters.Select(m => m.Name) .Zip(routeValueArguments, (parameter, argument) => new { parameter, argument }) .ToDictionary(kvp => kvp.parameter, kvp => kvp.argument); var routeValueDictionary = new RouteValueDictionary(rawRouteValueDictionary); // action and controller obtained through your logic return url.Action(action, controller, routeValueDictionary); 

The EvaluateExpression method compiles and calls every mutable expression very naively, so in practice it may seem pretty slow:

 private static object EvaluateExpression(Expression expression) { var constExpr = expression as ConstantExpression; if (constExpr != null) { return constExpr.Value; } var lambda = Expression.Lambda(expression); var compiled = lambda.Compile(); return compiled.DynamicInvoke(); } 

However, the Microsoft ASP.NET MVC Futures package has a convenient ExpressionHelper.GetRouteValuesFromExpression(expr)β€Œβ€‹ which also handles routing and areas. Then your whole method can be replaced by:

 var routeValues = Microsoft.Web.Mvc.Internal.ExpressionHelper.GetRouteValuesFromExpression<T>(expression); return url.Action(routeValues["Action"], routeValues["Controller"], routeValues); 

It uses a built-in expression compiler in the cache, so it works for all uses, and you don’t have to reinvent the wheel.

+3
source share

As an alternative to other projects, I recently started using nameof .

 Url.Action(nameof(MyController.MyMethod), nameof(MyController), new { id = 99 }) 

the only real flaw is that they can be mixed and produce incorrect results after compilation:

 Url.Action(nameof(SomeOtherController.MyMethod), nameof(MyController), new { id = 99 }) 

The controllers do not match, but I do not think it matters a lot. It still throws a compilation error when changing the name of the controller or method and is not updated elsewhere in the code.

0
source share

All Articles