ASP.NETCoreで「Excelのように」フィルターを作成する

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






All Articles