使用多个条件筛选数据

4

我正在尝试使用参数条件名称对数据进行排序。代码可以运行,但读起来很不好。我想知道是否有更好的编写方式。感谢您的帮助。

    public async Task<ActionResult<IEnumerable<Product>>> FilterAsync(string conditionName)
    {
        var values = await GetProducts();

        IOrderedEnumerable<Product> dataSorted =null;
        if (conditionName == "TitleASC")
        {
            dataSorted = values.OrderBy(c => c.Title);
        }

        if (conditionName == "TitleDESC")
        {
            dataSorted = values.OrderByDescending(c => c.CategoriesTitle);
        }

        if (conditionName == "DateTimeASC")
        {
            dataSorted =values.OrderBy(c => c.DateTime);
        }

        if (conditionName == "DateTimeDESC")
        {
            dataSorted = values.OrderByDescending(c => c.DateTime);
        }

        if (conditionName == "CategoryASC")
        {
            dataSorted = values.OrderBy(c => c.CategoriesTitle);
        }
        if (conditionName == "CategoryDESC")
        {
            dataSorted = values.OrderByDescending(c => c.CategoriesTitle);
        }

        return Ok(dataSorted);
    }

你可以用 switch 语句替换它,这样看起来会容易一些 :) - Yeldar Kurmangaliyev
是的,我同意这看起来不太好。也许可以用LINQ语句来实现,或者我想太多了吗? - Great Khan 2016
2
有一个名为 Linq.Dynamic 的 Nuget 对你的解决方案非常有用。你可以从单词中分离 desc 或 asc,然后基于字符串进行查询。在这种情况下,你不需要更新代码,它会动态处理。 - Shervin Ivari
1
你可以使用动态LINQ或表达式,并使用属性本身而不是名称。 - Pavel Anikhouski
1
conditionName == "TitleDESC" 时,您是否打算按 c.CategoriesTitle 排序,而在 conditionName == "TitleASC" 时按 c => c.Title 排序?这似乎是下面代码的错误复制和粘贴。此外,Code Review 可以作为这种问题的替代方案。 - Lance U. Matthews
请查看Gridify库。https://github.com/Alirezanet/Gridify - AliReza Sabouri
8个回答

4

重要免责声明

在计算机科学中,总是存在着一种权衡考虑。

我建议的解决方法需要提高你的解决方案的工程级别。然而这并不总是正确的做法,因为它会增加代码的抽象级别。

如果这段代码不经常更改,可能的排序条件较少,并且你认为它将保持不变的话,你可以继续使用当前的实现(或基于switch语句的等效解决方案,这在代码可读性方面可能更好)。

为什么使用if串是个问题

除了代码可读性的问题外,长长的一串if语句的主要问题是违反了开闭原则。您可以在这里找到有关该原则的解释。

简而言之,这意味着像您展示的代码这样的代码会不断发生变化,因为每次引入新条件(或删除现有条件)时,必须通过添加(或删除)一个新的if语句来修改现有代码。这不是理想的情况,因为更改已经工作的现有代码可能会导致错误,并且在时间和注意力方面是一项昂贵的操作。理想的情况是拥有一段良好运行、经过充分测试并且可以保持稳定的代码。为了满足新的要求,我们希望编写新模块(例如,新类),而不触及任何现有的工作模块。

将if串更改为switch语句可行吗?

不可行。 将if串转换为switch可能会提高代码的可读性,但它并不是违反开闭原则的根本解决方案。维护switch语句时受到与级联if语句相同的问题的影响。

设计模式来拯救。

首先,尝试应用以下设计模式,这基本上是策略模式

请注意,使用此方法,通过编写接口的新实现,您可以扩展程序的功能,但您永远不必修改现有的接口实现和使用该接口的代码。

// define an interface encapsualting the desired behavior

IProductsProvider 
{
  bool CanHandleCondition(string condition);
  Task<IEnumerable<Products>> GetProducts(string condition);
}

// write an implementation of your interface for each possible condition that you have

class TitleAscendingProvider : IProductsProvider 
{
  private readonly IApiClient _apiClient;

  public TitleAscendingProvider(IApiClient apiClient)
  {
    _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
  }

  public bool CanHandleCondition(string condition) => condition == "TitleASC";

  public async Task<IEnumerable<Products>> GetProducts(string condition)
  {
    var products = await _apiClient.GetProducts();
    return products.OrderBy(c => c.Title);
  }
}

class TitleDescendingProvider : IProductsProvider 
{
  private readonly IApiClient _apiClient;

  public TitleAscendingProvider(IApiClient apiClient)
  {
    _apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
  }

  public bool CanHandleCondition(string condition) => condition == "TitleDESC";

  public async Task<IEnumerable<Products>> GetProducts(string condition)
  {
    var products = await _apiClient.GetProducts();
    return products.OrderByDescending(c => c.CategoriesTitle);
  }
}

// this is the implementation of the interface that you will register with your DI container
// inject all the other implementations of the IProductsProvider interface

class CompositeProvider : IProductsProvider 
{
  private readonly IProductsProvider[] _providers;

  public TitleAscendingProvider(IEnumerable<IProductsProvider> providers)
  {
    if (providers is null) 
    {
      throw new ArgumentNullException(nameof(providers));
    }

    _providers = providers.ToArray();
  }

  public bool CanHandleCondition(string condition) => _providers.Any(p => p.CanHandleCondition(condition));

  public Task<IEnumerable<Products>> GetProducts(string condition)
  {
    var provider = _providers.FirstOrDefault(p => p.CanHandleCondition(condition));
    if (provider == null) 
    {
      throw new InvalidOperationException("Unable to find a proper provider for the condition '{condition}'")
    }

    return provider.GetProducts(condition);
  }
}

// remember to register the class CompositeProvider as the implementation of the interface IProductsProvider in the DI container
// let the DI container to inject the implementation of IProductsProvider in your controller

private readonly IProductsProvider _provider;

public async Task<ActionResult<IEnumerable<Product>>> FilterAsync(string conditionName)
{
    var products = await _provider.GetProducts(conditionName);
    return Ok(products);
}

基于表达式树的另一种可能方法

另一种可能的方法是使用表达式树。表达式树基本上是代表某些代码的对象,您可以在运行时检查它,以分析它并对其执行有用的操作。给定一个表达式树可以进行的其中一件事是编译它: 这样做会得到一个委托,您可以执行它。换句话说,您从代表某些代码的对象开始,最终得到一个委托,您可以调用以执行由表达式树表示的代码

这种方法的基本思想是询问用户他想要使用哪个字段对产品进行排序以及排序方向(升序或降序)。然后,您可以构建一个表达式树来表示一些代码,该代码给定产品类的一个实例,访问必须用于对产品进行排序的属性。最后,您可以编译表达式树以获取一个委托实例,您可以将其传递给LINQ to object OrderBy 扩展方法。

这种解决方案并不完全适用于您的问题,因为它需要调用通用方法,并且无法从用户输入(用于对产品进行排序的属性名称)中推断出通用类型参数,但我认为将表达式树提到这里是值得一提的。把我的回答中的这一部分看作进一步研究表达式树世界的触发器。

以下代码是使用表达式树来获取用于排序某些对象的委托的示例,从要排序的属性名称开始。

static class Program
  {
    public static void Main(string[] args)
    {
      var people = new Person[]
      {
        new Person { Name = "Bob", Age = 11 },
        new Person { Name = "Alice", Age = 8 },
        new Person { Name = "Tony", Age = 1 }
      };

      //imagine this comes from the user...
      var propertyName = nameof(Person.Age);          

      // the issue is that here you need to pass the right generic type argument and based on my understanding you can't infer it from the propertyName variable
      var expression = GetPropertyExpression<int>(propertyName);
      var func = expression.Compile();

      var sorted = people.OrderBy(func).ToArray();
    }

    private static Expression<Func<Person, T>> GetPropertyExpression<T>(string propertyName)
    {
      var parameter = Expression.Parameter(typeof(Person), "model");
      var property = Expression.Property(parameter, propertyName);
      var expression = Expression.Lambda<Func<Person, T>>(property, parameter);
      return expression;
    }

    public class Person 
    {
      public string Name { get; set; }
      public int  Age { get; set; }
    }
  }

欢迎任何有更深入理解表达式树的社区成员来改进这个想法,或者解释为什么表达式树不适用于这个问题的场景。
此处,您可以找到使用表达式树构建动态查询的另一个示例。
2020年4月26日编辑
正如其他答案指出的那样,值得一试的是System.Linq.Dynamic.Core库,它允许创建基于文本的动态查询。
这可能是在解决您的问题时利用表达式树最安全的方式。除非您是表达式树专家,否则最好使用适当的库,避免使用自制的“穷人”解决方案上线生产。

我并不是从数据库中获取数据,而是从Web客户端获取。 - Great Khan 2016
@GreatKhan2016,这是一样的,无论你从哪里获取数据都没有关系。只需更改注入依赖项的名称即可。我会编辑答案。 - Enrico Massone
@GreatKhan2016 也可以考虑一下 Shervin Ivari 提供的答案。提到的 System.Linq.Dynamic.Core 对你的问题似乎很有帮助。 - Enrico Massone

3

您可以使用System.Linq.Dynamic.Core;

 int i = conditionName.Contains("DESC") ? conditionName.IndexOf("DESC") : conditionName.IndexOf("ASC");
 //exp will be like Title desc
 var exp = conditionName.Substring(0, i) + " " + conditionName.Substring(i);
 var result = dataSorted.Orderby(exp).Tolist();

Linq.Dynamic 允许您使用接受字符串参数的扩展方法来表达 LINQ 查询。


3
您可以这样简化您的解决方案:
public async Task<ActionResult<IEnumerable<Product>>> FilterAsync(string 
conditionName)
{
    var values = await GetProducts();
    IOrderedEnumerable<Product> dataSorted =null;

    switch (conditionName)
    {
        case "TitleASC":
            dataSorted = values.OrderBy(c => c.Title);
            break;
        case "TitleDESC":
            dataSorted = values.OrderByDescending(c => c.CategoriesTitle);
            break;
        case "DateTimeASC":
            dataSorted =values.OrderBy(c => c.DateTime).ThenBy(c => c.CategoriesTitle);
            break;
        case "DateTimeDESC":
            dataSorted = values.OrderByDescending(c => c.DateTime).ThenByDescending(c => c.CategoriesTitle);
            break;
        default:
            // if you want you can add default order here
            break;
    }
    return Ok(dataSorted);
}

1
关于 switch 表达式 呢?
public async Task<ActionResult<IEnumerable<Product>>> FilterAsync(string conditionName)
{
    var values = await GetProducts();

    IOrderedEnumerable<Product> dataSorted = conditionName switch
            {
                "TitleASC"     => values.OrderBy(c => c.Title),
                "TitleDESC"    => values.OrderByDescending(c => c.CategoriesTitle),
                "DateTimeASC"  => values.OrderBy(c => c.DateTime),
                "DateTimeDESC" => values.OrderByDescending(c => c.DateTime),
                "CategoryASC"  => values.OrderBy(c => c.CategoriesTitle),
                "CategoryDESC" => values.OrderByDescending(c => c.CategoriesTitle),
                _              => null
            };

    return Ok(dataSorted);
}

0

0

您可以利用switch语句并拆分排序键和排序顺序,从而减少一半的条件:

// Helper methods:
public static class LinqExtensions
{ 
    public static IEnumerable<TSource> OrderByFlag<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, bool descending = false)
    {
        return descending
            ? source.OrderByDescending(keySelector)
            : source.OrderBy(keySelector);
    } 
}

// Then, in the controller:
public async Task<ActionResult<IEnumerable<Product>>> FilterAsync(string conditionName, bool desc = false)
{
    var products = await GetProducts();

    switch (conditionName)
    {
        case "Title":
            products = products.OrderByFlag(p => p.Title, desc);
            break;             

        case "DateTime":
            products = products.OrderByFlag(p => p.DateTime, desc);
            break;

        case "Category":
            products = products.OrderByFlag(p => p.CategoriesTitle, desc);
            break;

        default:
            throw new ArgumentException(nameof(conditionName), "Unknown sorting criteria");
    }

    return Ok(products);
}

-1

switch语句或者switch表达式可以帮助你实现这个目的,让你的代码更易读。


-2

一种选择是将功能抽象化。类似于...

public async Task<ActionResult<IEnumerable<Product>>> FilterAsync(string conditionName)
    {
        var values = await GetProducts();

        IOrderedEnumerable<Product> dataSorted =null;

        val dataSorted = SortFunction(conditionName,values)
    }

并让SortFunction函数执行逻辑。 在此函数中,使用Case / switch语句处理各种选项。您可以使用上面建议的switch语句。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接