//---------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// @owner [....]
// @backupOwner [....]
//---------------------------------------------------------------------
namespace System.Data.Objects.ELinq
{
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Data.Common.CommandTrees;
using System.Data.Common.CommandTrees.ExpressionBuilder;
using System.Data.Entity;
using System.Data.Metadata.Edm;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
///
/// Determines which leaves of a LINQ expression tree should be evaluated locally before
/// sending a query to the store. These sub-expressions may map to query parameters (e.g. local variables),
/// to constants (e.g. literals 'new DateTime(2008, 1, 1)') or query sub-expression
/// (e.g. 'context.Products'). Parameter expressions are replaced with QueryParameterExpression
/// nodes. All other elements are swapped in place with either expanded expressions (for sub-queries)
/// or constants. Where the expression includes mutable state that may influence the translation
/// to a query, a Func(Of Boolean) delegate is returned indicating when a recompilation is necessary.
///
internal sealed class Funcletizer
{
// Compiled query information
private readonly ParameterExpression _rootContextParameter;
private readonly ObjectContext _rootContext;
private readonly ConstantExpression _rootContextExpression;
private readonly ReadOnlyCollection _compiledQueryParameters;
private readonly Mode _mode;
private readonly HashSet _linqExpressionStack = new HashSet();
// Object parameters
private static readonly string s_parameterPrefix = "p__linq__";
private long _parameterNumber;
private Funcletizer(
Mode mode,
ObjectContext rootContext,
ParameterExpression rootContextParameter,
ReadOnlyCollection compiledQueryParameters)
{
_mode = mode;
_rootContext = rootContext;
_rootContextParameter = rootContextParameter;
_compiledQueryParameters = compiledQueryParameters;
if (null != _rootContextParameter && null != _rootContext)
{
_rootContextExpression = Expression.Constant(_rootContext);
}
}
internal static Funcletizer CreateCompiledQueryEvaluationFuncletizer(
ObjectContext rootContext,
ParameterExpression rootContextParameter,
ReadOnlyCollection compiledQueryParameters)
{
EntityUtil.CheckArgumentNull(rootContext, "rootContext");
EntityUtil.CheckArgumentNull(rootContextParameter, "rootContextParameter");
EntityUtil.CheckArgumentNull(compiledQueryParameters, "compiledQueryParameters");
return new Funcletizer(Mode.CompiledQueryEvaluation, rootContext, rootContextParameter, compiledQueryParameters);
}
internal static Funcletizer CreateCompiledQueryLockdownFuncletizer()
{
return new Funcletizer(Mode.CompiledQueryLockdown, null, null, null);
}
internal static Funcletizer CreateQueryFuncletizer(ObjectContext rootContext)
{
EntityUtil.CheckArgumentNull(rootContext, "rootContext");
return new Funcletizer(Mode.ConventionalQuery, rootContext, null, null);
}
internal ObjectContext RootContext
{
get { return _rootContext; }
}
internal ParameterExpression RootContextParameter
{
get { return _rootContextParameter; }
}
internal ConstantExpression RootContextExpression
{
get { return _rootContextExpression; }
}
internal bool IsCompiledQuery
{
get { return _mode == Mode.CompiledQueryEvaluation || _mode == Mode.CompiledQueryLockdown; }
}
///
/// Performs funcletization on the given expression. Also returns a delegates that can be used
/// to determine if the entire tree needs to be recompiled.
///
internal Expression Funcletize(Expression expression, out Func recompileRequired)
{
EntityUtil.CheckArgumentNull(expression, "expression");
// Find all candidates for funcletization. Some sub-expressions are reduced to constants,
// others are reduced to variables. The rules vary based on the _mode.
Func isClientConstant;
Func isClientVariable;
expression = ReplaceRootContextParameter(expression);
if (_mode == Mode.CompiledQueryEvaluation)
{
// We lock down closure expressions for compiled queries, so everything is either
// a constant or a query parameter produced from the explicit parameters to the
// compiled query delegate.
isClientConstant = Nominate(expression, this.IsClosureExpression);
isClientVariable = Nominate(expression, this.IsCompiledQueryParameterVariable);
}
else if (_mode == Mode.CompiledQueryLockdown)
{
// When locking down a compiled query, we can evaluate all closure expressions.
isClientConstant = Nominate(expression, this.IsClosureExpression);
isClientVariable = (exp) => false;
}
else
{
Debug.Assert(_mode == Mode.ConventionalQuery, "No other options...");
// There are no variable parameters outside of compiled queries, so everything is
// either a constant or a closure expression.
isClientConstant = Nominate(expression, this.IsImmutable);
isClientVariable = Nominate(expression, this.IsClosureExpression);
}
// Now rewrite given nomination functions
FuncletizingVisitor visitor = new FuncletizingVisitor(this, isClientConstant, isClientVariable);
Expression result = visitor.Visit(expression);
recompileRequired = visitor.GetRecompileRequiredFunction();
return result;
}
///
/// Replaces context parameter (e.g. 'ctx' in CompiledQuery.Compile(ctx => ctx.Products)) with constant
/// containing the object context.
///
private Expression ReplaceRootContextParameter(Expression expression)
{
if (null != _rootContextExpression)
{
return EntityExpressionVisitor.Visit(
expression, (exp, baseVisit) =>
exp == _rootContextParameter ? _rootContextExpression : baseVisit(exp));
}
else
{
return expression;
}
}
///
/// Returns a function indicating whether the given expression and all of its children satisfy the
/// 'localCriterion'.
///
private static Func Nominate(Expression expression, Func localCriterion)
{
EntityUtil.CheckArgumentNull(localCriterion, "localCriterion");
HashSet candidates = new HashSet();
bool cannotBeNominated = false;
Func, Expression> visit = (exp, baseVisit) =>
{
if (exp != null)
{
bool saveCannotBeNominated = cannotBeNominated;
cannotBeNominated = false;
baseVisit(exp);
if (!cannotBeNominated)
{
// everyone below me can be nominated, so
// see if this one can be also
if (localCriterion(exp))
{
candidates.Add(exp);
}
else
{
cannotBeNominated = true;
}
}
cannotBeNominated |= saveCannotBeNominated;
}
return exp;
};
EntityExpressionVisitor.Visit(expression, visit);
return candidates.Contains;
}
private enum Mode
{
CompiledQueryLockdown,
CompiledQueryEvaluation,
ConventionalQuery,
}
///
/// Determines whether the node may be evaluated locally and whether
/// it is a constant. Assumes that all children are also client expressions.
///
private bool IsImmutable(Expression expression)
{
if (null == expression) { return false; }
switch (expression.NodeType)
{
case ExpressionType.New:
{
// support construction of primitive types
PrimitiveType primitiveType;
if (!ClrProviderManifest.Instance.TryGetPrimitiveType(TypeSystem.GetNonNullableType(expression.Type),
out primitiveType))
{
return false;
}
return true;
}
case ExpressionType.Constant:
return true;
case ExpressionType.NewArrayInit:
// allow initialization of byte[] 'literals'
return (typeof(byte[]) == expression.Type);
case ExpressionType.Convert:
return true;
default:
return false;
}
}
///
/// Determines whether the node may be evaluated locally and whether
/// it is a variable. Assumes that all children are also variable client expressions.
///
private bool IsClosureExpression(Expression expression)
{
if (null == expression) { return false; }
if (IsImmutable(expression)) { return true; }
if (ExpressionType.MemberAccess == expression.NodeType)
{
MemberExpression member = (MemberExpression)expression;
if (member.Member.MemberType == MemberTypes.Property)
{
return ExpressionConverter.CanFuncletizePropertyInfo((PropertyInfo)member.Member);
}
return true;
}
return false;
}
///
/// Determines whether the node may be evaluated as a compiled query parameter.
/// Assumes that all children are also eligible compiled query parameters.
///
private bool IsCompiledQueryParameterVariable(Expression expression)
{
if (null == expression) { return false; }
if (IsClosureExpression(expression)) { return true; }
if (ExpressionType.Parameter == expression.NodeType)
{
ParameterExpression parameter = (ParameterExpression)expression;
return _compiledQueryParameters.Contains(parameter);
}
return false;
}
///
/// Determine whether the given CLR type is legal for an ObjectParameter or constant
/// DbExpression.
///
private bool TryGetTypeUsageForTerminal(Type type, out TypeUsage typeUsage)
{
EntityUtil.CheckArgumentNull(type, "type");
if (_rootContext.Perspective.TryGetTypeByName(TypeSystem.GetNonNullableType(type).FullName,
false, // bIgnoreCase
out typeUsage) &&
(TypeSemantics.IsScalarType(typeUsage)))
{
return true;
}
typeUsage = null;
return false;
}
///
/// Creates the next available parameter name.
///
internal string GenerateParameterName()
{
// To avoid collisions with user parameters (the full set is not
// known at this time) we plug together an 'unlikely' prefix and
// a number.
return String.Format(CultureInfo.InvariantCulture, "{0}{1}",
s_parameterPrefix,
_parameterNumber++);
}
///
/// Walks the expression tree and replaces client-evaluable expressions with constants
/// or QueryParameterExpressions.
///
private sealed class FuncletizingVisitor : EntityExpressionVisitor
{
private readonly Funcletizer _funcletizer;
private readonly Func _isClientConstant;
private readonly Func _isClientVariable;
private readonly List> _recompileRequiredDelegates = new List>();
internal FuncletizingVisitor(
Funcletizer funcletizer,
Func isClientConstant,
Func isClientVariable)
{
EntityUtil.CheckArgumentNull(funcletizer, "funcletizer");
EntityUtil.CheckArgumentNull(isClientConstant, "isClientConstant");
EntityUtil.CheckArgumentNull(isClientVariable, "isClientVariable");
_funcletizer = funcletizer;
_isClientConstant = isClientConstant;
_isClientVariable = isClientVariable;
}
///
/// Returns a delegate indicating (when called) whether a change has been identified
/// requiring a complete recompile of the query.
///
internal Func GetRecompileRequiredFunction()
{
// assign list to local variable to avoid including the entire Funcletizer
// class in the closure environment
ReadOnlyCollection> recompileRequiredDelegates = _recompileRequiredDelegates.AsReadOnly();
return () => recompileRequiredDelegates.Any(d => d());
}
internal override Expression Visit(Expression exp)
{
if (exp != null)
{
if (!_funcletizer._linqExpressionStack.Add(exp))
{
// This expression is already in the stack.
throw EntityUtil.InvalidOperation(Strings.ELinq_CycleDetected);
}
try
{
if (_isClientConstant(exp))
{
return InlineValue(exp, false);
}
else if (_isClientVariable(exp))
{
TypeUsage queryParameterType;
if (_funcletizer.TryGetTypeUsageForTerminal(exp.Type, out queryParameterType))
{
DbParameterReferenceExpression parameterReference = queryParameterType.Parameter(_funcletizer.GenerateParameterName());
return new QueryParameterExpression(parameterReference, exp, _funcletizer._compiledQueryParameters);
}
else if (_funcletizer.IsCompiledQuery)
{
throw InvalidCompiledQueryParameterException(exp);
}
else
{
return InlineValue(exp, true);
}
}
return base.Visit(exp);
}
finally
{
_funcletizer._linqExpressionStack.Remove(exp);
}
}
return base.Visit(exp);
}
private static NotSupportedException InvalidCompiledQueryParameterException(Expression expression)
{
ParameterExpression parameterExp;
if (expression.NodeType == ExpressionType.Parameter)
{
parameterExp = (ParameterExpression)expression;
}
else
{
// If this is a simple query parameter (involving a single delegate parameter) report the
// type of that parameter. Otherwise, report the type of the part of the parameter.
HashSet parameters = new HashSet();
EntityExpressionVisitor.Visit(expression, (exp, baseVisit) =>
{
if (null != exp && exp.NodeType == ExpressionType.Parameter)
{
parameters.Add((ParameterExpression)exp);
}
return baseVisit(exp);
});
if (parameters.Count != 1)
{
return EntityUtil.NotSupported(Strings.CompiledELinq_UnsupportedParameterTypes(expression.Type.FullName));
}
parameterExp = parameters.Single();
}
if (parameterExp.Type.Equals(expression.Type))
{
// If the expression type is the same as the parameter type, indicate that the parameter type is not valid.
return EntityUtil.NotSupported(Strings.CompiledELinq_UnsupportedNamedParameterType(parameterExp.Name, parameterExp.Type.FullName));
}
else
{
// Otherwise, indicate that using the specified parameter to produce a value of the expression's type is not supported in compiled query
return EntityUtil.NotSupported(Strings.CompiledELinq_UnsupportedNamedParameterUseAsType(parameterExp.Name, expression.Type.FullName));
}
}
///
/// Compiles a delegate returning the value of the given expression.
///
private Func