强类型 url 操作
Strongly typed url action
我读过多篇类似于
的帖子和博客
Delegate-based strongly-typed URL generation in ASP.NET MVC
但是 none 他们确实做了我想做的事情。目前我有一个混合方法,如:
// 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
上的方法确保 return 参数是一个 ActionResult
而没有实际指定参数,例如:
Url.Action<MyController>(c => c.MyMethod2, new { id = 99 })
所以这会传递一个指向方法的指针(就像反射MethodInfo
)而不是Func<>
,所以它不会关心参数。如果可能的话,签名会是什么样子?
你不能这样做:
c => c.MyMethod2
因为那是一个方法组。方法组中的任何方法都可以 return void 或任何其他内容,因此编译器不允许:
Error CS0428 Cannot convert method group '...' to non-delegate type '...'
组 returning 一个 ActionMethod
或 none 中可能有 a 方法。你需要做出决定。
但无论如何您都不必提供方法组。您可以只使用现有的签名,减去 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);
它在内部使用缓存表达式编译器,因此它适用于所有用例,您无需重新发明轮子。
作为其他项目的替代方案,我最近开始使用 nameof。
Url.Action(nameof(MyController.MyMethod), nameof(MyController), new { id = 99 })
唯一真正的缺点是它们可以混合并产生不正确的结果post编译:
Url.Action(nameof(SomeOtherController.MyMethod), nameof(MyController), new { id = 99 })
控制器不匹配,但我认为这没什么大不了的。当控制器名称或方法名称更改并且代码中的其他地方未更新时,它仍然会在编译期间抛出错误。
Ivaylo Kenov 创建了一个 Nuget 包 AspNet.Mvc.TypedRouting (Github repository)
只需调用 AddTypedRouting
扩展方法 在 AddMvc
一个
之后
services.AddMvc().AddTypedRouting();
或者如果您正在开发网络 API:
services.AddControllers().AddTypedRouting();
然后您可以根据需要获取带有表达式的 URL
Url.Action<FooController>(c => c.Get(fooId));
我读过多篇类似于
的帖子和博客Delegate-based strongly-typed URL generation in ASP.NET MVC
但是 none 他们确实做了我想做的事情。目前我有一个混合方法,如:
// 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
上的方法确保 return 参数是一个 ActionResult
而没有实际指定参数,例如:
Url.Action<MyController>(c => c.MyMethod2, new { id = 99 })
所以这会传递一个指向方法的指针(就像反射MethodInfo
)而不是Func<>
,所以它不会关心参数。如果可能的话,签名会是什么样子?
你不能这样做:
c => c.MyMethod2
因为那是一个方法组。方法组中的任何方法都可以 return void 或任何其他内容,因此编译器不允许:
Error CS0428 Cannot convert method group '...' to non-delegate type '...'
组 returning 一个 ActionMethod
或 none 中可能有 a 方法。你需要做出决定。
但无论如何您都不必提供方法组。您可以只使用现有的签名,减去 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);
它在内部使用缓存表达式编译器,因此它适用于所有用例,您无需重新发明轮子。
作为其他项目的替代方案,我最近开始使用 nameof。
Url.Action(nameof(MyController.MyMethod), nameof(MyController), new { id = 99 })
唯一真正的缺点是它们可以混合并产生不正确的结果post编译:
Url.Action(nameof(SomeOtherController.MyMethod), nameof(MyController), new { id = 99 })
控制器不匹配,但我认为这没什么大不了的。当控制器名称或方法名称更改并且代码中的其他地方未更新时,它仍然会在编译期间抛出错误。
Ivaylo Kenov 创建了一个 Nuget 包 AspNet.Mvc.TypedRouting (Github repository)
只需调用 AddTypedRouting
扩展方法 在 AddMvc
一个
services.AddMvc().AddTypedRouting();
或者如果您正在开发网络 API:
services.AddControllers().AddTypedRouting();
然后您可以根据需要获取带有表达式的 URL
Url.Action<FooController>(c => c.Get(fooId));