自定义解析绑定
Fireasy 对一些常见的字符串、日期等类型的方法或属性提供了解析,比如说 string
的 Substring、string
的 Length 属性、DateTime
的 AddMonths 方法等等,如果你要使你自己扩展的方法也可用于 lambda
表达式解析,此时自定义解析绑定可能对你会有帮助。
以下的示例用来说明如何对 string
的扩展方法 LeftString 进行解析绑定。首先定义扩展方法 RightString(该方法是取右边的 n 个字符串),并在方法上添加特性 MethodCallBindAttribute
,指定绑定类 RightStringBinder
。
/// <summary>
/// 自定义函数库。
/// </summary>
public static class Funcs
{
/// <summary>
/// 取字符串的右边n个字符。
/// </summary>
/// <param name="str">字符串。</param>
/// <param name="length">长度。</param>
/// <returns></returns>
[MethodCallBind(typeof(RightStringBinder))]
public static string RightString(this string str, int length)
{
throw new InvalidOperationException("不能直接使用该方法。");
}
}
定义解析绑定类 RightStringBinder
,使其实现 IMethodCallBinder
接口。
/// <summary>
/// 方法 LeftString 的绑定。
/// </summary>
private class RightStringBinder : IMethodCallBinder
{
public Expression Bind(MethodCallBindContext context)
{
var arguments = context.Visitor.Visit(context.Expression.Arguments);
// ret = str.Substring(str.Length - length, length)
var lenExp = Expression.MakeMemberAccess(arguments[0], typeof(string).GetProperty("Length"));
var startExp = Expression.Subtract(lenExp, arguments[1]);
return Expression.Call(arguments[0], "Substring", new Type[0], startExp, arguments[1]);
}
}
实现 Bind 方法,该方法使用调用参数来构造表达式。MethodCallExpression
的参数 Arguments 与扩展方法 RightString 中的参数对象,即 arguments[0] 为字符串 str,arguments[1] 为截取的长度 length。构造表达式,用 lenExp 表示字符串的长度,startExp 表示截取的起始位置,最后调用 string.Substring 方法获取字符串。
现在,一起来写测试用例验证这个神奇的功能!
[TestMethod]
public void TestCustomBind()
{
using (var db = new DbContext())
{
//使用 string 的自定义扩展方法筛选以 ee 结尾的数据
var list = db.Products.Where(s => s.ProductName.RightString(2) == "ee");
}
}
甚至,你可以在解析绑定中使用 SQL
子查询表达式,比如下面的例子实现数据权限的筛选功能。
/// <summary>
/// 自定义函数库。
/// </summary>
public static class Funcs
{
/// <summary>
/// 检查用户的数据权限。
/// </summary>
/// <param name="userId">用户ID。</param>
/// <param name="deptId">科室ID。</param>
/// <returns></returns>
[MethodCallBind(typeof(CheckDataPermissionBinder))]
public static bool CheckDataPermission(int userId, int deptId)
{
throw new InvalidOperationException("不能直接使用该方法。");
}
}
CheckDataPermissionBinder
实现了一个 SQL
子查询,大致思路是通过 userId 查询到角色对应的数据权限(deptId集合),然后将传入的 deptId 放到 In 子查询中实现数据权限筛选。
/// <summary>
/// 方法 CheckDataPermission 的绑定。
/// </summary>
private class CheckDataPermissionBinder : IMethodCallBinder
{
public Expression Bind(MethodCallBindContext context)
{
var sql = @"
select
t.dept_id
from
system_dept_permission t
where
role_id in (
select role_id
from system_manager_role t
where t.user_id = {0})
union (
select t.dept_id
from system_user t
where user_id = {0}
)";
var arguments = context.Visitor.Visit(context.Expression.Arguments);
var userId = (arguments[0] as ConstantExpression).Value.ToString();
var sqlExp = new SqlExpression(string.Format(sql, userId));
return new InExpression(arguments[1], new Expression[] { sqlExp });
}
}
在使用业务数据查询时,就可以使用 CheckDataPermission 方法来判断业务中的部门ID是否在用户的数据权限范围之内。测试用例仅供参考,无法正确执行。
[TestMethod]
public void TestInBind()
{
using (var db = new DbContext())
{
var userId = 8;
//判断 deptId 是否在用户的数据权限范围之内
var list = db.Depts.Where(s => Funcs.CheckDataPermission(userId, (int)s.DeptID));
}
}
为了方便理解,现将生成的 SQL
输出如下:
SELECT t0.DeptID, t0.DeptName, t0.DeptCode
FROM Depts AS t0
WHERE t0.DeptID IN (
select
t.dept_id
from
system_dept_permission t
where
role_id in (
select role_id
from system_manager_role t
where t.user_id = 8)
union (
select t.dept_id
from system_user t
where user_id = 8
)
)
对于无法使用特性的一些方法(如 .Net 类库或第三方类库),你可以使用 TranslateUtils
类的 AddMethodBinder 方法来绑定某一方法的表达式解析规则。如下所示:
[TestMethod]
public void TestOverrideBind()
{
TranslateUtils.AddMethodBinder<ConvertToInt32Binder>(m => m.DeclaringType == typeof(Convert) && m.Name == "ToInt32");
using (var context = new DbContext())
{
var list = context.OrderDetails.Where(s => Convert.ToInt32(s.Discount) == 1).ToList();
}
}
public class ConvertToInt32Binder : IMethodCallBinder
{
public Expression Bind(MethodCallBindContext context)
{
var exp = (ColumnExpression)context.Visitor.Visit(context.Expression.Arguments[0]);
return new SqlExpression($"(cast {exp.MapInfo.ColumnName} as integer)", typeof(int));
}
}
ConvertToInt32Binder
类用于匹配 Convert.ToInt32 方法,然后替换掉原有的 lambda
表达式解析方法。