< Summary

Information
Class: OzonOrdersWeb.WebServices.Grid.ServerGridQuery
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Grid/ServerGridQuery.cs
Line coverage
71%
Covered lines: 154
Uncovered lines: 60
Coverable lines: 214
Total lines: 476
Line coverage: 71.9%
Branch coverage
61%
Covered branches: 174
Total branches: 284
Branch coverage: 61.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Validate(...)62.5%464084.61%
ApplyFilters(...)91.17%3434100%
ApplySort(...)83.33%1818100%
PageAsync()0%620%
FacetsAsync()0%2040%
FacetsTypedAsync()0%4260%
FacetKey(...)0%156120%
.cctor()100%11100%
StripSeparators(...)100%11100%
ArticleKey(...)100%22100%
StripSeparatorsExpression(...)100%22100%
BuildPredicate(...)81.25%1616100%
BuildText(...)84.61%262695.83%
Guarded()100%22100%
BuildNumber(...)57.69%382673.68%
BuildDate(...)50%292071.42%
BuildSet(...)61.53%282685%
ConvertSetValue(...)37.5%921633.33%
Const(...)100%88100%
TryParseDate(...)50%22100%
ReadString(...)33.33%7675%
ReadDecimal(...)25%301250%
Rebind(...)100%11100%
InvokeOrder(...)100%11100%
.ctor(...)100%11100%
VisitParameter(...)50%22100%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/WebServices/Grid/ServerGridQuery.cs

#LineLine coverage
 1using System.Globalization;
 2using System.Linq.Expressions;
 3using System.Reflection;
 4using System.Text.Json;
 5using Microsoft.EntityFrameworkCore;
 6using OzonOrdersWeb.WebServices.Orders.Grid;
 7
 8namespace OzonOrdersWeb.WebServices.Grid;
 9
 10/// <summary>
 11/// Запрос блока строк серверного грида (AG Grid Infinite Row Model) для страниц на EF-запросах:
 12/// диапазон, сортировки, модель фильтров AG Grid, быстрый поиск, параметры страницы (вкладка, фильтр верхнего уровня).
 13/// </summary>
 14public sealed class ServerGridRequest
 15{
 16    public int StartRow { get; set; }
 17    public int EndRow { get; set; } = 100;
 18    public List<AgGridSortModelItem> SortModel { get; set; } = [];
 19    public Dictionary<string, AgGridFilterModelItem> FilterModel { get; set; } = new(StringComparer.Ordinal);
 20    public string? Search { get; set; }
 21    /// <summary>Параметры страницы вне колонок (например, orderNumber для поиска заказа).</summary>
 22    public Dictionary<string, string?> Params { get; set; } = new(StringComparer.OrdinalIgnoreCase);
 23    /// <summary>Для счётчиков значений: колонка.</summary>
 24    public string? ColId { get; set; }
 25}
 26
 27public enum ServerGridColumnType
 28{
 29    Text,
 30    Number,
 31    Date,
 32    Bool
 33}
 34
 35/// <summary>Колонка whitelist: colId → выражение над сущностью (транслируется EF в SQL).</summary>
 36public sealed class ServerGridColumn
 37{
 38    public required LambdaExpression Selector { get; init; }
 39    public required ServerGridColumnType Type { get; init; }
 40    public bool Sortable { get; init; } = true;
 41    public bool Filterable { get; init; } = true;
 42
 43    /// <summary>
 44    /// Текстовый фильтр колонки сравнивает значение и строку фильтра без разделителей (артикулы):
 45    /// «1020479SX» находит «10-20479-SX». Пустые/непустые проверяются по исходному значению.
 46    /// </summary>
 47    public bool NormalizeText { get; init; }
 48    public Type ValueType => Selector.ReturnType;
 49}
 50
 51/// <summary>
 52/// Описание серверного грида: whitelist колонок, поля быстрого поиска и порядок по умолчанию.
 53/// Колонки вне whitelist в запросе игнорируются — значения пользователя уходят только константами выражений (параметры 
 54/// </summary>
 55public sealed class ServerGridDefinition<T>
 56{
 57    public Dictionary<string, ServerGridColumn> Columns { get; } = new(StringComparer.Ordinal);
 58    public List<Expression<Func<T, string?>>> SearchFields { get; } = new();
 59
 60    /// <summary>
 61    /// Поля, которые быстрый поиск сравнивает без разделителей: «1020479SX» находит «10-20479-SX» и «1020479_SX».
 62    /// Разделители вырезаются и в значении поля (REPLACE в SQL), и в строке поиска.
 63    /// </summary>
 64    public List<Expression<Func<T, string?>>> NormalizedSearchFields { get; } = new();
 65    public required Func<IQueryable<T>, IOrderedQueryable<T>> DefaultOrder { get; init; }
 66    /// <summary>Добор порядка после пользовательской сортировки (стабильная пагинация), обычно Id.</summary>
 67    public required Expression<Func<T, long>> TieBreaker { get; init; }
 68
 69    public ServerGridDefinition<T> Col<TValue>(string colId, Expression<Func<T, TValue>> selector,
 70        ServerGridColumnType? type = null, bool sortable = true, bool filterable = true, bool normalizeText = false)
 71    {
 72        Columns[colId] = new ServerGridColumn
 73        {
 74            Selector = selector,
 75            Type = type ?? InferType(typeof(TValue)),
 76            Sortable = sortable,
 77            Filterable = filterable,
 78            NormalizeText = normalizeText
 79        };
 80        return this;
 81    }
 82
 83    public ServerGridDefinition<T> Search(params Expression<Func<T, string?>>[] fields)
 84    {
 85        SearchFields.AddRange(fields);
 86        return this;
 87    }
 88
 89    public ServerGridDefinition<T> SearchNormalized(params Expression<Func<T, string?>>[] fields)
 90    {
 91        NormalizedSearchFields.AddRange(fields);
 92        return this;
 93    }
 94
 95    private static ServerGridColumnType InferType(Type type)
 96    {
 97        var t = Nullable.GetUnderlyingType(type) ?? type;
 98        if (t == typeof(string) || t.IsEnum) return ServerGridColumnType.Text;
 99        if (t == typeof(bool)) return ServerGridColumnType.Bool;
 100        if (t == typeof(DateTime) || t == typeof(DateTimeOffset)) return ServerGridColumnType.Date;
 101        return ServerGridColumnType.Number;
 102    }
 103}
 104
 105public sealed class ServerGridFacet
 106{
 107    public string Value { get; set; } = string.Empty;
 108    public int Count { get; set; }
 109}
 110
 111/// <summary>AG Grid → IQueryable: фильтры, сортировка, поиск, блок, счётчики значений.</summary>
 112public static class ServerGridQuery
 113{
 114    public const int MaxBlockSize = 500;
 115    public const int MaxConditions = 40;
 116    public const int MaxSorts = 5;
 117    public const int MaxSetValues = 500;
 118    public const int MaxTextLength = 300;
 119    private const int MaxFacetValues = 500;
 120
 121    public static string? Validate(ServerGridRequest? request)
 122    {
 5123        if (request is null) return "Пустой запрос.";
 5124        if (request.StartRow < 0 || request.EndRow <= request.StartRow) return "Некорректный диапазон строк.";
 6125        if (request.EndRow - request.StartRow > MaxBlockSize) return $"Слишком большой блок (максимум {MaxBlockSize}).";
 4126        if (request.FilterModel is { Count: > MaxConditions }) return "Слишком много фильтров.";
 4127        if (request.SortModel is { Count: > MaxSorts }) return "Слишком много столбцов сортировки.";
 4128        if (request.Search is { Length: > MaxTextLength }) return "Слишком длинная строка поиска.";
 14129        foreach (var (colId, item) in request.FilterModel ?? new Dictionary<string, AgGridFilterModelItem>())
 130        {
 3131            if (colId.Length > 64) return "Некорректный идентификатор колонки.";
 3132            if (item?.Values is { Count: > MaxSetValues }) return $"Слишком много значений в фильтре «{colId}».";
 3133            if (item?.Filter is { ValueKind: JsonValueKind.String } f && f.GetString()!.Length > MaxTextLength)
 0134                return $"Слишком длинное значение фильтра «{colId}».";
 135        }
 136
 4137        return null;
 0138    }
 139
 140    public static IQueryable<T> ApplyFilters<T>(IQueryable<T> query, ServerGridDefinition<T> def, ServerGridRequest requ
 141    {
 49142        var search = request.Search?.Trim();
 49143        if (!string.IsNullOrEmpty(search) && (def.SearchFields.Count > 0 || def.NormalizedSearchFields.Count > 0))
 144        {
 15145            var p = Expression.Parameter(typeof(T), "x");
 15146            Expression? any = null;
 144147            foreach (var field in def.SearchFields)
 148            {
 57149                var body = Rebind(field, p);
 57150                var cond = Expression.AndAlso(Expression.NotEqual(body, Expression.Constant(null, typeof(string))),
 57151                    Expression.Call(body, StringContains, Expression.Constant(search)));
 57152                any = any == null ? cond : Expression.OrElse(any, cond);
 153            }
 154
 15155            var stripped = StripSeparators(search);
 15156            if (stripped.Length > 0)
 157            {
 48158                foreach (var field in def.NormalizedSearchFields)
 159                {
 10160                    var body = Rebind(field, p);
 10161                    var cond = Expression.AndAlso(Expression.NotEqual(body, Expression.Constant(null, typeof(string))),
 10162                        Expression.Call(StripSeparatorsExpression(body), StringContains, Expression.Constant(stripped)))
 10163                    any = any == null ? cond : Expression.OrElse(any, cond);
 164                }
 165            }
 166
 15167            if (any != null)
 15168                query = query.Where(Expression.Lambda<Func<T, bool>>(any, p));
 169        }
 170
 162171        foreach (var (colId, item) in request.FilterModel ?? new Dictionary<string, AgGridFilterModelItem>())
 172        {
 32173            if (colId == excludeColId || item is null) continue;
 32174            if (!def.Columns.TryGetValue(colId, out var col) || !col.Filterable) continue;
 29175            var predicate = BuildPredicate<T>(col, item);
 57176            if (predicate != null) query = query.Where(predicate);
 177        }
 178
 49179        return query;
 180    }
 181
 182    public static IQueryable<T> ApplySort<T>(IQueryable<T> query, ServerGridDefinition<T> def, IEnumerable<AgGridSortMod
 183    {
 11184        IOrderedQueryable<T>? ordered = null;
 32185        foreach (var sort in (sortModel ?? Enumerable.Empty<AgGridSortModelItem>()).Take(MaxSorts))
 186        {
 5187            if (!def.Columns.TryGetValue(sort.ColId, out var col) || !col.Sortable) continue;
 4188            var desc = string.Equals(sort.Sort, "desc", StringComparison.OrdinalIgnoreCase);
 4189            var method = ordered == null
 4190                ? (desc ? "OrderByDescending" : "OrderBy")
 4191                : (desc ? "ThenByDescending" : "ThenBy");
 4192            ordered = (IOrderedQueryable<T>)InvokeOrder(ordered ?? query, method, col.Selector);
 193        }
 194
 11195        return ordered == null ? def.DefaultOrder(query) : ordered.ThenByDescending(def.TieBreaker);
 196    }
 197
 198    public static async Task<(List<T> Items, int Total)> PageAsync<T>(IQueryable<T> query, ServerGridDefinition<T> def,
 199        ServerGridRequest request, CancellationToken cancellationToken)
 200    {
 0201        var filtered = ApplyFilters(query, def, request);
 0202        var total = await filtered.CountAsync(cancellationToken);
 0203        var start = Math.Max(0, request.StartRow);
 0204        var take = Math.Clamp(request.EndRow - start, 1, MaxBlockSize);
 0205        if (start >= total) return (new List<T>(), total);
 0206        var items = await ApplySort(filtered, def, request.SortModel).Skip(start).Take(take).ToListAsync(cancellationTok
 0207        return (items, total);
 0208    }
 209
 210    /// <summary>Значения колонки с количеством строк при остальных фильтрах (GROUP BY на сервере).</summary>
 211    public static async Task<List<ServerGridFacet>> FacetsAsync<T>(IQueryable<T> query, ServerGridDefinition<T> def,
 212        ServerGridRequest request, CancellationToken cancellationToken)
 213    {
 0214        if (string.IsNullOrWhiteSpace(request.ColId) || !def.Columns.TryGetValue(request.ColId, out var col)) return new
 0215        var filtered = ApplyFilters(query, def, request, excludeColId: request.ColId);
 0216        var method = typeof(ServerGridQuery).GetMethod(nameof(FacetsTypedAsync), BindingFlags.NonPublic | BindingFlags.S
 0217            .MakeGenericMethod(typeof(T), col.ValueType);
 0218        var task = (Task<List<ServerGridFacet>>)method.Invoke(null, new object[] { filtered, col.Selector, cancellationT
 0219        return await task;
 0220    }
 221
 222    private static async Task<List<ServerGridFacet>> FacetsTypedAsync<T, TKey>(IQueryable<T> query, LambdaExpression sel
 223    {
 0224        var typed = (Expression<Func<T, TKey>>)selector;
 0225        var rows = await query.GroupBy(typed)
 0226            .Select(g => new { g.Key, Count = g.Count() })
 0227            .OrderByDescending(g => g.Count)
 0228            .Take(MaxFacetValues)
 0229            .ToListAsync(ct);
 230        // NULL и "" у строк — одно значение «(пусто)» (так же их отбирает set-фильтр): счётчики складываются.
 0231        return rows
 0232            .GroupBy(r => FacetKey(r.Key))
 0233            .Select(g => new ServerGridFacet { Value = g.Key, Count = g.Sum(r => r.Count) })
 0234            .OrderByDescending(f => f.Count)
 0235            .ToList();
 0236    }
 237
 0238    private static string FacetKey(object? key) => key switch
 0239    {
 0240        null => string.Empty,
 0241        bool b => b ? "true" : "false",
 0242        DateTime d => d.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture),
 0243        IFormattable f => f.ToString(null, CultureInfo.InvariantCulture),
 0244        _ => key.ToString() ?? string.Empty
 0245    };
 246
 247    // ---------- Предикаты ----------
 248
 249    /// <summary>Разделители артикулов, которые вырезаются при нормализованном поиске.</summary>
 1250    private static readonly string[] Separators = { "-", "_", " ", "/", ".", "\\", ",", "(", ")", "+", "#", "\u00A0" };
 251
 1252    private static readonly MethodInfo StringReplace = typeof(string).GetMethod(nameof(string.Replace), new[] { typeof(s
 253
 254    /// <summary>Строка поиска без разделителей: остаются только буквы и цифры.</summary>
 79255    public static string StripSeparators(string value) => new(value.Where(char.IsLetterOrDigit).ToArray());
 256
 257    /// <summary>Ключ артикула для сравнения: без разделителей, в верхнем регистре.</summary>
 58258    public static string ArticleKey(string? value) => StripSeparators(value ?? string.Empty).ToUpperInvariant();
 259
 260    public static Expression StripSeparatorsExpression(Expression value)
 261    {
 520262        foreach (var separator in Separators)
 240263            value = Expression.Call(value, StringReplace, Expression.Constant(separator), Expression.Constant(string.Emp
 20264        return value;
 265    }
 266
 1267    private static readonly MethodInfo StringContains = typeof(string).GetMethod(nameof(string.Contains), new[] { typeof
 1268    private static readonly MethodInfo StringStartsWith = typeof(string).GetMethod(nameof(string.StartsWith), new[] { ty
 1269    private static readonly MethodInfo StringEndsWith = typeof(string).GetMethod(nameof(string.EndsWith), new[] { typeof
 270
 271    private static Expression<Func<T, bool>>? BuildPredicate<T>(ServerGridColumn col, AgGridFilterModelItem item)
 272    {
 29273        var p = Expression.Parameter(typeof(T), "x");
 29274        var value = Rebind(col.Selector, p);
 29275        var body = item.FilterType switch
 29276        {
 5277            "set" => BuildSet(value, item.Values),
 6278            "number" => col.Type == ServerGridColumnType.Number ? BuildNumber(value, item) : null,
 5279            "date" => col.Type == ServerGridColumnType.Date ? BuildDate(value, item) : null,
 13280            _ => col.Type == ServerGridColumnType.Text && value.Type == typeof(string) ? BuildText(value, item, col.Norm
 29281        };
 29282        return body == null ? null : Expression.Lambda<Func<T, bool>>(body, p);
 283    }
 284
 285    private static Expression? BuildText(Expression value, AgGridFilterModelItem item, bool normalize = false)
 286    {
 13287        var nul = Expression.Constant(null, typeof(string));
 13288        var empty = Expression.Constant(string.Empty);
 13289        switch (item.Type)
 290        {
 2291            case "blank": return Expression.OrElse(Expression.Equal(value, nul), Expression.Equal(value, empty));
 0292            case "notBlank": return Expression.AndAlso(Expression.NotEqual(value, nul), Expression.NotEqual(value, empty
 293        }
 294
 11295        var text = ReadString(item.Filter)?.Trim();
 11296        if (string.IsNullOrEmpty(text)) return null;
 11297        if (text.Length > MaxTextLength) text = text[..MaxTextLength];
 298        // Нормализованная колонка: значение и строка фильтра сравниваются без разделителей; проверки на null идут
 299        // по исходному значению (REPLACE от NULL в БД даёт NULL, а в LINQ to Objects упал бы).
 11300        var original = value;
 11301        if (normalize && StripSeparators(text) is { Length: > 0 } stripped)
 302        {
 6303            value = StripSeparatorsExpression(value);
 6304            text = stripped;
 305        }
 306        else
 307        {
 5308            normalize = false;
 309        }
 310
 11311        var c = Expression.Constant(text);
 11312        var notNull = Expression.NotEqual(original, nul);
 2313        Expression Guarded(Expression comparison) => normalize ? Expression.AndAlso(notNull, comparison) : comparison;
 11314        return item.Type switch
 11315        {
 2316            "equals" => Guarded(Expression.Equal(value, c)),
 1317            "notEqual" => Expression.OrElse(Expression.Equal(original, nul), Expression.NotEqual(value, c)),
 1318            "startsWith" => Expression.AndAlso(notNull, Expression.Call(value, StringStartsWith, c)),
 1319            "endsWith" => Expression.AndAlso(notNull, Expression.Call(value, StringEndsWith, c)),
 1320            "notContains" => Expression.OrElse(Expression.Equal(original, nul), Expression.Not(Expression.Call(value, St
 5321            _ => Expression.AndAlso(notNull, Expression.Call(value, StringContains, c))
 11322        };
 323    }
 324
 325    private static Expression? BuildNumber(Expression value, AgGridFilterModelItem item)
 326    {
 5327        var nullable = !value.Type.IsValueType || Nullable.GetUnderlyingType(value.Type) != null;
 5328        switch (item.Type)
 329        {
 1330            case "blank": return nullable ? Expression.Equal(value, Expression.Constant(null, value.Type)) : Expression.
 0331            case "notBlank": return nullable ? Expression.NotEqual(value, Expression.Constant(null, value.Type)) : Expre
 332        }
 333
 4334        var from = ReadDecimal(item.Filter);
 4335        if (from == null) return null;
 4336        var a = Const(from.Value, value.Type);
 4337        return item.Type switch
 4338        {
 0339            "notEqual" => Expression.NotEqual(value, a),
 1340            "greaterThan" => Expression.GreaterThan(value, a),
 2341            "greaterThanOrEqual" => Expression.GreaterThanOrEqual(value, a),
 0342            "lessThan" => Expression.LessThan(value, a),
 0343            "lessThanOrEqual" => Expression.LessThanOrEqual(value, a),
 1344            "inRange" => ReadDecimal(item.FilterTo) is { } to
 1345                ? Expression.AndAlso(Expression.GreaterThanOrEqual(value, a), Expression.LessThanOrEqual(value, Const(to
 1346                : Expression.GreaterThanOrEqual(value, a),
 0347            _ => Expression.Equal(value, a)
 4348        };
 349    }
 350
 351    private static Expression? BuildDate(Expression value, AgGridFilterModelItem item)
 352    {
 5353        var nullable = Nullable.GetUnderlyingType(value.Type) != null;
 5354        switch (item.Type)
 355        {
 0356            case "blank": return nullable ? Expression.Equal(value, Expression.Constant(null, value.Type)) : Expression.
 0357            case "notBlank": return nullable ? Expression.NotEqual(value, Expression.Constant(null, value.Type)) : Expre
 358        }
 359
 5360        if (!TryParseDate(item.DateFrom, out var day)) return null;
 5361        var start = Const(day.Date, value.Type);
 5362        var nextDay = Const(day.Date.AddDays(1), value.Type);
 5363        switch (item.Type)
 364        {
 1365            case "greaterThan": return Expression.GreaterThanOrEqual(value, nextDay);
 0366            case "lessThan": return Expression.LessThan(value, start);
 0367            case "notEqual": return Expression.OrElse(Expression.LessThan(value, start), Expression.GreaterThanOrEqual(v
 368            case "inRange":
 1369                var end = TryParseDate(item.DateTo, out var to) ? Const(to.Date.AddDays(1), value.Type) : nextDay;
 1370                return Expression.AndAlso(Expression.GreaterThanOrEqual(value, start), Expression.LessThan(value, end));
 371            default:
 3372                return Expression.AndAlso(Expression.GreaterThanOrEqual(value, start), Expression.LessThan(value, nextDa
 373        }
 374    }
 375
 376    private static Expression BuildSet(Expression value, List<string>? rawValues)
 377    {
 5378        var values = (rawValues ?? new List<string>()).Distinct(StringComparer.Ordinal).Take(MaxSetValues).ToList();
 6379        if (values.Count == 0) return Expression.Constant(false);
 4380        var underlying = Nullable.GetUnderlyingType(value.Type) ?? value.Type;
 4381        var hasBlank = values.Any(string.IsNullOrEmpty);
 9382        var nonBlank = values.Where(v => !string.IsNullOrEmpty(v)).ToList();
 4383        Expression? result = null;
 384
 4385        if (nonBlank.Count > 0)
 386        {
 4387            var listType = typeof(List<>).MakeGenericType(value.Type);
 4388            var list = (System.Collections.IList)Activator.CreateInstance(listType)!;
 18389            foreach (var v in nonBlank)
 390            {
 5391                var converted = ConvertSetValue(v, underlying);
 10392                if (converted != null) list.Add(converted);
 393            }
 394
 4395            if (list.Count > 0)
 396            {
 4397                var contains = listType.GetMethod(nameof(List<object>.Contains))!;
 4398                result = Expression.Call(Expression.Constant(list), contains, value);
 399            }
 400        }
 401
 4402        if (hasBlank && (!value.Type.IsValueType || Nullable.GetUnderlyingType(value.Type) != null))
 403        {
 0404            Expression blank = Expression.Equal(value, Expression.Constant(null, value.Type));
 0405            if (value.Type == typeof(string)) blank = Expression.OrElse(blank, Expression.Equal(value, Expression.Consta
 0406            result = result == null ? blank : Expression.OrElse(result, blank);
 407        }
 408
 4409        return result ?? Expression.Constant(false);
 410    }
 411
 412    private static object? ConvertSetValue(string raw, Type type)
 413    {
 414        try
 415        {
 9416            if (type == typeof(string)) return raw;
 2417            if (type == typeof(bool)) return raw is "true" or "1" or "True";
 0418            if (type.IsEnum) return Enum.Parse(type, raw, ignoreCase: true);
 0419            if (type == typeof(DateTime)) return DateTime.Parse(raw, CultureInfo.InvariantCulture);
 0420            if (type == typeof(DateTimeOffset)) return DateTimeOffset.Parse(raw, CultureInfo.InvariantCulture, DateTimeS
 0421            return Convert.ChangeType(raw, type, CultureInfo.InvariantCulture);
 422        }
 0423        catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException or ArgumentExcepti
 424        {
 0425            return null;
 426        }
 5427    }
 428
 429    private static Expression Const(object value, Type targetType)
 430    {
 16431        var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType;
 16432        var converted = underlying == typeof(DateTimeOffset) && value is DateTime dateTime
 16433            ? new DateTimeOffset(DateTime.SpecifyKind(dateTime, DateTimeKind.Utc))
 16434            : value is DateTime ? value : Convert.ChangeType(value, underlying, CultureInfo.InvariantCulture);
 16435        return Expression.Convert(Expression.Constant(converted, underlying), targetType);
 436    }
 437
 438    private static bool TryParseDate(string? raw, out DateTime value)
 439    {
 6440        value = default;
 6441        if (string.IsNullOrWhiteSpace(raw)) return false;
 6442        return DateTime.TryParse(raw, CultureInfo.InvariantCulture, DateTimeStyles.None, out value);
 443    }
 444
 11445    private static string? ReadString(JsonElement? element) => element is { } e
 11446        ? e.ValueKind switch
 11447        {
 11448            JsonValueKind.String => e.GetString(),
 0449            JsonValueKind.Number => e.GetRawText(),
 0450            _ => null
 11451        }
 11452        : null;
 453
 454    private static decimal? ReadDecimal(JsonElement? element)
 455    {
 5456        if (element is not { } e) return null;
 10457        if (e.ValueKind == JsonValueKind.Number && e.TryGetDecimal(out var d)) return d;
 0458        if (e.ValueKind == JsonValueKind.String && decimal.TryParse(e.GetString()?.Replace(',', '.'), NumberStyles.Numbe
 0459        return null;
 460    }
 461
 462    private static Expression Rebind(LambdaExpression lambda, ParameterExpression p) =>
 96463        new ParameterReplacer(lambda.Parameters[0], p).Visit(lambda.Body);
 464
 465    private static IQueryable InvokeOrder(IQueryable source, string method, LambdaExpression selector)
 466    {
 4467        var call = Expression.Call(typeof(Queryable), method, new[] { source.ElementType, selector.ReturnType },
 4468            source.Expression, Expression.Quote(selector));
 4469        return source.Provider.CreateQuery(call);
 470    }
 471
 96472    private sealed class ParameterReplacer(ParameterExpression from, ParameterExpression to) : ExpressionVisitor
 473    {
 97474        protected override Expression VisitParameter(ParameterExpression node) => node == from ? to : base.VisitParamete
 475    }
 476}

Methods/Properties

Validate(OzonOrdersWeb.WebServices.Grid.ServerGridRequest)
ApplyFilters(System.Linq.IQueryable`1<T>,OzonOrdersWeb.WebServices.Grid.ServerGridDefinition`1<T>,OzonOrdersWeb.WebServices.Grid.ServerGridRequest,System.String)
ApplySort(System.Linq.IQueryable`1<T>,OzonOrdersWeb.WebServices.Grid.ServerGridDefinition`1<T>,System.Collections.Generic.IEnumerable`1<OzonOrdersWeb.WebServices.Orders.Grid.AgGridSortModelItem>)
PageAsync()
FacetsAsync()
FacetsTypedAsync()
FacetKey(System.Object)
.cctor()
StripSeparators(System.String)
ArticleKey(System.String)
StripSeparatorsExpression(System.Linq.Expressions.Expression)
BuildPredicate(OzonOrdersWeb.WebServices.Grid.ServerGridColumn,OzonOrdersWeb.WebServices.Orders.Grid.AgGridFilterModelItem)
BuildText(System.Linq.Expressions.Expression,OzonOrdersWeb.WebServices.Orders.Grid.AgGridFilterModelItem,System.Boolean)
Guarded()
BuildNumber(System.Linq.Expressions.Expression,OzonOrdersWeb.WebServices.Orders.Grid.AgGridFilterModelItem)
BuildDate(System.Linq.Expressions.Expression,OzonOrdersWeb.WebServices.Orders.Grid.AgGridFilterModelItem)
BuildSet(System.Linq.Expressions.Expression,System.Collections.Generic.List`1<System.String>)
ConvertSetValue(System.String,System.Type)
Const(System.Object,System.Type)
TryParseDate(System.String,System.DateTime&)
ReadString(System.Nullable`1<System.Text.Json.JsonElement>)
ReadDecimal(System.Nullable`1<System.Text.Json.JsonElement>)
Rebind(System.Linq.Expressions.LambdaExpression,System.Linq.Expressions.ParameterExpression)
InvokeOrder(System.Linq.IQueryable,System.String,System.Linq.Expressions.LambdaExpression)
.ctor(System.Linq.Expressions.ParameterExpression,System.Linq.Expressions.ParameterExpression)
VisitParameter(System.Linq.Expressions.ParameterExpression)