ASP.NET Core中使用表达式树创建URL的方法
创始人
2024-11-27 17:21:07
0次

表达式树(Expression Tree)

表达式树是不可执行的代码,它只是用于表示一种树状的数据结构,树上的每一个节点都表示为某种表达式类型,大概有25种表达式类型,它们都派生自Expression类。创建表达式树具体有两个优势:

1.对表达式树的代码进行编辑修改,使表达式树中的代码变成动态代码,根据不同的数据库修改树上的代码逻辑从而达到动态切换数据库查询语句的目的,用表达式树可以动态构建针对不同数据库的查询语句。

2.完成类似反射访问未知对象的属性,通过动态构造表达式树,生成委托。

当我们在ASP.NET Core中生成一个action的url会这样写:

var url=_urlHelper.Action("Index", "Home");

这样的写法存在的问题在于我们传递了两个字符串类型的参数,而我们又无法避免对action和controller做重命名操作, 例如将index重命名为default, 你无法通过IDE在重命名action的过程中,将

_urlHelper.Action("Index", "Home");

重构为

UrlHelper.Action("Default", "Home");

所以我们的目标是:设计出具有静态检查的API,让IDE提示出这个错误来,甚至是重命名时直接把相关代码都能重命名。

目标

设计出类似两组API:

var url = _urlHelper.Action((HomeController c) => c.Index());
//期待输出 /home/index
var link = _urlHelper.Link((ProductController c) => c.Details(10));
//期待输出 http://locahost/product/details/10

设计API

根据上面的需求,定义两组API:

public static string Action(this IUrlHelper helper, 
Expression> action)
where TController : Controller
{
 //实现
}

public static string Link(this IUrlHelper helper, 
Expression> action,
string protocal = null, string host = null)
where TController : Controller
{
 //实现
}

实现API

我们实际上最终还是要依赖ASP.NET Core提供的API:

var link = helper.Action(action: actionName, controller: 
controllerName, values: routes);

所以问题变成了如何根据(HomeController c) => c.Index()这样的表达式来解析出actionName, ControllerName以及routeValues。

1. 解析ControllerName

解析ControllerName比较简单粗暴,因为我们已经从表达式树中得到了HomeController这个类型,直接取 Home 字符串即可:

private static string GetControllerName(Type controllerType)
{
 var controllerName = controllerType.Name.EndsWith("Controller")
 ? controllerType.Name.Substring(0,
 controllerType.Name.Length - "Controller".Length)
 : controllerType.Name;
 return controllerName;
}

2. 解析ActionName

由于表达式 (HomeController c) => c.Index() 是一个 MethodCallExpression 类型,而Action的名字就是方法名:

private static MethodCallExpression
GetMethodCallExpression(
Expression> actionSelector)
{
 var call = actionSelector.Body as MethodCallExpression;
 if (call == null)
 {
 throw new ArgumentException("You must call a method on " +
 typeof(TController).Name, "actionSelector");
 }
 
 return call;
}

var methodCallExpression = GetMethodCallExpression(action);
var actionName = methodCallExpression.Method.Name;

3. 解析RouteValues

上面两步已经解析出了ControllerName和ActionName,也就是说通过上面的分析已经能完成下面的调用:

var action = helper.Action(action: "index", controller: "home", values: null);
//等价于
var url = _urlHelper.Action((HomeController c) => c.Index());
//输出 /home/index

但是考虑下面的Action:

[HttpGet,Route("product/{id}")]
public IActionResult Details(int id)
{
 //...
}

这个Action期待传入一个int类型的id,也就是说你要通过这样的方式来生成url:

var action = helper.Action(action: "details", controller: 
"product", values: new { id = 10 });

所以要想让我们的API正常工作,还需要生成一个object类型:new { id = 10 } 。而这个object类型里面的属性正好可以来自于表达式树的方法调用参数:

var action = _urlHelper.Action((ProductController c) => c.Details(10));

要想生成这个匿名对象,需要遍历方法调用表达式的所有参数,分别解析出属性名,例如id; 以及值,例如10。最后再把解析出来的参数字典生成为dynamic类型的对象:

如何解析表达式树请查看expression-trees 。

public class RouteValueExtractor
{
 public static object GetRouteValues(MethodCallExpression call)
 {
 var routes = new Dictionary();

 var parameters = call.Method.GetParameters();
 var pairs = call.Arguments.Select((a, i) => new
 {
  Argument = a,
  ParamName = parameters[i].Name
 });
 foreach (var item in pairs)
 {
  string name = item.ParamName;
  object value = GetValue(item.Argument);
  if (value != null)
  {
  var valueType = value.GetType();
  if (valueType.IsValueType)
  {
   routes.Add(name, value);
  }
  else
  {
   throw new NotSupportedException("Unsupported parameter type {0}");
  }

  }
 }

 return DictionaryToObject(routes);
 }

 private static object GetValue(Expression expression)
 {
 if (expression.NodeType == ExpressionType.Constant)
 {
  return ((ConstantExpression) expression).Value;
 }

 throw new NotSupportedException("Unsupported parameter expression");
 }

 private static dynamic DictionaryToObject(IDictionary dictionary)
 {
 var expandoObj = new ExpandoObject();
 var expandoObjCollection = (ICollection>) expandoObj;

 foreach (var keyValuePair in dictionary)
 {
  expandoObjCollection.Add(keyValuePair);
 }

 dynamic eoDynamic = expandoObj;
 return eoDynamic;
 }
}

一个完整的API实现:

public static string Action(this IUrlHelper helper, 
Expression> action)
where TController : Controller
{
 var controllerName = GetControllerName(typeof(TController));
 var methodCallExpression = GetMethodCallExpression(action);
 var actionName = methodCallExpression.Method.Name;

 var routes = RouteValueExtractor.GetRouteValues(methodCallExpression);

 var link = helper.Action(action: actionName, controller: 
 controllerName, values: routes);

 return link;
}

相关内容

热门资讯

空调连续工作多久需要停机休息 空调连续工作的时间取决于多个因素,包括空调的型号、功率、使用环境和维护情况等。一般来说,空调可以连续...
空调哪个符号是除湿的 空调除湿功能通常用一个水滴形状的符号表示。不同品牌和型号的空调遥控器可能会有所差异,但一般来说,除湿...
格力空调多长时间加一次氟利昂 1、一般情况下,空调对氟利昂的使用量是非常小的,所以在空调使用了四年左右的时候再考虑加也没问题。2、...
空调用多长时间需要加氟一次 一般情况下是五年。当出现以下这几种情况时,空调才需要加氟。1、空调的室内机和室外机各有两个接口,当接...
格调生活·C标思享荟:卡萨帝在... 9月12日的郑州龙湖金融岛,夜风轻拂,灯光璀璨。“C标时刻共传奇——卡萨帝思享荟暨20周年品牌盛典郑...
品牌价值破千亿、高端份额持续领... 9月12日,“C标时刻共传奇——卡萨帝思享荟暨20周年品牌盛典郑州站”在郑州龙湖金融岛举办。2026...
空调主板坏了修800还是换新的 空调主板坏了是修还是换新,需要考虑多个因素,以下是一些建议:1. 维修成本:如果维修费用较低,比如 ...
空调开多久需要休息多长时间 问题:空调开多久需要休息多长时间回答:1、在使用空调的过程中,一般空调开7~8个小时左右需要休息2~...
中央空调多长时间清洗一次 1、一般空调开机前清洗一次即可,空调开机中间时段再清洗一次,空调关机时清洗一次,这样比较合理。2、空...
中央空调氟利昂多久加一次 看空调制热和制冷程度,很多人的观点认为氟利昂必须一年放一次,其实这个观点是错误的,理解上面有点偏差,...