Я пытаюсь реализовать схему кэширования для моего репозитория EF, аналогичную той, что приведена в блоге здесь .Как сообщают автор и комментаторы, ограничение заключается в том, что метод генерации ключей не может создавать ключи кэша, которые зависят от параметров данного запроса.Вот метод генерации ключа кеша:
private static string GetKey<T>(IQueryable<T> query)
{
string key = string.Concat(query.ToString(), "\n\r",
typeof(T).AssemblyQualifiedName);
return key;
}
Таким образом, следующие запросы приведут к одному и тому же ключу кеша:
var isActive = true;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();
и
var isActive = false;
var query = context.Products
.OrderBy(one => one.ProductNumber)
.Where(one => one.IsActive == isActive).AsCacheable();
Обратите внимание, чтоединственное отличие состоит в том, что isActive = true
в первом запросе и isActive = false
во втором.
Любые предложения / идеи по эффективной генерации ключей кэша, которые различаются по параметрам IQueryable
, будут по-настоящему оценены.
Благодарность Сергею Барскому за предоставление схемы кэширования EF CodeFirst.
Обновление
Я сам использовал обход дерева выражений IQueryable с целью разрешения значенийпараметры, используемые в запросе.С предложением maxlego я расширил класс System.Linq.Expressions.ExpressionVisitor , чтобы посетить интересующие нас узлы выражений - в данном случае MemberExpression .Обновленный метод GetKey
выглядит примерно так:
public static string GetKey<T>(IQueryable<T> query)
{
var keyBuilder = new StringBuilder(query.ToString());
var queryParamVisitor = new QueryParameterVisitor(keyBuilder);
queryParamVisitor.GetQueryParameters(query.Expression);
keyBuilder.Append("\n\r");
keyBuilder.Append(typeof (T).AssemblyQualifiedName);
return keyBuilder.ToString();
}
И класс QueryParameterVisitor
, вдохновленный ответами Брайана Ватта и Марка Гравелла на этот вопрос , выглядит так:
/// <summary>
/// <see cref="ExpressionVisitor"/> subclass which encapsulates logic to
/// traverse an expression tree and resolve all the query parameter values
/// </summary>
internal class QueryParameterVisitor : ExpressionVisitor
{
public QueryParameterVisitor(StringBuilder sb)
{
QueryParamBuilder = sb;
Visited = new Dictionary<int, bool>();
}
protected StringBuilder QueryParamBuilder { get; set; }
protected Dictionary<int, bool> Visited { get; set; }
public StringBuilder GetQueryParameters(Expression expression)
{
Visit(expression);
return QueryParamBuilder;
}
private static object GetMemberValue(MemberExpression memberExpression, Dictionary<int, bool> visited)
{
object value;
if (!TryGetMemberValue(memberExpression, out value, visited))
{
UnaryExpression objectMember = Expression.Convert(memberExpression, typeof (object));
Expression<Func<object>> getterLambda = Expression.Lambda<Func<object>>(objectMember);
Func<object> getter = null;
try
{
getter = getterLambda.Compile();
}
catch (InvalidOperationException)
{
}
if (getter != null) value = getter();
}
return value;
}
private static bool TryGetMemberValue(Expression expression, out object value, Dictionary<int, bool> visited)
{
if (expression == null)
{
// used for static fields, etc
value = null;
return true;
}
// Mark this node as visited (processed)
int expressionHash = expression.GetHashCode();
if (!visited.ContainsKey(expressionHash))
{
visited.Add(expressionHash, true);
}
// Get Member Value, recurse if necessary
switch (expression.NodeType)
{
case ExpressionType.Constant:
value = ((ConstantExpression) expression).Value;
return true;
case ExpressionType.MemberAccess:
var me = (MemberExpression) expression;
object target;
if (TryGetMemberValue(me.Expression, out target, visited))
{
// instance target
switch (me.Member.MemberType)
{
case MemberTypes.Field:
value = ((FieldInfo) me.Member).GetValue(target);
return true;
case MemberTypes.Property:
value = ((PropertyInfo) me.Member).GetValue(target, null);
return true;
}
}
break;
}
// Could not retrieve value
value = null;
return false;
}
protected override Expression VisitMember(MemberExpression node)
{
// Only process nodes that haven't been processed before, this could happen because our traversal
// is depth-first and will "visit" the nodes in the subtree before this method (VisitMember) does
if (!Visited.ContainsKey(node.GetHashCode()))
{
object value = GetMemberValue(node, Visited);
if (value != null)
{
QueryParamBuilder.Append("\n\r");
QueryParamBuilder.Append(value.ToString());
}
}
return base.VisitMember(node);
}
}
Я все еще делаю профилирование производительности при генерации ключа кеша и надеюсь, что это не слишком дорого (я будуобновите вопрос с результатами, как только я их получу).Я оставлю вопрос открытым, если у кого-то есть предложения по оптимизации этого процесса или есть рекомендации по более эффективному способу генерации ключей кэша, зависящих от параметров запроса.Хотя этот метод дает желаемый результат, он ни в коем случае не является оптимальным.