Как использовать Queryable.Where, когда тип установлен во время выполнения? - PullRequest
0 голосов
/ 01 февраля 2019

Я реализую интерфейс пользовательского интерфейса поиска / фильтра для приложения, использующего EF6.У меня есть код, который создает выражение для использования с Queryable.Where для данного DbSet, где тип DbSet определяется во время выполнения (DBContext имеет много из них, и они могут измениться).Вызов Where работает нормально, если я обманываю, сначала приводя Expression к определенному типу.В противном случае я получаю эту ошибку:

'Наилучший перегруженный метод соответствует' System.Linq.Queryable.Where (System.Linq.IQueryable, System.Linq.Expressions.Expression>) 'имеет недопустимые аргументы'

Я изо всех сил пытаюсь найти способ отфильтровать DbSet таким образом, где базовый тип 'table' предоставляется во время выполнения.Вот очень упрощенная версия кода для иллюстрации:

    void ProcessFilter(AppDbContext context, NameValueCollection filters, Type tableType)
    {
        // If tableType == typeof(Organisation), expression is a Expression<Func<Organisation, bool>>
        var expression = GetFilterExpression(filters);
        var dbset = Set(context, tableType);

        dynamic dynamicSet = dbset;

        // This fails
        var results = Queryable.Where(dynamicSet, expression);
        // see /3830651/iqueryable-ne-universalnyi-otsutstvuet-count-i-skip-eto-rabotaet-s-iqueryable-t

        // Suppose tableType == typeof(Organisation)
        // This works
        var typedExpression = expression as Expression<Func<Organisation, bool>>;
        var typedResults = Queryable.Where(dynamicSet, typedExpression);

    }

    public static IQueryable Set(DbContext context, Type T)
    {
        // Similar to code in
        // https://stackoverflow.com/questions/21533506/find-a-specified-generic-dbset-in-a-dbcontext-dynamically-when-i-have-an-entity
        var method = typeof(DbContext).GetMethods(BindingFlags.Public | BindingFlags.Instance).Where(x => x.Name == "Set" && x.IsGenericMethod).First();

        // Build a method with the specific type argument 
        method = method.MakeGenericMethod(T);

        return method.Invoke(context, null) as IQueryable;
    }

Ответы [ 2 ]

0 голосов
/ 02 февраля 2019

Отвечая на ваш конкретный вопрос.Учитывая

IQueryable source
LambdaExpression predicate

, как вызывать статический универсальный метод

Queryable.Where<T>(IQueryable<T> source, Expression<Func<T, bool>> predicate)

Это можно сделать с помощью (A) отражения, (B) динамической диспетчеризации DLR и (C) Expression.Call.

То, что вы пытаетесь сделать, это вариант (B).Однако

var result = Queryable.Where((dynamic)source, predicate);

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

Чтобы иметь возможность динамически сопоставлять целевой метод, вам также необходимо указать второй аргумент dynamic:

var result = Queryable.Where((dynamic)source, (dynamic)predicate); 

Эквивалентная реализация опции (C) вышеупомянутогоэто:

var result = source.Provider.CreateQuery(Expression.Call(
    typeof(Queryable), nameof(Queryable.Where), new[] { source.ElementType },
    source.Expression, predicate));
0 голосов
/ 02 февраля 2019

поздравляю с первым вопросом.

Давайте начнем с рассмотрения подхода к фильтрации коллекции данных на основе некоторых пользовательских фильтров.Я предполагаю, что NameValueCollection Type, который вы предпочитаете передавать в своих фильтрах, содержит PropertyNames в качестве ключей и PropertyValues ​​в качестве значения.

Прежде чем приступить к фильтрации всей коллекции, давайте сначала выясним, как определитьесть ли у одного объекта свойства, соответствующие нашим фильтрам.И поскольку мы не знаем Type нашего объекта до времени выполнения, нам нужно будет использовать Generics в C # для этого.

Шаг 1

- ПолучитьВсе свойства класса

Нам потребуется получить все свойства нашего универсального класса, например, <TClass>.Выполнение этого с помощью Reflection считается медленным, и Мэтт Уоррен объясняет Почему в .NET медленное отражение и как его обойти.Поэтому мы будем реализовывать кэширование модели компонентов класса, чтобы получить ее PropertyDescriptorCollection, которая существует в пространстве имен System.ComponentModel.PropertyDescriptorCollection.

Кэш компонентов

private static IDictionary<string, PropertyDescriptorCollection> _componentsCache
        = new Dictionary<string, PropertyDescriptorCollection>();

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

internal static bool InnerFilter<T>(T obj, NameValueCollection filters)
        where T : class
{
        Type type = typeof(T);
        PropertyDescriptorCollection typeDescriptor = null;

        if (_componentsCache.ContainsKey(type.Name))
            typeDescriptor = _componentsCache[type.Name];
        else
        {
            typeDescriptor = TypeDescriptor.GetProperties(type);
            _componentsCache.Add(type.Name, typeDescriptor);
        }
}

Шаг 2

- Проход по фильтрам

После того, как мы получили PropertyDescriptorCollection для универсального класса T в переменной typeDescriptor, как показано выше, теперь давайте пройдемся по нашим фильтрам и посмотрим, соответствует ли какое-либо из его имен свойств любому из наших ключей фильтра.Если T имеет имя свойства, которое соответствует любому из наших ключей фильтра, теперь мы проверяем, соответствует ли фактическое значение свойства нашему значению фильтра.Чтобы улучшить качество нашей функции поиска / фильтрации, мы будем использовать Регулярные выражения в C # , чтобы определить, является ли сравнение попаданием или промахом.

for (int i = 0; i < filters.Count; i++)
{
    string filterName = filters.GetKey(i);
    string filterValue = filters[i];

    PropertyDescriptor propDescriptor = typeDescriptor[filterName];
    if (propDescriptor == null)
        continue;
    else
    {
        string propValue = propDescriptor.GetValue(obj).ToString();
        bool isMatch = Regex.IsMatch(propValue, $"({filterValue})");
        if (isMatch)
            return true;
        else
            continue;
    }
}

Шаг 3

- Реализация методов расширения.

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

- универсальная функция фильтра коллекции, которая использует вышеуказанную функцию.

, поскольку IQueryable<T> можно преобразовать в IEnumerable<T> с помощью. Where() функция в System.Linq, мы собираемся использовать это в нашем вызове функции, как показано ниже.

public static IEnumerable<T> Filter<T>(this IEnumerable<T> collection, NameValueCollection filters)
        where T : class
{
    if (filters.Count < 1)
        return collection;

    return collection.Where(x => x.InnerFilter(filters));
}

Шаг 4

Соберите все вместе.

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

public static class Question54484908 
{
    private static IDictionary<string, PropertyDescriptorCollection> _componentsCache = new Dictionary<string, PropertyDescriptorCollection> ();

    public static IEnumerable<T> Filter<T> (this IEnumerable<T> collection, NameValueCollection filters)
        where T : class 
    {
        if (filters.Count < 1)
            return collection;

        return collection.Where (x => x.InnerFilter (filters));
    }

    internal static bool InnerFilter<T> (this T obj, NameValueCollection filters)
        where T : class 
    {
        Type type = typeof (T);
        PropertyDescriptorCollection typeDescriptor = null;

        if (_componentsCache.ContainsKey (type.Name))
            typeDescriptor = _componentsCache[type.Name];
        else {
            typeDescriptor = TypeDescriptor.GetProperties (type);
            _componentsCache.Add (type.Name, typeDescriptor);
        }

        for (int i = 0; i < filters.Count; i++) {
            string filterName = filters.GetKey (i);
            string filterValue = filters[i];

            PropertyDescriptor propDescriptor = typeDescriptor[filterName];
            if (propDescriptor == null)
                continue;
            else {
                string propValue = propDescriptor.GetValue (obj).ToString ();
                bool isMatch = Regex.IsMatch (propValue, $"({filterValue})");
                if (isMatch)
                    return true;
                else
                    continue;
            }
        }

        return false;
    }
}

FINALLY

ФильтрацияIEnumerable<T>, List<T>, массивы

Вот как вы собираетесь использовать тВыше кода в любом месте вашего проекта.

private IEnumerable<Question> _questions;
_questions = new List<Question>()
{
    new Question("Question 1","How do i work with tuples"),
    new Question("Question 2","How to use Queryable.Where when type is set at runtime?")
};
var filters = new NameValueCollection 
{ 
   { "Description", "work" }
};
var results = _questions.Filter(filters);

Фильтрация DbSet<T>

Каждый DbContext имеет функцию .Set<T>, которая возвращает DbSet<T>, который может использоваться как IQueryable<T> и, таким образом, наша функция может быть использована так же, как показано ниже.

Пример

_dbContext.Set<Question>().Filter(filters);

Надеюсь, что это ответ на ваш вопрос или скорее направит вас в правильном направлении.

...