Расширения LINQ для Azure Table Storage, реализующие Or и Contains

в 11:22, , рубрики: .net, AtContent, azure table storage, CPlase, nosql, windows azure, Облачные вычисления, метки: , , ,

Всем привет! Рад представить вам уже пятую статью из цикла «Внутреннее устройство и архитектура сервиса AtContent.com». В ней я расскажу о том как сделать работу с Azure Table Storage более функциональной и удобной.

LINQ

Платформа Windows Azure дает очень мощный набор инструментов для реализации своих идей. И среди них – Azure Table Storage – нереляционная база данных с неограниченным объемом. Большим плюсом этого хранилища является то, что можно делать к нему достаточно сложные запросы. Но помимо этого есть и некоторые неудобства. Так, например, с помощью LINQ нельзя выполнить запросы, в которых есть логика Or или Contains без дополнительных модификаций.

Когда же нам пришлось с этим столкнуться мы изучили проблему и окзалось, что с помощью REST API можно делать запросы в которых есть Or. И нам очень нужны были такие запросы. Не переписывать же теперь весь код с LINQ на REST API ради этого. Копнув чуть глубже мы нашли решение!

Итак, чтобы сохранить удобный способ работы с Azure Table Storage – LINQ – и дополнить его возможность делать Contains-запросы потребовалось разобрать механизмы, которые происходят в глубинах LINQ при трансформации запроса в REST API. Сама проблема заключается в том, что динамически модифицировать LINQ-запрос у нас получилось только с помощью Where. При этом выражение добавляется к запросу с модификатором And. Поэтому составить запрос с переменным числом параметров, соединенных оператором And не представляет никакой сложности. Если же потребуется сделать то же самое для Or – придется привлекать на помощь лямда-выражения.

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

Первая попытка закончилась неудачей. Поисковые системы ничем не могли помочь в данном вопросе и поэтому приходилось действовать с помощью эксперимента. В IQuerable.Where() можно подставить Expression<Func<T,bool>>. С горем пополам составив такое выражение и опробовав его меня постигло разочарование. Полученное выражение после Invoke() включало в себя имена классов и такой запрос к Azure Table Storage конечно же возвращал исключение.

Но это меня не остановило. Перспектива использовать REST API вместо LINQ не давала мне уснуть. Всего пара дней интенсивного чтения MSDN и родилось элегантное решение, которое можно очень просто применять.

public static class LinqExtension
{
    public static Expression<Func<T, bool>> Contains<T>(string ObjectStringName, string FieldStringNameToCompare, IList<String> ListOfValues)
    {
        Expression ExprBody = null;
        ParameterExpression ParameterObject = Expression.Parameter(typeof(T), ObjectStringName);
        var PropertyFieldToCompare = Expression.Property(ParameterObject, FieldStringNameToCompare);
        foreach (var ValueToCompare in ListOfValues)
        {
            ConstantExpression ValueConst = Expression.Constant(ValueToCompare, typeof(string));
            BinaryExpression EqualTermExpression = Expression.Equal(PropertyFieldToCompare, ValueConst);
            if (ExprBody == null) { ExprBody = EqualTermExpression; }
            else
            {
                ExprBody = Expression.Or(ExprBody, EqualTermExpression);
            }
        }
        if (ExprBody == null)
        {
            ExprBody = Expression.IsTrue(Expression.Constant(false));
        }
        var FinalExpression = Expression.Lambda<Func<T, bool>>(ExprBody, new ParameterExpression[] { ParameterObject });
        return FinalExpression;
    }

    public static IQueryable<T> Contains<T>(this IQueryable<T> obj, string ObjectStringName, string FieldStringNameToCompare, IList<String> ListOfValues)
    {
        var Expression = Contains<T>(ObjectStringName, FieldStringNameToCompare, ListOfValues);
        if (Expression != null)
            return obj.Where<T>(Expression);
        return obj;
    }
}

Оно позволяет применить к запросу операцию Contains. Таким образом получается аналог операции IN в SQL. Это позволило значительно расширить применимость Azure Table Storage в нашем проекте.

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

ConstantExpression ValueConst = Expression.Constant(ValueToCompare, typeof(string));

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

Описаное в статье решение входит в Open Source библиотеку CPlase, которая готовится к публикации.

Читайте в серии:

Также приглашаю опробовать наш сервис
AtContent™

Автор: VadNov

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js