Hacer filtros "como en Excel" en ASP.NET Core

"Háganos filtros como en Excel" es una solicitud de desarrollo bastante popular. Desafortunadamente, la implementación de la consulta general es "un poco" más larga que su declaración lacónica. Si nunca ha utilizado estos filtros, aquí tiene un ejemplo . La característica principal es que aparecen listas desplegables con valores del rango seleccionado en la línea con los nombres de las columnas. Por ejemplo, en las columnas A y B - 4000 líneas y 3999 valores (la primera línea está ocupada por los nombres de las columnas). Por lo tanto, las listas desplegables correspondientes contendrán 3999 valores. La columna C tiene 220 líneas y 219 valores en la lista desplegable, respectivamente.













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;
}
      
      





Todo lo que queda es limpiar el código, eliminar la duplicación y proporcionar una mejor API. El patrón de diseño del constructor funciona bien para esto . Omitiré los detalles de implementación. Un lector curioso sin duda podrá diseñar una API similar por su cuenta.







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