强类型url操作

本文关键字:操作 url 强类型 | 更新日期: 2023-09-27 18:27:19

我读过很多类似的帖子和博客

ASP.NET MVC 中基于委托的强类型URL生成

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

// 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上定义的方法,会进行检查,以确保返回参数是ActionResult,而不实际指定参数,例如:

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

因此,这将向传递一个指向方法的指针(类似于反射MethodInfo),而不是Func<>,因此它不关心参数。如果可能的话,签名会是什么样子?

强类型url操作

你不能这样做:

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包中有一个方便的ExpressionHelper.GetRouteValuesFromExpression(expr)‌​,它还处理路由和区域。然后,您的整个方法可以替换为:

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

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

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

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

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

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

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

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

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

services.AddMvc().AddTypedRouting();

或者如果你正在开发一个网络API:

services.AddControllers().AddTypedRouting();

然后,你可以获得带有表达式的URL,就像你想一样

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