展平表达树

时间:2018-02-16 17:22:51

标签: c# linq lambda

我正在创建一个Assembler接口,它有助于在我的应用程序中的不同层之间转换对象。

E.g。

public interface IAssembler<TObjectSource, TObjectTarget> 
{
  IEnumerable<Expression<Func<TObjectSource, object>>> GetIncludes();

  TObjectTarget Project(TObjectSource source);
}

GetIncludes方法将通知较低级别层创建目标对象需要源对象的哪些部分,因此需要由该层加载哪些部分。

Book汇编程序的实现可以是:

public class BookAssembler : IAssembler<Book, BookDTO> 
{
  public BookAssembler(IAssembler<Publisher, PublisherDTO> publisherAssembler)
  {
    PublisherAssembler = publisherAssembler;
  }

  private IAssembler<Publisher, PublisherDTO> PublisherAssembler { get; }

  public IEnumerable<Expression<Func<Book, object>>> GetIncludes() {
    return Expression<Func<Book, object>>[] 
    {
      include => include.CreatedBy,
      include => include.DeletedBy,
      include => include.Publishers.Select(
        includePublisher => PublisherAssembler.GetIncludes()
      ),
      ...
    }
  }

  public BookDTO Project(Book book) {
    //Convert the book into the book DTO.
  }
}

BookAssembler未知PublisherAssembler返回的包含列表,例如它可以是0到N.

我需要能够压缩返回的可枚举对象,例如如果PublisherAssembler.GetIncludes()返回一个包含2个项目的枚举,我需要BookAssembler.GetIncludes()的输出为:

return Expression<Func<Book, object>>[] 
{
  include => include.CreatedBy,
  include => include.DeletedBy,
  include => include.Publishers.Select(includePublisher => includePublisher.Item1),
  include => include.Publishers.Select(includePublisher => includePublisher.Item2),
  ...
}

关于如何做到这一点的任何想法?

1 个答案:

答案 0 :(得分:0)

以下内容将按照我的要求进行,但仍需要扩展以处理所有表达式类型并执行更多错误检查。

它基本上只是将表达式树格式化,因此它不包含任何带复合对象的Select调用。

public class ExpressionFlattener 
{
    public ExpressionFlattener(MethodInfo selectMethod)
    {
        SelectMethod = selectMethod;
    }

    /// <summary>
    /// Flattens the specified expression.
    /// </summary>
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
    /// <param name="expression">The expression.</param>
    /// <returns>IEnumerable&lt;Expression&lt;Func&lt;TEntity, System.Object&gt;&gt;&gt;.</returns>
    public IEnumerable<Expression<Func<TEntity, object>>> Flatten<TEntity>(Expression<Func<TEntity, object>> expression) => Flatten(expression, new List<Expression>()).Cast<Expression<Func<TEntity, object>>>();

    /// <summary>
    /// Gets the select method.
    /// </summary>
    /// <value>The select method.</value>
    private MethodInfo SelectMethod { get; }

    /// <summary>
    /// Flattens the specified expression.
    /// </summary>
    /// <param name="expression">The expression.</param>
    /// <param name="expressions">The expressions.</param>
    /// <returns>List&lt;Expression&gt;.</returns>
    /// <exception cref="ArgumentException">Invalid expression specified, the specified expression is of an unhandled node type.</exception>
    private List<Expression> Flatten(Expression expression, List<Expression> expressions)
    {
        switch (expression.NodeType)
        {
            case ExpressionType.Call: return FlattenMethodCall(expression as MethodCallExpression, expressions);
            case ExpressionType.Lambda: return FlattenLambda(expression as LambdaExpression, expressions);
            case ExpressionType.MemberAccess: expressions.Add(expression); return expressions;
            case ExpressionType.New: return FlattenNew(expression as NewExpression, expressions);
            default: throw new ArgumentException("Invalid expression specified, the specified expression is of an unhandled node type.");
        }
    }

    /// <summary>
    /// Flattens the lambda expression.
    /// </summary>
    /// <param name="expression">The expression.</param>
    /// <param name="expressions">The expressions.</param>
    /// <returns>List&lt;Expression&lt;Func&lt;TEntity, System.Object&gt;&gt;&gt;.</returns>
    /// <exception cref="ArgumentException">Invalid expression specified, the specified expression has a body with an unhandled node type.</exception>
    private List<Expression> FlattenLambda(LambdaExpression expression, List<Expression> expressions)
    {
        //Flatten the body expression.
        List<Expression> innerExpressions = Flatten(expression.Body, new List<Expression>());

        //We should never be creating a lambda expression which returns more than one property as we should
        //have flattened everything out with the call to the above. So it should be safe? to assume we're
        //inputting the same parameter type but just getting out an object.
        Type delegateType = typeof(Func<,>).MakeGenericType(expression.Parameters[0].Type, typeof(object));

        //Rebuild the lambda expression for each expression returned.
        foreach (Expression innerExpression in innerExpressions)
            expressions.Add(Expression.Lambda(delegateType, innerExpression, expression.Parameters[0]));

        return expressions;
    }

    /// <summary>
    /// Flattens the method call expression.
    /// </summary>
    /// <param name="expression">The expression.</param>
    /// <param name="expressions">The expressions.</param>
    /// <param name="lambdaExpression">The lambda expression.</param>
    /// <returns>List&lt;Expression&lt;Func&lt;TEntity, System.Object&gt;&gt;&gt;.</returns>
    /// <exception cref="ArgumentException">Invalid method call expression specified, the method call specified expression is to an unhandled method.</exception>
    private List<Expression> FlattenMethodCall(MethodCallExpression expression, List<Expression> expressions)
    {
        switch (expression.Method.Name)
        {
            case "Select":
                {
                    //The second argument will be the right side of the select statement e.g. property.Select(argument0 => argument1);
                    List<Expression> innerExpressions = Flatten(expression.Arguments[1], new List<Expression>());

                    //Create the select method.
                    //The first argument should always be the object we're passing in, so it will be the same.
                    //The second argument has been normailised into an object, e.g. from a single property or an anonyamous object containing multiple properties.
                    MethodInfo method = SelectMethod.MakeGenericMethod(expression.Method.GetGenericArguments().First(), typeof(object));

                    //Rebuild the select expression for each expression returned.
                    foreach (Expression innerExpression in innerExpressions)
                        expressions.Add(Expression.Call(method, expression.Arguments[0], innerExpression));

                    return expressions;
                }

            default: throw new ArgumentException("Invalid method call expression specified, the method call expression is to an unknown method.");
        }
    }

    /// <summary>
    /// Flattens the new expression.
    /// </summary>
    /// <param name="expression">The expression.</param>
    /// <param name="expressions">The expressions.</param>
    /// <returns>List&lt;Expression&gt;.</returns>
    private List<Expression> FlattenNew(NewExpression expression, List<Expression> expressions)
    {
        //TODO: Make sure we're an anonyamous type.
        foreach (Expression argument in expression.Arguments)
            Flatten(argument, expressions);
        return expressions;
    }

}

传递给构造函数的select方法是:

Enumerable.Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)

我已经编写了测试,按预期检查了以下工作:

单一财产:

输入:

book => book.CreatedBy

输出:

book => book.CreatedBy

集合的单一属性:

输入:

book => book.Publishers.Select(publisher => publisher.CreatedBy)

输出:

book => book.Publishers.Select(publisher => publisher.CreatedBy)

集合的多个属性:

输入:

book => book.Publishers.Select(publisher => new
  {
    publisher.CreatedBy,
    publisher.DeletedBy
  });

输出:

book => book.Publishers.Select(publisher => publisher.CreatedBy);
book => book.Publishers.Select(publisher => publisher.DeletedBy);

我需要写更多测试,但我很高兴我现在正走在正确的轨道上。