Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strongly typed url action

I've read multiple posts and blogs similar to

Delegate-based strongly-typed URL generation in ASP.NET MVC

But none of them really quite do what I'd like to do. Currently I have a hybrid approach like:

// 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're controller methods don't have any 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 takes a parameter, then I have to pass a value (that I would never use):

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

So the question is there a way to change the extension method to still require the first parameter to be a method defined on type T that does check to make sure the return parameter is an ActionResult without actually specifying a parameter, something like:

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

So this would pass a pointer to the method (like a reflection MethodInfo) instead of the Func<>, so it wouldn't care about parameters. What would that signature look like if it was possible?

like image 687
Erik Philips Avatar asked Dec 17 '15 22:12

Erik Philips


People also ask

Which of the following actions call a strongly typed view?

Scaffolding Template works based on strongly typed view. The view, that is designed by targeting specific model class object, then that view is called "Strongly Typed View". In strongly typed view , view is bind with corresponding model class object/objects. Scaffolding Template works based on strongly typed view.

What is strongly typed view in MVC with example?

The view which binds to a specific type of ViewModel is called as Strongly Typed View. By specifying the model, the Visual studio provides the intellisense and compile time checking of type. We learnt how to pass data from Controller to View in this tutorial. This is usually done using the ViewBag or ViewData.

What is strongly typed and loosely typed in MVC?

In ASP.NET MVC, we can pass the data from the controller action method to a view in many different ways like ViewBag, ViewData, TempData and strongly typed model object. If we pass the data to a View using ViewBag, TempData, or ViewData, then that view becomes a loosely typed view.


2 Answers

You can't do this:

c => c.MyMethod2

Because that is a method group. Any method in a method group can return void or anything else, so the compiler won't allow it:

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

There may be a method in the group returning an ActionMethod, or none. You need to decide that.

But you don't have to provide a method group anyway. You can just use your existing signature, minus the object routeValues, and call it like this:

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

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

Then your next problem is creating the anonymous object at runtime. Luckily you don't have to, as Url.Action() also has an overload accepting a RouteValueDictionary.

Zip the parameters and arguments together into a dictionary, create a RouteValueDictionary from that, and pass that 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 very naively compiles and invokes every non-constant expression, so may prove to be horribly slow in practice:

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, in the Microsoft ASP.NET MVC Futures package there's the convenient ExpressionHelper.GetRouteValuesFromExpression(expr)‌​, which also handles routing and areas. Your entire method then can be replaced with:

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

It uses a cached expression compiler internally, so it works for all use cases and you won't have to reinvent the wheel.

like image 82
CodeCaster Avatar answered Sep 28 '22 16:09

CodeCaster


As an alternative for other projects, I've recently started using nameof.

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

the only real downside is that they can be mixed and produce incorrect results post compile:

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

The controllers don't match but I don't think it's a big deal. It still throws and error during compiling when the controller name or the method name changes and isn't updated else where in code.

like image 23
Erik Philips Avatar answered Sep 28 '22 18:09

Erik Philips