Skip to content

Commit 5c4abbc

Browse files
author
Jeff Treuting
committed
Fix caching bug when variables are used in the query instead of constants
I added a HashGeneratorTests unit test and added some various kinds of predicates that include constants, variables, list variables. We should keep expanding this to make sure it covers everything we can think of.
1 parent 4837ace commit 5c4abbc

13 files changed

Lines changed: 1096 additions & 580 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Text;
6+
7+
namespace SharpRepository.Repository.Caching.Hash
8+
{
9+
/// <summary>
10+
/// Enables the partial evaluation of queries.
11+
/// </summary>
12+
/// <remarks>
13+
/// From http://msdn.microsoft.com/en-us/library/bb546158.aspx
14+
/// Copyright notice http://msdn.microsoft.com/en-gb/cc300389.aspx#O
15+
/// </remarks>
16+
public static class Evaluator
17+
{
18+
/// <summary>
19+
/// Performs evaluation & replacement of independent sub-trees
20+
/// </summary>
21+
/// <param name="expression">The root of the expression tree.</param>
22+
/// <param name="fnCanBeEvaluated">A function that decides whether a given expression node can be part of the local function.</param>
23+
/// <returns>A new tree with sub-trees evaluated and replaced.</returns>
24+
public static Expression PartialEval(Expression expression, Func<Expression, bool> fnCanBeEvaluated)
25+
{
26+
return new SubtreeEvaluator(new Nominator(fnCanBeEvaluated).Nominate(expression)).Eval(expression);
27+
}
28+
29+
/// <summary>
30+
/// Performs evaluation & replacement of independent sub-trees
31+
/// </summary>
32+
/// <param name="expression">The root of the expression tree.</param>
33+
/// <returns>A new tree with sub-trees evaluated and replaced.</returns>
34+
public static Expression PartialEval(Expression expression)
35+
{
36+
return PartialEval(expression, Evaluator.CanBeEvaluatedLocally);
37+
}
38+
39+
private static bool CanBeEvaluatedLocally(Expression expression)
40+
{
41+
return expression.NodeType != ExpressionType.Parameter;
42+
}
43+
44+
/// <summary>
45+
/// Evaluates & replaces sub-trees when first candidate is reached (top-down)
46+
/// </summary>
47+
private class SubtreeEvaluator : ExpressionVisitor
48+
{
49+
private HashSet<Expression> candidates;
50+
51+
internal SubtreeEvaluator(HashSet<Expression> candidates)
52+
{
53+
this.candidates = candidates;
54+
}
55+
56+
internal Expression Eval(Expression exp)
57+
{
58+
return this.Visit(exp);
59+
}
60+
61+
public override Expression Visit(Expression exp)
62+
{
63+
if (exp == null)
64+
{
65+
return null;
66+
}
67+
if (this.candidates.Contains(exp))
68+
{
69+
return this.Evaluate(exp);
70+
}
71+
return base.Visit(exp);
72+
}
73+
74+
private Expression Evaluate(Expression e)
75+
{
76+
if (e.NodeType == ExpressionType.Constant)
77+
{
78+
return e;
79+
}
80+
LambdaExpression lambda = Expression.Lambda(e);
81+
Delegate fn = lambda.Compile();
82+
return Expression.Constant(fn.DynamicInvoke(null), e.Type);
83+
}
84+
85+
}
86+
87+
/// <summary>
88+
/// Performs bottom-up analysis to determine which nodes can possibly
89+
/// be part of an evaluated sub-tree.
90+
/// </summary>
91+
class Nominator : ExpressionVisitor
92+
{
93+
Func<Expression, bool> fnCanBeEvaluated;
94+
HashSet<Expression> candidates;
95+
bool cannotBeEvaluated;
96+
97+
internal Nominator(Func<Expression, bool> fnCanBeEvaluated)
98+
{
99+
this.fnCanBeEvaluated = fnCanBeEvaluated;
100+
}
101+
102+
internal HashSet<Expression> Nominate(Expression expression)
103+
{
104+
this.candidates = new HashSet<Expression>();
105+
this.Visit(expression);
106+
return this.candidates;
107+
}
108+
109+
public override Expression Visit(Expression expression)
110+
{
111+
if (expression != null)
112+
{
113+
bool saveCannotBeEvaluated = this.cannotBeEvaluated;
114+
this.cannotBeEvaluated = false;
115+
base.Visit(expression);
116+
if (!this.cannotBeEvaluated)
117+
{
118+
if (this.fnCanBeEvaluated(expression))
119+
{
120+
this.candidates.Add(expression);
121+
}
122+
else
123+
{
124+
this.cannotBeEvaluated = true;
125+
}
126+
}
127+
this.cannotBeEvaluated |= saveCannotBeEvaluated;
128+
}
129+
return expression;
130+
}
131+
}
132+
}
133+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using SharpRepository.Repository.Specifications;
6+
7+
namespace SharpRepository.Repository.Caching.Hash
8+
{
9+
public class HashGenerator
10+
{
11+
public static string FromSpecification<T>(ISpecification<T> specification)
12+
{
13+
return FromPredicate<T>(specification.Predicate);
14+
}
15+
16+
public static string FromPredicate<T>(Expression<Func<T,bool>> predicate)
17+
{
18+
return FromPredicate(predicate.Body);
19+
}
20+
21+
public static string FromPredicate(Expression expression)
22+
{
23+
return new HashGeneratorHelper(expression).GetHash();
24+
}
25+
}
26+
27+
internal class HashGeneratorHelper
28+
{
29+
private readonly Expression _expression;
30+
private string _values;
31+
32+
public HashGeneratorHelper(Expression expression)
33+
{
34+
_expression = expression;
35+
}
36+
37+
public string GetHash()
38+
{
39+
if (_expression == null)
40+
return null;
41+
42+
var expression = _expression;
43+
44+
// locally evaluate as much of the query as possible
45+
expression = Evaluator.PartialEval(expression);
46+
47+
// support local collections
48+
expression = LocalCollectionExpander.Rewrite(expression);
49+
50+
// use the string representation of the expression for the cache key
51+
string key = expression.ToString();
52+
53+
// the key is potentially very long, so use an md5 fingerprint
54+
// (fine if the query result data isn't critically sensitive)
55+
//key = key.ToMd5Fingerprint();
56+
57+
58+
59+
return key;
60+
}
61+
62+
63+
}
64+
65+
internal class HashGeneratorHelper2<T>
66+
{
67+
private readonly ISpecification<T> _specification;
68+
private string _values;
69+
70+
public HashGeneratorHelper2(ISpecification<T> specification)
71+
{
72+
_specification = specification;
73+
}
74+
75+
public string GetHash()
76+
{
77+
if (_specification == null || _specification.Predicate == null || _specification.Predicate.Body == null)
78+
return null;
79+
80+
VisitExpression(_specification.Predicate.Body);
81+
82+
var hash = _specification.ToString();
83+
84+
if (!String.IsNullOrEmpty(_values))
85+
{
86+
hash += String.Format("values[{0}]", _values);
87+
}
88+
89+
return hash;
90+
}
91+
private void VisitExpression(Expression expression)
92+
{
93+
if (expression == null)
94+
return;
95+
96+
switch (expression.NodeType)
97+
{
98+
// case ExpressionType.Negate:
99+
// case ExpressionType.NegateChecked:
100+
// case ExpressionType.Not:
101+
// case ExpressionType.Convert:
102+
// case ExpressionType.ConvertChecked:
103+
// case ExpressionType.ArrayLength:
104+
// case ExpressionType.Quote:
105+
// case ExpressionType.TypeAs:
106+
// case ExpressionType.UnaryPlus:
107+
// VisitUnary((UnaryExpression)expression);
108+
// break;
109+
110+
case ExpressionType.Add:
111+
case ExpressionType.AddChecked:
112+
case ExpressionType.Subtract:
113+
case ExpressionType.SubtractChecked:
114+
case ExpressionType.Multiply:
115+
case ExpressionType.MultiplyChecked:
116+
case ExpressionType.Divide:
117+
case ExpressionType.Modulo:
118+
case ExpressionType.And:
119+
case ExpressionType.AndAlso:
120+
case ExpressionType.Or:
121+
case ExpressionType.OrElse:
122+
case ExpressionType.LessThan:
123+
case ExpressionType.LessThanOrEqual:
124+
case ExpressionType.GreaterThan:
125+
case ExpressionType.GreaterThanOrEqual:
126+
case ExpressionType.Equal:
127+
case ExpressionType.NotEqual:
128+
case ExpressionType.Coalesce:
129+
case ExpressionType.ArrayIndex:
130+
case ExpressionType.RightShift:
131+
case ExpressionType.LeftShift:
132+
case ExpressionType.ExclusiveOr:
133+
case ExpressionType.Power:
134+
VisitBinary((BinaryExpression)expression);
135+
break;
136+
137+
case ExpressionType.MemberAccess:
138+
VisitMemberAccess((MemberExpression)expression);
139+
break;
140+
141+
case ExpressionType.Constant:
142+
VisitConstant((ConstantExpression)expression);
143+
break;
144+
145+
default:
146+
VisitGeneric(expression);
147+
break;
148+
}
149+
}
150+
151+
private void VisitGeneric(Expression expression)
152+
{
153+
// nothing to do
154+
}
155+
156+
private void VisitUnary(UnaryExpression u)
157+
{
158+
VisitExpression(u.Operand);
159+
}
160+
161+
private void VisitBinary(BinaryExpression b)
162+
{
163+
VisitExpression(b.Left);
164+
VisitExpression(b.Right);
165+
}
166+
167+
private void VisitConstant(ConstantExpression c)
168+
{
169+
if (!String.IsNullOrEmpty(_values))
170+
return; // only need to get this once, it has all the values
171+
172+
// _values = c.Value.GetHashCode().ToString();
173+
// return;
174+
175+
var type = c.Value.GetType();
176+
foreach (var fieldInfo in type.GetFields())
177+
{
178+
string value;
179+
180+
var fieldValue = fieldInfo.GetValue(c.Value);
181+
182+
if (fieldValue == null)
183+
{
184+
value = "-null-";
185+
}
186+
else
187+
{
188+
var fieldType = fieldValue.GetType();
189+
190+
if (fieldType != typeof(string) && GetEnumerableType(fieldType) != null)
191+
{
192+
value = "IENUMERABLE TODO";
193+
}
194+
else
195+
{
196+
value = fieldValue.ToString();
197+
}
198+
}
199+
200+
_values += fieldInfo.Name + "=" + value + ";";
201+
}
202+
}
203+
204+
private void VisitMemberAccess(MemberExpression m)
205+
{
206+
VisitExpression(m.Expression);
207+
}
208+
209+
static Type GetEnumerableType(Type type)
210+
{
211+
return (from intType in type.GetInterfaces() where intType.IsGenericType && intType.GetGenericTypeDefinition() == typeof (IEnumerable<>) select intType.GetGenericArguments()[0]).FirstOrDefault();
212+
}
213+
}
214+
}

0 commit comments

Comments
 (0)