Erstellen von Filtern "wie in Excel" unter ASP.NET Core

"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)
}
      
      






All Articles