Search code examples
c#linq

Use of variables in Linq for property


I would like to return results from a List based on various search parameters and operators. But I would like to avoid a long if then else list for all the combinations, so it would be nice to be able to use variables in the Linq search... Something like this (which obviously is not working):

//NOW searchParameter contains one of the following: "orderNumber", "customerOrderNumber", "customerCode", "customerName", "shippingCity", "shippingCountry"
//AND searchOperator contains one of the following: "equalto", "notequalto", "before", "after", "lessthan", "morethan"
//AND searchCriteria is either a string or a DateTime
if (searchOperator == "equalto")    
{
    List<SalesOrder> results = Program.salesOrderCollection.getSalesOrders().Where(o => o[searchParameter].Equals(searchCriteria)).ToList();
}

Can you give me some ideas for achieving this? Thanks.


Solution

  • Why would you want to use a string as search parameter? Strings are for humans, not for computers!

    Separate your concerns

    This means: separate your input from the processing and from the output.

    The only reason to use a string would be if you have some external source of which you don't know the origin, like a human (okay, sometimes an external computer). If you have a UI, where an operator types a string that represents the parameter he wants to use, you should immediately convert to what his input stands for: this input is not meant to be used as a string, but as parameter predicate in a Where statement.

    In your case, this parameter has the format of Func<SalesOrder, bool>, or if it is a database query: Expression<Func<SalesOrder, bool>>

    The advantage of such a conversion function is that you can check validity of the input string in an early stage, immediately after the operator finished entering his data. Another example of course is re-usability: if the predicate parameter came from another source than a human entering a string, for instance, a human selecting an item in a combo box, or another part of your program, you can re-use your processing part.

    Apart from re-usabilitiy, other advantages of separating your concerns would be that the code is easier to understand, easier to change and easier to test. Changes in requirements would lead to smaller changes in your code.

    For example: your requirement is now that the operator selects the parameter to use in the search string from a combo box, containing items like Id, OrderDate, CustomerName.

    But suppose you have a new requirements

    • Change your forms application that operators can select CustomerName Kennedy or OrderDate > January 2019
    • Make a command line version of the program where users can type -CustomerName Kennedy, but also CUSTOMERNAME kennedy, -c Kennedy, and even -cuSTOMErname KENnedY

    If you have one procedure that converts the input into a Func<SalesOrder, bool>, your processing and output don't have to change.

    So let's separate!

    Back to your question

    Input: two strings: a paramNameand a paramValue, paramName is the name of the Parameter to use, and paramValue is a string representation of the value to compare with.

    It should be possible to convert paramValue from string to the proper type of the parameter. So if the parameter is a DateTime, then paramValue should be a string that can be converted to a DateTime.

    Output: The Func<SalesOrder, bool> representing parameter Predicate that can be used in a Where statement to select a subset of SalesOrders.

    class InputConverter
    {
      static Func<SalesOrder, bool> ToWherePredicate(string paramName, string paramValue)
      {
        switch (paramName)
        {
          case "Id:"
            int value = Int32.Parse(paramValue);
            return salesOrder => salesOrder.Id == value;
            break;
          case "OrderDate":
            DateTime value = DateTime.Parse(paramValue);
            return salesOrder => salesOrder.OrderDate == value;
            break;
          case "CustomerId":
            int value = Int32.Parse(paramValue);
            return salesOrder => salesOrder.CustomerId == value;
            break;
          case ...
    
          default:
            // Other string values are not supported
            throw new NotSupportedException(...);
        }
      }
    }
    

    See how easy it will be to extend this function to accept inputs like -Date, -Id, for a command line?

    The extension function you where waiting for: a Where that takes as input a sequence of SalesOrders and a KeyValuePair<string, string> and has as output the selected SalesOrders will now be a one-liner.

    I decided to create it as an extension function. This way you can use it as if it was a standard LINQ function See Extension Methods Demystified

    static IEnumerable<SalesOrder> Where(this IEnumerable<SalesOrder> salesOrders,
           string paramName, string paramValue)
    {
      Func<SalesOrder, bool> predicate = InputConverter.ToWherePredicate(paramName, paramValue)
      return salesOrders.Where(predicate);
    }
    

    Usage, reading input from a ComboBox and a TextBox (in baby steps):

    string paramName = (string)comboBoxParamName.SelectedValue;
    string paramValue = (string)textBoxParamValue.SelectedValue
    
    var requestedSalesOrders = Program.salesOrderCollection.getSalesOrders()
        .Where(paramName, paramValue)
        .GroupBy(salesOrder => salesOrder.OrderDate)
        .OrderBy(group => group.Key)
        ...
    

    See that because I created it as an extension function of IEnumerable, it behaves exactly like any other LINQ function.

    One final remark: if function GetSalesOrders returns an IQueryable<SalesOrder> instead of an IEnumerable<SalesOrder>, you should use Expression<Func<SalesOrder, bool>> instead of Func<SalesOrder, bool>. The lambda expressions in ToPredicate will not change.