自定义解析绑定


  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 表达式解析方法。