Как создать очень динамичный запрос LinqToEntity?

Мне нужно построить очень динамичный запрос Linq для различного количества таблиц. Например, у меня есть связанные таблицы:
Table_A
- ID
- Name
- Desc

Table_B
– ID
– Table_A_ID
– Имя
– Описание

Table_C
– ID
– Table_B_ID
– Имя
– Описание

У меня есть словарь с информацией о зависимостях таблиц, содержащий:
tableName, parentTableName, ForeignKey, parentPK
Пример:
"Table_B", "Table_A", "Table_A_ID", " ID"
"Table_C", "Table_B", "Table_B_ID", "ID"

-> tableInfo["Table_B"].ForeignKey вернет "Table_A_ID" и т. д.

Now the user can select which columns he wants to see.
Examples:

  • Table_B.Name, Table_C.Desc
     or
  • Table_A.Name, Table_B.Name
     or
  • Table_A.Name, Table_B.Name, Table_B.Desc, Table_C.Name

    Этот выбор будет доступен в другом списке:
    Например, для выбора 3:
    viewInfo["Table_A"] содержит "Name"
    viewInfo["Table_B"] содержит "Name", "Desc"
    viewInfo["Table_C"] содержит "Имя"

    Как мне динамически создать запрос, используя только необходимые таблицы и поля, чтобы получить желаемый результат?


  • person cew3    schedule 17.08.2010    source источник


    Ответы (3)


    Я сделал то же самое для проекта, над которым я работаю, где запрос полностью создается во время выполнения на основе выбора, сделанного пользователем в пользовательском интерфейсе.

    Я создаю запросы LINQ, используя деревья выражений, используя классы в пространстве имен System.Linq.Expressions. Это очень мощно, но имеет крутую кривую обучения.

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

    Например, выполнение следующего кода в LINQPad создаст дамп дерева выражений.

    var query = from p in Puzzles
    select p;
    
    query.Expression.Dump(20);
    

    Снимок экрана LINQPad

    Так как же на самом деле написать код, который динамически создает простой запрос LINQ?

    Рассмотрим следующий пример, простейший из запросов:

    var query = from person in data
       select person;
    

    Следующий код будет генерировать эквивалентный запрос на лету.

    using System;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    
    namespace TestLinqGenerator
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Set up dummy data
                var data = new[]
                               {
                                   new {Name = "Fred"},
                                   new {Name = "Simon"}
                               }.AsQueryable();
                var dataType = data.ElementType;
    
                // IQueryable: data
                var source = Expression.Constant(data);
    
                // Parameter: person
                var parameter = Expression.Parameter(dataType, "person");
    
                // person => person
                var lambda = Expression.Lambda(parameter, parameter);
    
                // Expression: data.Select(person => person)
                var callSelect = Expression.Call(GetSelect().MakeGenericMethod(dataType, dataType), source, Expression.Quote(lambda));
    
                // IQueryable: data.Select(person => person)
                var query = data.Provider.CreateQuery(callSelect);
    
                // Execute query
                var results = query.Cast<object>().ToList();
    
            }
    
            private static MethodInfo GetSelect()
            {
                // Get MethodInfo of the following method from System.Linq.Queryable:
                // public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
                return typeof(System.Linq.Queryable).GetMethods().Where(
                    method => method.Name == "Select" && method.GetParameters().Length == 2 &&
                              method.GetParameters()[1].ParameterType.GetGenericArguments()[0].Name == typeof(Func<,>).Name).Single();
            }
    
        }
    }
    

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

    Дополнительная информация

    Глядя на реализацию Queryable.Select с помощью Reflector, можно понять, что должно происходить при динамическом написании запроса. Я скопировал это ниже:

    public static IQueryable<TResult> Select<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, int, TResult>> selector)
    {
        if (source == null)
        {
            throw Error.ArgumentNull("source");
        }
        if (selector == null)
        {
            throw Error.ArgumentNull("selector");
        }
        return source.Provider.CreateQuery<TResult>(Expression.Call(null, ((MethodInfo) MethodBase.GetCurrentMethod()).MakeGenericMethod(new Type[] { typeof(TSource), typeof(TResult) }), new Expression[] { source.Expression, Expression.Quote(selector) }));
    }
    

    Интересно, что реализация Queryable.Select просто создает представление LINQ Expression для вызова самого себя. Поставщик LINQ на самом деле переводит это выражение во что-то другое — TSQL. Сам метод Select на самом деле не выполняет выбор.

    Ваш код должен делать то же самое — создавать выражения LINQ.

    Как только вы освоитесь с простым выбором, вы можете добавить Queryable.Where к сочетанию и другим функциям запроса LINQ. Я предлагаю оставить прогнозы (select new {x, y, z} и т. д.) напоследок, потому что они довольно сложны. Вам нужно будет генерировать типы во время выполнения почти так же, как компилятор генерирует для вас анонимные типы. System.Reflection.Emit — ваш рабочий инструмент.

    Одним из преимуществ этого подхода является то, что его можно использовать с любым провайдером LINQ, таким как LINQ to Entities, LINQ to SQL, Mindscape Lightspeed и реализацией поставщика LINQ в памяти, предоставляемой AsQueryable.

    Мой код, который генерирует выражения LINQ, будет принимать IQueryable, и во время выполнения он в настоящее время поставляется с IQueryables Mindscape Lightspeed, но также может быть одним из других. Затем в своих модульных тестах я создаю тестовые данные, используя массивы объектов, а затем превращаю их в IQueryable, используя AsQueryable, которые передаются в генератор выражений LINQ. Мои модульные тесты могут затем генерировать все диапазоны сложных запросов, но их можно легко протестировать, не требуя базы данных. Пример выше показывает, как это можно сделать.

    person John Mills    schedule 17.08.2010
    comment
    У вас есть пример создания запроса вручную? Если я посмотрю на сгенерированное лямбда-выражение, то пойму, что если я захочу создать его вручную, мне придется обрабатывать строго типизированные вещи. Вы так поняли свое решение? - person cew3; 18.08.2010
    comment
    У меня нет небольших фрагментов кода, которыми я могу поделиться, чтобы продемонстрировать технику. Подход, который я использую, задает запросы в модели более высокого уровня, а затем преобразует модель в дерево выражений LINQ, представляющее запрос LINQ, подобный тому, который вы написали бы на C#. Затем вы можете выполнить запрос и получить результаты. Используя этот метод, вы получаете всю мощь LINQ, но это не тривиально. Я обновлю свой ответ, чтобы включить больше информации. - person John Mills; 18.08.2010
    comment
    Спасибо за подробную информацию! Но... быстрый запуск слишком быстрый для меня :-( Не могли бы вы дать мне больше информации о том, как это закодировать? - person cew3; 18.08.2010
    comment
    Я написал для вас небольшую примерную программу и изменил ответ. - person John Mills; 19.08.2010
    comment
    Как вы заставили LinqPad оценить Puzzles? Как заставить его работать с Linq to Objects? - person toddmo; 30.12.2014
    comment
    Путем сброса свойства Expression интерфейса IQueryable<T>. Чтобы получить поставщика LINQ для LINQ to Objects, вам необходимо изменить исходную коллекцию (которая реализует IEnumerable<T>) на IQueryable<T>. Вы можете сделать это, используя метод расширения AsQueryable. то есть var q = from x in myCollection.AsQueryable() select x; q.Expression.Dump();. - person John Mills; 06.01.2015

    Существует проект под названием Динамический LINQ, помогающий динамически создавать запросы. Думаю, вам стоит обратить внимание на этот проект.

    Помимо этого, также можно создавать запросы по частям, запрашивая запрос LINQ. Вы можете поместить условные операторы в свой код, и если будет следовать какая-то ветвь, вы можете создать новый запрос из существующего запроса, запросив его снова. Запрос не выполняется до тех пор, пока вы не запросите результаты, поэтому с точки зрения производительности не имеет большого значения, если вы создадите запрос небольшими частями или сделаете один большой запрос с самого начала. Используя этот метод, вы можете (на основе значений входных данных) создавать структурно разные запросы, которые имеют некоторые общие части, имея при этом преимущества статической типизации и intellisense.

    person Mark Byers    schedule 17.08.2010
    comment
    Моя проблема в том, что у меня есть набор из примерно 200 таблиц с некоторыми зависимостями от других таблиц в этом наборе. Если я понял вашу идею с повторными запросами, я думаю, что невозможно создать запрос с нефиксированным источником данных (запрос = ... в XXX), верно? Но XXX не исправляется во время разработки. Все, что у меня есть, это знание иерархии выбранных пользователем таблиц (и полей) для создания запроса во время выполнения. - person cew3; 18.08.2010
    comment
    @cew: не совсем понятно, что вы пытаетесь сделать из своего вопроса, но если ваша таблица является переменной и у вас есть 200 таблиц, вы можете подумать, выиграет ли ваша база данных от небольшой нормализации. Например, если у вас есть table_foo1, table_foo2, table_foo3 с одинаковыми столбцами, то лучше просто иметь один table_foo с дополнительным столбцом, содержащим тип (1,2 или 3). Но вам придется предоставить более подробную информацию, если вы хотите получить более конкретные идеи о том, как нормализовать. - person Mark Byers; 18.08.2010
    comment
    У меня есть много разных, не нормализуемых таблиц без жестко закодированных отношений. Информация о зависимостях извлекается из другого источника данных. Мне нужно объединить данные из полей разных таблиц, которые пользователь выбрал в пользовательском интерфейсе. - person cew3; 18.08.2010

    Я решил свою проблему, используя очень интересную структуру NLinq, найденную на Codeplex. Вам просто нужно создать строку, содержащую ваш «обычный» запрос Linq!

    Цитата из описания проекта:

    NLinq — это платформа, ориентированная на повторную реализацию функций Linq в Visual Studio .Net 2003 и Visual Studio 2005 (C# и VB .Net) путем предоставления анализатора грамматики Linq и среды выполнения «Linq To Objects». С помощью NLinq вы можете воспользоваться основными функциями C# 3.0 прямо сейчас, не требуя этого.

    Пример:

    Data sources used for the samples
            Person[] people = new Person[] { 
                new Person("Bill", 31), 
                new Person("John", 30), 
                new Person("Cindy", 25), 
                new Person("Sue", 29) 
            };
    
            // For testing physical links
            people[0].Friends.Add(people[0]);
            people[0].Friends.Add(people[1]);
            people[1].Friends.Add(people[2]);
            people[2].Friends.Add(people[3]);
            people[3].Friends.Add(people[0]);
    
            // For testing logical links
            Address[] addresses = new Address[] {
                new Address("Bill", "Redmon"),
                new Address("Bill", "Boston"),
                new Address("Cindy", "New York")
            };
    
    Projections query = new NLinqQuery(
                    @"  from c in people 
                        from d in people
                        where c.Age > d.Age
                        select new NLinq.Play.Person ( c.Firstname, d.Age )");
    
            linq = new LinqToMemory(query);
            linq.AddSource("people", people);
    
    
    Result:
    Sue (25)
    John (25)
    John (29)
    Bill (30)
    Bill (25)
    Bill (29)
    
    person cew3    schedule 23.08.2010