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?
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.
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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With