现在,我一直认为,如果有人要你用绳子吊死自己,你应该只问两个问题,“你确定吗?”和“你需要多少绳子?”。所以你向我要一些绳子,你告诉我们你确定你想要这根绳子,所以我是谁不给你这根绳子?
这就是你问的...我们称之为 v0.5 。它甚至还有一个代号:Armed and Dangerous。我什至给 v0.1 起了一个代号:Running with Scissors。它在http://pastebin.com/6qLs8TPt 处负责。 (v0.2 是 Lost in Space,因为我把它忘在另一台电脑上了 :-))。我在 v0.3 Playing with Fire http://pastebin.com/pRbKt1Z2 中添加了一个简单的记录器和 Stack Trace 增强器,并在 v0.4 Crossing withoutlooking http://pastebin.com/yEhc9vjg有点 EF 兼容性。
我用简单的查询和连接检查了它。它似乎工作。可以补充很多。例如,一个“智能”投影仪分析可能从查询中删除的Select 并尝试重建它......但这可能是一个好的开始。有一个小问题:正如我在评论中所写,取决于Select 的存在/不存在,LINQ-to-SQL 将返回的对象加载到其对象跟踪器中。如果在“原始”查询中有一个Select(所以没有对象跟踪),并且我将其删除以在本地执行它,那么将加载完整的对象,并且将“打开”对象跟踪。显然你可以context.ObjectTrackingEnabled = false。
现在它应该与简单的 EF 查询兼容。 不兼容AsNoTracking()/Include()。
NotSupportedException 在 Exception 属性中进行跟踪。请注意,仅在最“外部”的 IQueryable 中修改此属性。所以
// Optional :-) You can ignore the logger and live happy...
// Not optional: you can live happy very far from me!
SafeQueryable.Logger = (iqueriable, expression, e) => Console.WriteLine(e);
var q1 = context.Items.AsSafe();
var q2 = q1.Where(x => x.ID.GetHashCode() > 0);
var q3 = q2.Select(x => x.ID);
var ex = ((ISafeQueryable)q3).Exception;
如何使用它?有一个单一的扩展方法,AsSafe()。您在IQueryable<T> 上使用它,然后执行查询...并快乐地生活...玩火! :-)
使用示例:
// Queries that are NotSupportedException with LINQ-to-SQL
var q1 = context.Items.AsSafe().Where(x => x.ID.GetHashCode() > 0).Take(2).Select(x => x.ID);
var q2 = context.Items.Where(x => x.ID.GetHashCode() > 0).AsSafe().Take(2).Select(x => x.ID);
var q3 = context.Items.Where(x => x.ID.GetHashCode() > 0).Take(2).AsSafe().Select(x => x.ID);
var q4 = context.Items.Where(x => x.ID.GetHashCode() > 0).Take(2).Select(x => x.ID).AsSafe();
//// Queries that are OK with LINQ-to-SQL
//var q1 = context.Items.AsSafe().Where(x => x.ID > 0).Take(2).Select(x => x.ID);
//var q2 = context.Items.Where(x => x.ID > 0).AsSafe().Take(2).Select(x => x.ID);
//var q3 = context.Items.Where(x => x.ID > 0).Take(2).AsSafe().Select(x => x.ID);
//var q4 = context.Items.Where(x => x.ID > 0).Take(2).Select(x => x.ID).AsSafe();
var r1 = q1.ToList();
var r2 = q2.First();
var r3 = q3.Max();
// The Aggregate isn't normally supported by LINQ-to-SQL
var r4 = q4.Aggregate((x, y) => x + y);
var ex1 = ((ISafeQueryable)q1).Exception;
var ex2 = ((ISafeQueryable)q2).Exception;
var ex3 = ((ISafeQueryable)q3).Exception;
var ex4 = ((ISafeQueryable)q4).Exception;
如您所见,您可以在任何步骤使用AsSafe(),它会起作用。发生这种情况是因为查询被AsSafe() 包装在SafeQueryable<T> 中,既是IQueryProvider 又是IQueryable<T>(类似于LINQ-to-SQL 的类)。这样,您调用的每个其他 IQueryable 方法都会产生其他 SafeQueryable<T> 对象(因此它会自我复制:-))
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
/// <summary>
/// v0.5 Codename: Armed and Dangerous
///
/// (previous version v0.1 Codename: Running with Scissors: http://pastebin.com/6qLs8TPt)
/// (previous version v0.2 Codename: Lost in Space: lost in space :-) )
/// (previous version v0.3 Codename: Playing with Fire: http://pastebin.com/pRbKt1Z2)
/// (previous version v0.4 Codename: Crossing without Looking: http://pastebin.com/yEhc9vjg)
///
/// Support class with an extension method to make "Safe" an IQueryable
/// or IQueryable<T>. Safe as "I work for another company, thousand
/// of miles from you. I do hope I won't ever buy/need something from
/// your company".
/// The Extension methods wraps a IQueryable in a wrapper that then can
/// be used to execute a query. If the original Provider doesn't suppport
/// some methods, the query will be partially executed by the Provider
/// and partially executed locally.
///
/// Minimal support for EF.
///
/// Note that this **won't** play nice with the Object Tracking!
///
/// Not suitable for programmers under 5 years (of experience)!
/// Dangerous if inhaled or executed.
/// </summary>
public static class SafeQueryable
{
/// <summary>
/// Optional logger to log the queries that are "corrected. Note that
/// there is no "strong guarantee" that the IQueriable (that is also
/// an IQueryProvider) is executing its (as in IQueriable.Expression)
/// Expression, so an explicit Expression parameter is passed. This
/// because the IQueryProvider.Execute method receives an explicit
/// expression parameter. Clearly there is a "weak guarantee" that
/// unless you do "strange things" this won't happen :-)
/// </summary>
public static Action<IQueryable, Expression, NotSupportedException> Logger { get; set; }
/// <summary>
/// Return a "Safe" IQueryable<T>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <returns></returns>
public static IQueryable<T> AsSafe<T>(this IQueryable<T> source)
{
if (source is SafeQueryable<T>)
{
return source;
}
return new SafeQueryable<T>(source);
}
}
/// <summary>
/// Simple interface useful to collect the Exception, or to recognize
/// a SafeQueryable<T>.
/// </summary>
public interface ISafeQueryable
{
NotSupportedException Exception { get; }
}
/// <summary>
/// "Safe" wrapper around a IQueryable<T;>
/// </summary>
/// <typeparam name="T"></typeparam>
public class SafeQueryable<T> : IOrderedQueryable<T>, IQueryProvider, ISafeQueryable
{
protected static readonly FieldInfo StackTraceStringField = typeof(Exception).GetField("_stackTraceString", BindingFlags.Instance | BindingFlags.NonPublic);
// The query. Note that it can be "transformed" to a "safe" version
// of itself. When it happens, IsSafe becomes true
public IQueryable<T> Query { get; protected set; }
// IsSafe means that the query has been "corrected" if necessary and
// won't throw a NotSupportedException
protected bool IsSafe { get; set; }
// Logging of the "main" NotSupportedException.
public NotSupportedException Exception { get; protected set; }
public SafeQueryable(IQueryable<T> query)
{
Query = query;
}
/* IQueryable<T> */
public IEnumerator<T> GetEnumerator()
{
if (IsSafe)
{
return Query.GetEnumerator();
}
return new SafeEnumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public Type ElementType
{
get { return Query.ElementType; }
}
public Expression Expression
{
get { return Query.Expression; }
}
public IQueryProvider Provider
{
get { return this; }
}
/* IQueryProvider */
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
return CreateQueryImpl<TElement>(expression);
}
public IQueryable CreateQuery(Expression expression)
{
Type iqueryableArgument = GetIQueryableTypeArgument(expression.Type);
MethodInfo createQueryImplMethod = typeof(SafeQueryable<T>)
.GetMethod("CreateQueryImpl", BindingFlags.Instance | BindingFlags.NonPublic)
.MakeGenericMethod(iqueryableArgument);
return (IQueryable)createQueryImplMethod.Invoke(this, new[] { expression });
}
public TResult Execute<TResult>(Expression expression)
{
return ExecuteImpl<TResult>(expression);
}
public object Execute(Expression expression)
{
Type iqueryableArgument = GetIQueryableTypeArgument(expression.Type);
MethodInfo executeImplMethod = typeof(SafeQueryable<T>)
.GetMethod("ExecuteImpl", BindingFlags.Instance | BindingFlags.NonPublic)
.MakeGenericMethod(iqueryableArgument);
return executeImplMethod.Invoke(this, new[] { expression });
}
/* Implementation methods */
// Gets the T of IQueryablelt;T>
protected static Type GetIQueryableTypeArgument(Type type)
{
IEnumerable<Type> interfaces = type.IsInterface ?
new[] { type }.Concat(type.GetInterfaces()) :
type.GetInterfaces();
Type argument = (from x in interfaces
where x.IsGenericType
let gt = x.GetGenericTypeDefinition()
where gt == typeof(IQueryable<>)
select x.GetGenericArguments()[0]).FirstOrDefault();
return argument;
}
protected IQueryable<TElement> CreateQueryImpl<TElement>(Expression expression)
{
return new SafeQueryable<TElement>(Query.Provider.CreateQuery<TElement>(expression));
}
protected TResult ExecuteImpl<TResult>(Expression expression)
{
if (IsSafe && Query.Expression == expression)
{
TResult result = Query.Provider.Execute<TResult>(expression);
return result;
}
try
{
// Note that thanks to how everything knits together, if you
// call query1.First(); query1.First(); the second call will
// get to use the query cached by the first one (technically
// the cached query will be only the "query1" part)
// We try executing it directly
TResult result = Query.Provider.Execute<TResult>(expression);
// Success!
if (!IsSafe && CanCache(expression, true))
{
IsSafe = true;
}
return result;
}
catch (NotSupportedException e1)
{
// Clearly there was a NotSupportedException :-)
Tuple<IEnumerator<T>, bool, TResult> result = HandleEnumerationFailure<TResult>(e1, expression, true);
if (result == null)
{
throw;
}
// Success!
return result.Item3;
}
}
// Is used both indirectly by GetEnumerator() and by Execute<>.
// The returned Tuple<,,> has the first two elements that are valid
// when used by the GetEnumerator() and the last that is valid
// when used by Execute<>.
protected Tuple<IEnumerator<T>, bool, TResult> HandleEnumerationFailure<TResult>(NotSupportedException e1, Expression expression, bool singleResult)
{
// We "augment" the exception with the full stack trace
AugmentStackTrace(e1, 3);
if (SafeQueryable.Logger != null)
{
SafeQueryable.Logger(this, expression, e1);
}
// We save this first exception
Exception = e1;
{
var query = Query;
MethodInfo executeSplittedMethod = typeof(SafeQueryable<T>).GetMethod("ExecuteSplitted", BindingFlags.Instance | BindingFlags.NonPublic);
MethodCallExpression call;
Expression innerExpression = expression;
Type iqueryableArgument;
// We want to check that there is a MethodCallExpression with
// at least one argument, and that argument is an Expression
// of type IQueryable<iqueryableArgument>, and we save the
// iqueryableArgument
while ((call = innerExpression as MethodCallExpression) != null &&
call.Arguments.Count > 0 &&
(innerExpression = call.Arguments[0] as Expression) != null &&
(iqueryableArgument = GetIQueryableTypeArgument(innerExpression.Type)) != null)
{
try
{
Tuple<IEnumerator<T>, bool, TResult> result2 = (Tuple<IEnumerator<T>, bool, TResult>)executeSplittedMethod.MakeGenericMethod(iqueryableArgument, typeof(TResult)).Invoke(this, new object[] { expression, call, innerExpression, singleResult });
return result2;
}
catch (TargetInvocationException e2)
{
if (!(e2.InnerException is NotSupportedException))
{
throw;
}
}
}
return null;
}
}
// Is used both indirectly by GetEnumerator() and by Execute<>.
// The returned Tuple<,,> has the first two elements that are valid
// when used by the GetEnumerator() and the last that is valid
// when used by Execute<>.
protected Tuple<IEnumerator<T>, bool, TResult> ExecuteSplitted<TInner, TResult>(Expression expression, MethodCallExpression call, Expression innerExpression, bool singleResult)
{
// The NotSupportedException should happen here
IQueryable<TInner> innerQueryable = Query.Provider.CreateQuery<TInner>(innerExpression);
// We try executing it directly
IEnumerator<TInner> innerEnumerator = innerQueryable.GetEnumerator();
bool moveNextSuccess = innerEnumerator.MoveNext();
IEnumerator<T> enumerator;
TResult singleResultValue;
// Success!
{
// Now we wrap the partially used enumerator in an
// EnumerableFromStartedEnumerator
IEnumerable<TInner> innerEnumerable = new EnumerableFromStartedEnumerator<TInner>(innerEnumerator, moveNextSuccess, innerQueryable);
// Then we apply an AsQueryable, that does some magic
// to make the query appear to be a Queryable
IQueryable<TInner> innerEnumerableAsQueryable = Queryable.AsQueryable(innerEnumerable);
// We rebuild a new expression by changing the "old"
// inner parameter of the MethodCallExpression with the
// queryable we just built
var arguments = call.Arguments.ToArray();
arguments[0] = Expression.Constant(innerEnumerableAsQueryable);
MethodCallExpression call2 = Expression.Call(call.Object, call.Method, arguments);
Expression expressionWithFake = new SimpleExpressionReplacer(call, call2).Visit(expression);
// We "execute" locally the whole query through a second
// "outer" instance of the EnumerableQuery (this class is
// the class that "implements" the "fake-magic" of
// AsQueryable)
IQueryable<T> queryable = new EnumerableQuery<T>(expressionWithFake);
if (singleResult)
{
enumerator = null;
moveNextSuccess = false;
singleResultValue = queryable.Provider.Execute<TResult>(queryable.Expression);
}
else
{
enumerator = queryable.GetEnumerator();
moveNextSuccess = enumerator.MoveNext();
singleResultValue = default(TResult);
}
}
// We could enter here with a new query from Execute<>(),
// with IsSafe == true . It would be useless to try to cache
// that query.
if (!IsSafe && CanCache(expression, singleResult))
{
Stopwatch sw = Stopwatch.StartNew();
// We redo the same things to create a second copy of
// the query that is "complete", not partially
// enumerated. This second copy will be cached in the
// SafeQueryable<T>.
// Note that forcing the Queryable.AsQueryable to not
// "recast" the query to the original IQueryable<T> is
// quite complex :-) We have to
// .AsEnumerable().Select(x => x) .
IEnumerable<TInner> innerEnumerable = innerQueryable.AsEnumerable().Select(x => x);
IQueryable<TInner> innerEnumerableAsQueryable = Queryable.AsQueryable(innerEnumerable);
// Note that we cache the SafeQueryable<>.Expression!
var arguments = call.Arguments.ToArray();
arguments[0] = Expression.Constant(innerEnumerableAsQueryable);
MethodCallExpression call2 = Expression.Call(call.Object, call.Method, arguments);
Expression expressionWithFake = new SimpleExpressionReplacer(call, call2).Visit(Expression);
IQueryable<T> queryable = new EnumerableQuery<T>(expressionWithFake);
// Now the SafeQueryable<T> has a query that *just works*
Query = queryable;
IsSafe = true;
sw.Stop();
Console.WriteLine(sw.ElapsedTicks);
}
return Tuple.Create(enumerator, moveNextSuccess, singleResultValue);
}
protected bool CanCache(Expression expression, bool singleResult)
{
// GetEnumerator() doesn't permit changing the query
if (!singleResult)
{
return true;
}
// The expression is equal to the one in Query.Expression
// (should be very rare!)
if (Query.Expression == expression)
{
return true;
}
MethodCallExpression call;
Expression innerExpression = expression;
Type iqueryableArgument;
// We walk back the expression to see if a smaller part of it is
// the "original" Query.Expression . This happens for example
// when one of the operators that returns a single value
// (.First(), .FirstOrDefault(), .Single(), .SingleOrDefault(),
// .Any(), .All()., .Min(), .Max(), ...) are used.
while ((call = innerExpression as MethodCallExpression) != null &&
call.Arguments.Count > 0 &&
(innerExpression = call.Arguments[0] as Expression) != null &&
(iqueryableArgument = GetIQueryableTypeArgument(innerExpression.Type)) != null)
{
if (Query.Expression == innerExpression)
{
return true;
}
}
return false;
}
// The StackTrace of an Exception "stops" at the catch. This method
// "augments" it to include the full stack trace.
protected static void AugmentStackTrace(Exception e, int skipFrames = 2)
{
// Playing with a private field here. Don't do it at home :-)
// If not present, do nothing.
if (StackTraceStringField == null)
{
return;
}
string stack1 = e.StackTrace;
string stack2 = new StackTrace(skipFrames, true).ToString();
string stack3 = stack1 + stack2;
StackTraceStringField.SetValue(e, stack3);
}
/* Utility classes */
// An IEnumerator<T> that applies the AsSafe() paradigm, knowing that
// normally the exception happens only on the first MoveFirst().
protected class SafeEnumerator : IEnumerator<T>
{
protected readonly SafeQueryable<T> SafeQueryable_;
protected IEnumerator<T> Enumerator { get; set; }
public SafeEnumerator(SafeQueryable<T> safeQueryable)
{
SafeQueryable_ = safeQueryable;
}
public T Current
{
get
{
return Enumerator != null ? Enumerator.Current : default(T);
}
}
public void Dispose()
{
if (Enumerator != null)
{
Enumerator.Dispose();
}
}
object IEnumerator.Current
{
get { return Current; }
}
public bool MoveNext()
{
// We handle exceptions only on first MoveNext()
if (Enumerator != null)
{
return Enumerator.MoveNext();
}
try
{
// We try executing it directly
Enumerator = SafeQueryable_.Query.GetEnumerator();
bool result = Enumerator.MoveNext();
// Success!
SafeQueryable_.IsSafe = true;
return result;
}
catch (NotSupportedException e1)
{
// Clearly there was a NotSupportedException :-)
Tuple<IEnumerator<T>, bool, T> result = SafeQueryable_.HandleEnumerationFailure<T>(e1, SafeQueryable_.Query.Expression, false);
if (result == null)
{
throw;
}
Enumerator = result.Item1;
return result.Item2;
}
}
public void Reset()
{
if (Enumerator != null)
{
Enumerator.Reset();
}
}
}
}
// A simple expression visitor to replace some nodes of an expression
// with some other nodes
public class SimpleExpressionReplacer : ExpressionVisitor
{
public readonly Dictionary<Expression, Expression> Replaces;
public SimpleExpressionReplacer(Dictionary<Expression, Expression> replaces)
{
Replaces = replaces;
}
public SimpleExpressionReplacer(IEnumerable<Expression> from, IEnumerable<Expression> to)
{
Replaces = new Dictionary<Expression, Expression>();
using (var enu1 = from.GetEnumerator())
using (var enu2 = to.GetEnumerator())
{
while (true)
{
bool res1 = enu1.MoveNext();
bool res2 = enu2.MoveNext();
if (!res1 || !res2)
{
if (!res1 && !res2)
{
break;
}
if (!res1)
{
throw new ArgumentException("from shorter");
}
throw new ArgumentException("to shorter");
}
Replaces.Add(enu1.Current, enu2.Current);
}
}
}
public SimpleExpressionReplacer(Expression from, Expression to)
{
Replaces = new Dictionary<Expression, Expression> { { from, to } };
}
public override Expression Visit(Expression node)
{
Expression to;
if (node != null && Replaces.TryGetValue(node, out to))
{
return base.Visit(to);
}
return base.Visit(node);
}
}
// Simple IEnumerable<T> that "uses" an IEnumerator<T> that has
// already received a MoveNext(). "eats" the first MoveNext()
// received, then continues normally. For shortness, both IEnumerable<T>
// and IEnumerator<T> are implemented by the same class. Note that if a
// second call to GetEnumerator() is done, the "real" IEnumerator<T> will
// be returned, not this proxy implementation.
public class EnumerableFromStartedEnumerator<T> : IEnumerable<T>, IEnumerator<T>
{
public readonly IEnumerator<T> Enumerator;
public readonly IEnumerable<T> Enumerable;
// Received by creator. Return value of MoveNext() done by caller
protected bool FirstMoveNextSuccessful { get; set; }
// The Enumerator can be "used" only once, then a new enumerator
// can be requested by Enumerable.GetEnumerator()
// (default = false)
protected bool Used { get; set; }
// The first MoveNext() has been already done (default = false)
protected bool DoneMoveNext { get; set; }
public EnumerableFromStartedEnumerator(IEnumerator<T> enumerator, bool firstMoveNextSuccessful, IEnumerable<T> enumerable)
{
Enumerator = enumerator;
FirstMoveNextSuccessful = firstMoveNextSuccessful;
Enumerable = enumerable;
}
public IEnumerator<T> GetEnumerator()
{
if (Used)
{
return Enumerable.GetEnumerator();
}
Used = true;
return this;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public T Current
{
get
{
// There are various school of though on what should
// happens if called before the first MoveNext() or
// after a MoveNext() returns false. We follow the
// "return default(TInner)" school of thought for the
// before first MoveNext() and the "whatever the
// Enumerator wants" for the after a MoveNext() returns
// false
if (!DoneMoveNext)
{
return default(T);
}
return Enumerator.Current;
}
}
public void Dispose()
{
Enumerator.Dispose();
}
object IEnumerator.Current
{
get
{
return Current;
}
}
public bool MoveNext()
{
if (!DoneMoveNext)
{
DoneMoveNext = true;
return FirstMoveNextSuccessful;
}
return Enumerator.MoveNext();
}
public void Reset()
{
// This will 99% throw :-) Not our problem.
Enumerator.Reset();
// So it is improbable we will arrive here
DoneMoveNext = true;
}
}
请注意,LINQ-to-SQL 中有一个错误:
var q5 = context.Items.Where(x => x.ID > context.Nodis.Min(y => y.ID.GetHashCode()));
var r5 = q5.ToList();
这不会抛出NotSupportedException,但不会正确执行。我认为这可能是许多在查询中使用“内部”context.SomeTable“内部”的查询的问题。