[.NET, C#] Делаем фильтры «как в экселе» на ASP.NET Core
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
«Сделайте нам фильтры «как в экселе», — довольно популярный запрос на разработку. К сожалению, реализация запроса "слегка" длинее, чем его лаконичная постановка. Если вдруг вы никогда не пользовались этими фильтрами, то вот пример. Основная фишка в том, что в строчке с названиям колонок появляются выпадающие списки со значениями из выбранного диапазона. Например в колонках А и 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);
// Подробнее об этой особой уличной магии здесь
// https://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
===========
Похожие новости:
- [Информационная безопасность, Программирование, .NET, C#, Разработка под Windows] Как следить (наблюдать) за компьютером. Часть 1 — делаем скриншоты пользователей
- [Программирование, .NET, Разработка под MacOS, Разработка под Windows] От WPF к Авалонии
- [Программирование, .NET, ASP, C#] Что из себя представляет класс Startup и Program.cs в ASP.NET Core (перевод)
- [Проектирование и рефакторинг, C#, Разработка под Windows] Проектирование на C# глазами первокурсника -> NotePad++ №6
- [Анализ и проектирование систем, .NET, Проектирование и рефакторинг, Микросервисы] Взаимодействия. RPC vs REST vs MQ
- [C#] По пути в Авалонию
- [.NET] .NET 5 + Source Generator = Javascript
- [Программирование, Kotlin, Старое железо] Пиксели, Excel, Kotlin и немного ностальгии…
- [.NET, CRM-системы, Microsoft Azure, DevOps] CI/CD для Dynamics CRM на базе Azure DevOps
- [DIY или Сделай сам] Математика, красота, любовь — история одной валентинки
Теги для поиска: #_.net, #_c#, #_excel, #_serviceprovider, #_epression_trees, #_.net, #_c#
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:17
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
«Сделайте нам фильтры «как в экселе», — довольно популярный запрос на разработку. К сожалению, реализация запроса "слегка" длинее, чем его лаконичная постановка. Если вдруг вы никогда не пользовались этими фильтрами, то вот пример. Основная фишка в том, что в строчке с названиям колонок появляются выпадающие списки со значениями из выбранного диапазона. Например в колонках А и 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); // Подробнее об этой особой уличной магии здесь // https://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-разработке. Станет гораздо понятнее.
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. Пока имеем, что имеем.
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; } Для реальных проектов, этот метод можно сделать обобщенным Как именно описано здесь
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 =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 15:17
Часовой пояс: UTC + 5