【问题标题】:Strongly typed url action强类型 url 操作
【发布时间】:2023-03-30 17:55:02
【问题描述】:

我已经阅读了多个类似的帖子和博客

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

但他们都没有真正做到我想做的事。目前我有一种混合方法,例如:

// 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;
  }
}

如果您的控制器方法没有任何参数,则效果很好:

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

那么我可以:

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

但是如果我的方法需要一个参数,那么我必须传递一个值(我永远不会使用):

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

所以问题是有没有办法改变扩展方法,仍然要求第一个参数是在类型 T 上定义的方法,确实检查以确保返回参数是 @ 987654327@ 没有实际指定参数,例如:

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

所以这将传递一个指向方法的指针(就像一个反射MethodInfo)而不是Func&lt;&gt;,所以它不会关心参数。如果可能,该签名会是什么样子?

【问题讨论】:

  • 使用c.MyMethod2,您指向的是一个方法group,其中任何一个都可以返回其他内容...但我很确定我已经看到了这样的库启用此功能。也许您可以做一些反射魔术并检查GetControllerAndAction 与提供的参数匹配的组的方法确实返回ActionResult。这不会完全为您提供您正在寻找的编译时安全性,但无论如何您都不应该在控制器中将非操作方法作为公共方法。
  • c =&gt; c.MyMethod2 不能从 Method Group 转换为非委托类型 ActionResult
  • 您当然是对的,这仅适用于当前控制器,而不适用于视图。你为什么不使用c =&gt; c.MyMethod2(99) 代替(使用MethodCallExpression.Arguments 来获取参数)?
  • 让我考虑一下...我故意删除了该代码,因为它看起来模棱两可:c =&gt; c.MyMethod(99), new { id = 98 }...但也许我那时不需要路由值...我是试图想出一个我需要路由值的原因......

标签: c# asp.net-mvc expression func


【解决方案1】:

你不能这样做:

c => c.MyMethod2

因为那是一个方法组。方法组中的任何方法都可以返回 void 或其他任何内容,因此编译器不会允许:

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

组中可能有一个方法返回ActionMethod,或者没有。你需要做出决定。

但无论如何您都不必提供方法组。您可以只使用现有的签名,减去object routeValues,然后这样称呼它:

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

然后在你的方法中,你可以使用MethodInfo methodCallExpression.Method获取方法参数名,methodCallExpression.Arguments获取参数。

那么你的下一个问题是在运行时创建匿名对象。幸运的是,您不必这样做,因为 Url.Action() 也有一个接受 RouteValueDictionary 的重载。

将参数和参数一起压缩到字典中,从中创建一个RouteValueDictionary,并将其传递给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);

EvaluateExpression 方法非常天真地编译和调用每个非常量表达式,因此在实践中可能会非常缓慢:

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();
}

但是,在Microsoft ASP.NET MVC Futures package 中有方便的ExpressionHelper.GetRouteValuesFromExpression(expr)‌​,它也处理路由和区域。然后可以将您的整个方法替换为:

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

它在内部使用缓存的表达式编译器,因此它适用于所有用例,您不必重新发明轮子。

【讨论】:

  • 如果它们不是ConstantExpression,我是否必须编译并执行该表达式才能获得RouteValueDictionary 的值?
  • 我不能使用 Futures 包,但我可以 compile() 表达式并希望将值用作对象..
  • @Erik 我已经做到了,请参阅编辑和警告。它适用于我的常量、方法调用和成员访问,但你真的必须自己测试它。我认为您还必须在控制器上查找 Area 属性并将其添加到 routeValueDictionary 如果您希望它与区域一起使用。
  • 我要做的是检查控制器的命名空间。如果我发疯了,我可以根据 AreaRegistration 验证它:)
【解决方案2】:

作为其他项目的替代方案,我最近开始使用nameof

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

唯一真正的缺点是它们可能混合在一起并在编译后产生不正确的结果:

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

控制器不匹配,但我认为这没什么大不了的。当控制器名称或方法名称发生更改并且在代码中的其他位置未更新时,它仍然会在编译期间抛出错误。

【讨论】:

    【解决方案3】:

    Ivaylo Kenov 创建了一个 Nuget 包 AspNet.Mvc.TypedRouting (Github repository)

    只需调用AddTypedRouting 扩展方法 AddMvc 一个

    services.AddMvc().AddTypedRouting();
    

    或者,如果您正在开发 Web API:

    services.AddControllers().AddTypedRouting();
    

    然后你就可以随心所欲地获取带有表达式的网址

    Url.Action<FooController>(c => c.Get(fooId));
    

    【讨论】:

    • 这正是我不想想要做的;我不想传递所需的参数。
    • @ErikPhilips 这不是你在问题中所说的。你说你想要像Url.Action&lt;MyController&gt;(c =&gt; c.MyMethod2, new { id = 99 }) 这样的东西,你传递id 参数,但避免传递一个未使用的值-1,就像Url.Action&lt;MyController&gt;(c =&gt; c.MyMethod2(-1), new { id = 99 }) 一样。我认为这正是回答你的答案
    • 所以问题是有没有办法改变扩展方法,仍然要求第一个参数是在类型 T 上定义的方法,它确实检查以确保返回参数是 ActionResult 没有实际指定一个参数,类似于:
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-08
    • 1970-01-01
    • 1970-01-01
    • 2011-02-20
    • 1970-01-01
    相关资源
    最近更新 更多