ASP. NET How to create URL using expression tree in Core

  • 2021-10-27 06:58:36
  • OfStack

Expression tree (Expression Tree)

An expression tree is non-executable code. It is only used to represent a tree-like data structure. Every node in the tree is represented as an expression type. There are about 25 expression types, all of which are derived from the Expression class. Creating an expression tree has two specific advantages:

1. Edit and modify the code of the expression tree, make the code in the expression tree become dynamic code, modify the code logic in the tree according to different databases, so as to achieve the purpose of dynamically switching database query statements, and use the expression tree to dynamically build query statements for different databases.

2. Complete similar reflection to access the attributes of unknown objects, and generate delegates by dynamically constructing expression trees.

When we generate an url of action in ASP. NET Core, we write this:


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

The problem with this is that we pass two string-type arguments, and we can't avoid renaming action and controller. For example, renaming index to default, you can't rename action through IDE


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

Refactoring to


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

So our goal is to design API with static checking, so that IDE can prompt this error, and even rename the related code directly when renaming.

Objectives

Two similar sets of API were designed:


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

Design API

According to the above requirements, define two sets of API:


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

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

Implementation of API

We actually end up relying on the API provided by ASP. NET Core:


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

So the question becomes how to base (HomeController c) => c.Index() This expression parses actionName, ControllerName, and routeValues.

1. Parsing ControllerName

Parsing ControllerName is simple and rude, because we have got the type HomeController from the expression tree, so just take the Home string:


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. Parsing ActionName

Because of the expression (HomeController c) => c.Index() Is an MethodCallExpression type, and the name of Action is the method name:


private static MethodCallExpression
GetMethodCallExpression<TController>(
Expression<Action<TController>> 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. Parsing RouteValues

ControllerName and ActionName have been parsed out in the above two steps, which means that the following calls can be completed through the above analysis:


var action = helper.Action(action: "index", controller: "home", values: null);
// Equivalent to 
var url = _urlHelper.Action((HomeController c) => c.Index());
// Output  /home/index

But consider the following Action:


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

This Action expects to pass in an id of type int, which means that you will generate url in this way:


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

So for our API to work properly, we need to generate an object type: new { id = 10 } . The attributes in this object type can just come from the method call parameters of the expression tree:


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

To generate this anonymous object, you need to traverse all the parameters of the method call expression and resolve the property names respectively, such as id; And a value, such as 10. Finally, the parsed parameter dictionary is generated into an object of dynamic type:

See expression-trees for how to resolve the expression tree.


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

1 complete API implementation:


public static string Action<TController>(this IUrlHelper helper, 
Expression<Action<TController>> 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;
}

Summarize


Related articles: