Русский
Русский
English
Статистика
Реклама

Делаем фильтры как в экселе на ASP.NET Core

Сделайте нам фильтры как в экселе, довольно популярный запрос на разработку. К сожалению, реализация запроса "слегка" длинее, чем его лаконичная постановка. Если вдруг вы никогда не пользовались этими фильтрами, то вот пример. Основная фишка в том, что в строчке с названиям колонок появляются выпадающие списки со значениями из выбранного диапазона. Например в колонках А и B 4000 строк и 3999 значений (первую строчку занимают названия колонок). Таким образом, в соответсвтующих выпадающих списках будет по 3999 значений. В колонке C 220 строк и 219 значений в выпадающем списке соответственно.



ToDropdownOption


В .NET испокон веков существует прекрасный интерфейс IQuerable<T>, предоставляющий доступ к разнообразным источникам данных. Его и будем использовать. Определим метод-расширения ToDropdownOption поверх интерфейса.


public static IQueryable<DropdownOption<TValue>> ToDropdownOption<TQueryable, TValue, TDropdownOption>(   this IQueryable<TQueryable> q,   Expression<Func<TQueryable, string>> labelExpression,   Expression<Func<TQueryable, TValue>> valueExpression)   where TDropdownOption: DropdownOption<TValue>{   // Вызываем конструктор по умолчанию    // В Cache<TValue, TDropdownOption>.Constructor кешируется reflection   var newExpression = Expression.New(Cache<TValue, TDropdownOption>.Constructor);   // Подробнее об этой особой уличной магии здесь   // http://personeltest.ru/aways/habr.com/ru/company/jugru/blog/423891/#predicate-builder   var e2Rebind = Rebind(valueExpression, labelExpression);   var e1ExpressionBind = Expression.Bind(       Cache<TValue, TDropdownOption>.LabelPropertyInfo, labelExpression.Body);   var e2ExpressionBind = Expression.Bind(       Cache<TValue, TDropdownOption>.ValuePropertyInfo, e2Rebind.Body);   // Инициализируем значения Label и Value   var result = Expression.MemberInit(       newExpression, e1ExpressionBind, e2ExpressionBind);   var lambda = Expression.Lambda<Func<TQueryable, DropdownOption<TValue>>>(       result, labelExpression.Parameters);   /*   В итоге получим   return q.Select(x => new DropdownOption<TValue>   {     Label = labelExpression     Value = valueExpression   });   Но такой код не скомплируется,   поэтому пришлось написть с помощью API Expression Trees   */   return q.Select(lambda);}

Если код метода кажется непонятным, прочитайте расшифровку или посмотрите доклад Деревья выражений в enterprise-разработке. Станет гораздо понятнее.

Сами классы DropdownOption и DropdownOption<T> вылгядят следующим образом.


public class DropdownOption{   // Запрещаем программно создавать нетипизированные DropdownOption   // за пределами сборки   internal DropdownOption() {}   internal DropdownOption(string label, object value)   {       Value = value ?? throw new ArgumentNullException(nameof(value));       Label = label ?? throw new ArgumentNullException(nameof(label));   }   // Делаем свойства неизменяемыми за пределеами сборки   public string Label { get; internal set; }   public object Value { get; internal set; }}public class DropdownOption<T>: DropdownOption{    internal DropdownOption() {}    // Типизированные опции создавать за пределами сборки    public DropdownOption(string label, T value) : base(label, value)    {        _value = value;    }    private T _value;    // Перекрываем базовое свойство типизированным    public new virtual T Value    {        get => _value;       internal set       {           _value = value;           base.Value = value;       }    }}

Трюк с internal-конструктором позволяет привести любой DropdownOption<T> к DropdownOption без generic-параметра, одновременно, не позволяя создавать экземпляры класса без generic-параметра за пределами сборки.


Будет здорово когда/если ковариантные возвращаемые типы будут реализованы. С ними можно избавиться от перекрытия через new. Пока имеем, что имеем.

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


public IEnumerable GetDropdowns(IQueryable<SomeData> q) =>    q.ToDropdownOption(x => x.String, x => x.Id)

IDropdownProvider


Где вызывать этот метод расширения? Допустим, мы работаем с таким контроллером:


public IActionResult GetData(    [FromServices] IQueryable<SomeData> q    [FromQuery] SomeDataFilter filter) =>    Ok(q    .Filter(filter)    .ToList());

Классы SomeData и SomeDataFilter определены следующим образом:


public class SomeDataFilter{   public int[] Number { get; set; }   public DateTime[]? Date { get; set; }   public string[]? String { get; set; }}public class SomeData{   public int Number { get; set; }   public DateTime Date { get; set; }   public string String { get; set; }}

А метод Filter следующим образом:


public static IQueryable<SomeData> Filter(    this IQueryable<SomeData> q,    SomeDataFilter filter){    if (filter.Number != null)    {        q = q.Where(x => filter.Number.Contains(x.Number));    }    if (filter.Date != null)    {        q = q.Where(x => filter.Date.Contains(x.Date));    }    if (filter.String != null)    {        q = q.Where(x => filter.String.Contains(x.String));    }    return q;}

Для реальных проектов, этот метод можно сделать обобщенным Как именно описано здесь

SomeDataFilter содержит массивы значений из выпадающих списков, заполненных пользователем, а значит мы где-то в другом месте передали их на фронтенд, используя метод вроде этого:


public IActionResult GetSomeDataFilterDropdownOptions(   [FromServices] IQueryable<SomeData> q){   var number = q       .ToDropdownOption(x => x.Number.ToString(), x => x.Number)       .Distinct()       .ToList();   var date = q       .ToDropdownOption(x => x.Date.ToString("d"), x => x.Date)       .Distinct()       .ToList();   var @string = q       .ToDropdownOption(x => x.String, x => x.String)       .Distinct()       .ToList();   return Ok(new   {       number,       date,       @string   });}

Такой код может понадобится для любого типа фильтров, а не только SomeDataFilters, поэтому введем соответствующий интерфейс.


public interface IDropdownProvider<T>{  Dictionary<string, IEnumerable<DropdownOption>> GetDropdownOptions();}

И перенесем код получения опций в класс, реализующий интерфейс:


public class SomeDataFiltersDropdownProvider: IDropdownProvider<SomeDataFilter>{   private readonly IQueryable<SomeData> _q;   public SomeDataFiltersDropdownProvider(IQueryable<SomeData> q)   {       _q = q;   }   public Dictionary<string, IEnumerable<DropdownOption>> GetDropdownOptions()   {       return new Dictionary<string, IEnumerable<DropdownOption>>()       {           {               "name", _q               .ToDropdownOption(x => x.Number.ToString(), x => x.Number)               .Distinct()               .ToList();           },           {               "date", _q               .ToDropdownOption(x => x.Date.ToString("d"), x => x.Date)               .Distinct()               .ToList();                      },           {               "string", _q               .ToDropdownOption(x => x.String, x => x.String)               .Distinct()               .ToList();           }       };   }}

Осталось написать вот такой обобщенный метод контроллера, который будет по названию типа искать соответствующий DropdownProvider и вызывать его метод.


[HttpGet][Route("Dropdowns/{type}")]public async IActionResult Dropdowns(     string type,      [FromServices] IServiceProvider serviceProvider     [TypeResolver] ITypeResolver typeResolver){   var t = typeResolver(type);   if (t == null)   {       return NotFound();   }   // Преобразование к dynamic, чтобы не париться с приведением типов.   // T неизвестен, потому что метод контроллера не содержит дженерика.   dynamic service = serviceProvider       .GetService(typeof(IDropdownProvider<>)       .MakeGenericType(t));   if (service == null)   {       return NotFound();   }   var res = service.GetDropdownOptions();   return Ok(res);}

Одновременные запросы


На этом можно было бы и закончить, но, как говорится, есть нюанс. В примере сверху запросы к БД выполняются последовательно, хотя они не зависят друг от друга. Чем больше колонок с фильтрами, тем больший выигрыш можно получить за счет параллельного выполнения запросов. Реализации IQueryable чаще всего базируются на той или иной ORM, а реализации Unit Of Work ORM часто не потокобезопасны (иначе слишком сложно было бы реализовать change tracking). Поэтому будем использовать отдельные области видимости (scope) ServiceProvider и асинхронные версии методов.


public static async Task<TResult> InScopeAsync<TService, TResult>(    this IServiceProvider serviceProvider,    Func<TService, IServiceProvider, Task<TResult>> func){    using var scope = serviceProvider.CreateScope();     return await func(        scope.ServiceProvider.GetService<TService>(),        scope.ServiceProvider);}

В итоге код DropdownProvider можно переписать в следующем виде:


public async Task<Dictionary<string, IEnumerable<DropdownOption>>>   GetDropdownOptionsAsync(){    var dict = new Dictionary<string, IEnumerable<DropdownOption>>();    var name = sp.InScopeAsync<IQueryable<SomeData>>(q => q        .ToDropdownOption(x => x.Number.ToString(), x => x.Number)        .Distinct()        .ToListAsync());    var date = sp.InScopeAsync<IQueryable<SomeData>>(q => q        .ToDropdownOption(x => x.Date.ToString("d"), x => x.Date)        .Distinct()        .ToListAsync());       var @string = sp.InScopeAsync<IQueryable<SomeData>>(q => q        .ToDropdownOption(x => x.String, x => x.String)        .Distinct()        .ToListAsync());    // Теперь все запросы выполняются параллельно    await Task.WhenAll(new []{name, date, @string}});    dict["name"] = await name;    dict["date"] = await date;    dict["string"] = await @string;    return dict;}

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


public async Task<Dictionary<string, IEnumerable<DropdownOption>>>    GetDropdownOptionsAsync(){     return sp        .DropdownsFor<SomeDataFilters>        .With(x => x.Number)        .As<SomeData, int>(GetNumbers)        .With(x => x.Date)        .As<SomeData, DateTime>(GetDates)        .With(x => x.String)        .As<SomeData, string>(GetStrings)}
Источник: habr.com
К списку статей
Опубликовано: 18.02.2021 00:06:58
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Net

C

Excel

Serviceprovider

Epression trees

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru