Автоматическая компиляция запросов Linq

Мы обнаружили, что компиляция наших запросов Linq намного быстрее, чем их приходится собирать каждый раз, поэтому мы хотели бы начать используя скомпилированные запросы. Проблема в том, что он делает код более трудным для чтения, потому что фактический синтаксис запроса отключен в другом файле, вдали от того, где он используется.

Мне пришло в голову, что может быть возможно написать метод (или метод расширения), который использует отражение, чтобы определить, какие запросы передаются, и кэшировать скомпилированные версии автоматически для использования в будущем.

var foo = (from f in db.Foo where f.ix == bar select f).Cached();

Cached() должен был бы отражать переданный объект запроса и определять выбранные таблицы и типы параметров для запроса. Очевидно, что отражение немного медленное, поэтому было бы лучше использовать имена для объекта кеша (но вам все равно придется использовать отражение в первый раз для компиляции запроса).

var foo = (from f in db.Foo where f.ix == bar select f).Cached("Foo.ix");

Есть ли у кого-нибудь опыт в этом, или знаете, возможно ли это?

UPDATE:. Для тех, кто этого не видел, вы можете скомпилировать запросы LINQ к SQL со следующим кодом:

public static class MyCompiledQueries
{
    public static Func<DataContext, int, IQueryable<Foo>> getFoo =
        CompiledQuery.Compile(
            (DataContext db, int ixFoo) => (from f in db.Foo
                                            where f.ix == ixFoo
                                            select f)
        );
}

То, что я пытаюсь сделать, это иметь кэш этих объектов Func<>, которые я могу вызвать после автоматической компиляции запроса в первый раз.

Ответ 1

У вас не может быть методов расширения, вызываемых на анонимных лямбда-выражениях, поэтому вы захотите использовать класс Cache. Чтобы правильно кэшировать запрос, вам также нужно "поднять" любые параметры (включая ваш DataContext) в параметры для вашего лямбда-выражения. Это приводит к очень подробному использованию, например:

var results = QueryCache.Cache((MyModelDataContext db) => 
    from x in db.Foo where !x.IsDisabled select x);

Чтобы очистить это, мы можем создать экземпляр QueryCache для каждого контекста, если мы сделаем его нестатичным:

public class FooRepository
{
    readonly QueryCache<MyModelDataContext> q = 
        new QueryCache<MyModelDataContext>(new MyModelDataContext());
}

Затем мы можем написать метод Cache, который позволит нам написать следующее:

var results = q.Cache(db => from x in db.Foo where !x.IsDisabled select x);

Любые аргументы в вашем запросе также должны быть отменены:

var results = q.Cache((db, bar) => 
    from x in db.Foo where x.id != bar select x, localBarValue);

Здесь реализована реализация QueryCache:

public class QueryCache<TContext> where TContext : DataContext
{
    private readonly TContext db;
    public QueryCache(TContext db)
    {
        this.db = db;
    }

    private static readonly Dictionary<string, Delegate> cache = new Dictionary<string, Delegate>();

    public IQueryable<T> Cache<T>(Expression<Func<TContext, IQueryable<T>>> q)
    {
        string key = q.ToString();
        Delegate result;
        lock (cache) if (!cache.TryGetValue(key, out result))
        {
            result = cache[key] = CompiledQuery.Compile(q);
        }
        return ((Func<TContext, IQueryable<T>>)result)(db);
    }

    public IQueryable<T> Cache<T, TArg1>(Expression<Func<TContext, TArg1, IQueryable<T>>> q, TArg1 param1)
    {
        string key = q.ToString();
        Delegate result;
        lock (cache) if (!cache.TryGetValue(key, out result))
        {
            result = cache[key] = CompiledQuery.Compile(q);
        }
        return ((Func<TContext, TArg1, IQueryable<T>>)result)(db, param1);
    }

    public IQueryable<T> Cache<T, TArg1, TArg2>(Expression<Func<TContext, TArg1, TArg2, IQueryable<T>>> q, TArg1 param1, TArg2 param2)
    {
        string key = q.ToString();
        Delegate result;
        lock (cache) if (!cache.TryGetValue(key, out result))
        {
            result = cache[key] = CompiledQuery.Compile(q);
        }
        return ((Func<TContext, TArg1, TArg2, IQueryable<T>>)result)(db, param1, param2);
    }
}

Это может быть расширено для поддержки большего количества аргументов. Великий бит заключается в том, что, передавая значения параметров самому методу Cache, вы получаете неявное типирование для выражения лямбда.

EDIT: Обратите внимание, что вы не можете применять новые операторы к скомпилированным запросам. В частности, вы не можете сделать что-то вроде этого:

var allresults = q.Cache(db => from f in db.Foo select f);
var page = allresults.Skip(currentPage * pageSize).Take(pageSize);

Итак, если вы планируете подкачку запроса, вам нужно сделать это в операции компиляции, а не делать это позже. Это необходимо не только для исключения исключения, но и в соответствии со всей точкой Skip/Take (чтобы не возвращать все строки из базы данных). Этот шаблон будет работать:

public IQueryable<Foo> GetFooPaged(int currentPage, int pageSize)
{
    return q.Cache((db, cur, size) => (from f in db.Foo select f)
        .Skip(cur*size).Take(size), currentPage, pageSize);
}

Другим подходом к поисковому вызову будет возврат Func:

public Func<int, int, IQueryable<Foo>> GetPageableFoo()
{
    return (cur, size) => q.Cache((db, c, s) => (from f in db.foo select f)
        .Skip(c*s).Take(s), c, s);
}

Этот шаблон используется как:

var results = GetPageableFoo()(currentPage, pageSize);

Ответ 2

Поскольку никто не пытается, я сделаю это. Может быть, мы сможем как-то это разобраться. Вот моя попытка.

Я установил это с помощью словаря, я также не использую DataContext, хотя это тривиально, я считаю.

public static class CompiledExtensions
    {
        private static Dictionary<string, object> _dictionary = new Dictionary<string, object>();

        public static IEnumerable<TResult> Cache<TArg, TResult>(this IEnumerable<TArg> list, string name, Expression<Func<IEnumerable<TArg>, IEnumerable<TResult>>> expression)
        {
            Func<IEnumerable<TArg>,IEnumerable<TResult>> _pointer;

            if (_dictionary.ContainsKey(name))
            {
                _pointer = _dictionary[name] as Func<IEnumerable<TArg>, IEnumerable<TResult>>;
            }
            else
            {
                _pointer = expression.Compile();
                _dictionary.Add(name, _pointer as object);
            }

            IEnumerable<TResult> result;
            result = _pointer(list);

            return result;
        }
    }

теперь это позволяет мне сделать это

  List<string> list = typeof(string).GetMethods().Select(x => x.Name).ToList();

  IEnumerable<string> results = list.Cache("To",x => x.Where( y => y.Contains("To")));
  IEnumerable<string> cachedResult = list.Cache("To", x => x.Where(y => y.Contains("To")));
  IEnumerable<string> anotherCachedResult = list.Cache("To", x => from item in x where item.Contains("To") select item);

с нетерпением ожидая некоторого обсуждения этого вопроса, для дальнейшего развития этой идеи.

Ответ 3

Для будущих потомков:.NET Framework 4.5 сделает это по умолчанию (в соответствии с слайдом в презентации, которую я только что наблюдал).