"Jadikan kami filter seperti di Excel" adalah permintaan pengembangan yang cukup populer. Sayangnya, implementasi kueri umum "sedikit" lebih lama daripada pernyataan singkatnya. Jika Anda belum pernah menggunakan filter ini, berikut contohnya . Fitur utamanya adalah daftar drop-down dengan nilai dari rentang yang dipilih muncul di baris dengan nama kolom. Misalnya, di kolom A dan B - 4000 baris dan nilai 3999 (baris pertama ditempati oleh nama kolom). Jadi, daftar drop-down yang sesuai akan berisi 3999 nilai. Kolom C masing-masing memiliki 220 baris dan 219 nilai dalam daftar drop-down.

ToDropdownOption
.NET memiliki antarmuka hebat selama berabad-abad IQuerable<T>
yang menyediakan akses ke berbagai sumber data. Kami akan menggunakannya. Mari tentukan metode ekstensi ToDropdownOption
di atas antarmuka.
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);
}
Jika kode metode tampaknya tidak dapat dipahami, baca transkripnya atau lihat laporan Pohon Ekspresi dalam Pengembangan Perusahaan . Ini akan menjadi lebih jelas.
Kelas-kelas itu sendiri DropdownOption
dan DropdownOption<T>
vylgyadyat mengikuti.
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;
}
}
}
Trik konstruktor internal memungkinkan Anda untuk mentransmisikan apa pun DropdownOption<T>
ke DropdownOption
tanpa parameter generik, sementara pada saat yang sama mencegah pembuatan instance kelas tanpa parameter generik di luar assembly.
/ .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;
}
Yang tersisa hanyalah membersihkan kode, menghilangkan duplikasi, dan menyediakan API yang lebih baik. Pola desain pembangun bekerja dengan baik untuk ini . Saya akan menghilangkan detail implementasi. Seorang pembaca yang ingin tahu pasti akan dapat merancang API serupa sendiri.
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)
}