"Machen Sie uns Filter wie in Excel" ist eine ziemlich beliebte Entwicklungsanforderung. Leider ist die allgemeine Abfrageimplementierung "etwas" länger als ihre lakonische Aussage. Wenn Sie diese Filter noch nie verwendet haben, finden Sie hier ein Beispiel . Das Hauptmerkmal ist, dass Dropdown-Listen mit Werten aus dem ausgewählten Bereich in der Zeile mit den Spaltennamen angezeigt werden. Zum Beispiel in den Spalten A und B - 4000 Zeilen und 3999 Werte (die erste Zeile wird von den Spaltennamen belegt). Somit enthalten die entsprechenden Dropdown-Listen 3999 Werte. Spalte C enthält 220 Zeilen bzw. 219 Werte in der Dropdown-Liste.

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;
}
Alles, was bleibt, ist, den Code zu bereinigen, Doppelarbeit zu beseitigen und eine bessere API bereitzustellen. Das Builder- Entwurfsmuster funktioniert hierfür gut . Ich werde die Implementierungsdetails weglassen. Ein neugieriger Leser wird sicherlich in der Lage sein, eine ähnliche API selbst zu entwerfen.
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)
}