In the world of data manipulation and filtering, the Queryable and Enumerable search extension classes offer developers powerful tools to streamline the process of searching through collections. These extension methods, designed to work with IQueryable and IEnumerable interfaces, leverage a common set of expression creators to dynamically generate filtering expressions based on various data types. In this and later blog post, we will explore the intricacies of both extensions, shedding light on their functionalities and discussing the key differences between them.

1-QueryableExtensions

The QueryableExtensions class is tailored for scenarios where data resides in a database and is queried using LINQ to SQL or similar technology. Let's delve into the details of this class:

public static class QueryableExtensions
{
  // A collection of expression creators for DateTime, Numeric, and String types
  private static ReadOnlyCollection<IExpressionCreator> ExpressionCreators =>
    new(new IExpressionCreator[]
    {
      new DateTimeExpressionCreator(),
      new NumericTypesExpressionCreator(),
      new StringTypesExpressionCreator()
    });

  // Search method for IQueryable<T> sources
  public static IQueryable<T> Search<T>
    (this IQueryable<T> source,
    Expression<Func<T, object?>> keySelector,
    string searchValue)
  {
    // Input validation
    if (source == null)
    {
      throw new ArgumentNullException(nameof(source));
    }

    if (keySelector == null)
    {
      throw new ArgumentNullException(nameof(keySelector));
    }

    if (string.IsNullOrWhiteSpace(searchValue))
    {
      return source;
    }

    // List to store dynamically generated expressions
    var expressions = new List<Expression<Func<T, bool>>>();

    // Iterate through expression creators and generate expressions
    foreach (var expressionCreator in ExpressionCreators)
    {
      var createdExpressions = expressionCreator
        .CreateExpressions(searchValue, keySelector);
      expressions.AddRange(createdExpressions);
    }

    // Combine generated expressions using OR logic
    if (expressions.Any())
    {
      var combinedExpressions =
        expressions.CombineExpressionsWithOr()!;
      return source.Where(combinedExpressions);
    }

    return source;
  }
}

The stages of the QueryableExtensions' search process are as follows:

  1. Expression Creators Collection: The class initializes a collection of expression creators, including DateTimeExpressionCreator, NumericTypesExpressionCreator, and StringTypesExpressionCreator.
  2. Search Method for IQueryable: The Search method is an extension method for IQueryable<T> sources, accepting a key selector and a search value as parameters. It dynamically generates filtering expressions based on the provided key selector and search value.
  3. Dynamic Expression Generation: The method iterates through the registered expression creators, generating expressions for DateTime, Numeric, and String types.
  4. Combining Expressions: It combines the generated expressions using OR logic to create a single combined expression.
  5. Filtering Using Where Clause: The combined expression is then applied to the source using the LINQ Where clause.
  6. EnumerableExtensions Class: The EnumerableExtensions class, on the other hand, is designed for in-memory collections where data is not queried against a database. Let's explore its features:

2-EnumerableExtensions

The EnumerableExtensions class is as same as the QueryableExtensions with one difference. Unlike QueryableExtensions, where expressions are directly applied to the source, in this class, the combined expression is compiled and then applied using the LINQ Where clause. This is because of the nature of both interfaces. IQueryable accepts expression trees, which are data structures that represent lambda expressions in a tree-like format. These expression trees can be analyzed and translated by query providers (e.g., LINQ to SQL) for optimization. IEnumerable operates on delegates (functions) rather than expression trees. The operations are performed on the actual data in memory. Therefore, We designed QueryableExtensions for scenarios where data resides in a database, and queries are executed against the database using LINQ to SQL or similar technology. The generated expressions are applied directly to the IQueryable source, allowing the database engine to optimize and execute the filtering operations. On the other hand, we tailored EnumerableExtensions for in-memory collections where the entire dataset is available locally. The expressions are compiled and then applied using the LINQ Where clause, as there is no underlying database engine to optimize the query execution.

public static class EnumerableExtensions
{
  // A collection of expression creators for DateTime, Numeric, and String types
  private static ReadOnlyCollection<IExpressionCreator> ExpressionCreators =>
    new(new IExpressionCreator[]
    {
      new DateTimeExpressionCreator(),
      new NumericTypesExpressionCreator(),
      new StringTypesExpressionCreator()
    });

  // Search method for IEnumerable<T> sources
  public static IEnumerable<T> Search<T>(
    this IEnumerable<T> source,
    Expression<Func<T, object>> keySelector,
    string searchValue)
  {
    // Input validation
    if (source == null)
    {
      throw new ArgumentNullException(nameof(source));
    }

    if (keySelector == null)
    {
      throw new ArgumentNullException(nameof(keySelector));
    }

    if (string.IsNullOrWhiteSpace(searchValue))
    {
      return source;
    }

    // List to store dynamically generated expressions
    var expressions = new List<Expression<Func<T, bool>>>();

    // Iterate through expression creators and generate expressions
    foreach (var expressionCreator in ExpressionCreators)
    {
      var createdExpressions = expressionCreator
        .CreateExpressions(searchValue, keySelector);
      expressions.AddRange(createdExpressions);
    }

    // Combine generated expressions using OR logic
    if (expressions.Any())
    {
      var combinedExpressions =
        expressions.CombineExpressionsWithOr()!;
      return source.Where(combinedExpressions.Compile());
    }

    return source;
  }
}