「Excelのようにフィルターを作成する」は、かなり人気のある開発リクエストです。残念ながら、一般的なクエリの実装は、その簡潔なステートメントよりも「わずかに」長くなります。これらのフィルターを使用したことがない場合は、次に例を示します。主な機能は、選択した範囲の値を含むドロップダウンリストが列名の行に表示されることです。たとえば、列Aと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)
}