1

In my project, I want the user to be able to supply an argument in the shape of: "Fieldname=Value"; I will then check the arguments supplied, and check if the Fieldname is a valid property in a Model; then, I need to have a Linq.Where() query that dynamically selects the required Fieldname for the filtering:

start.exe -Filter "TestField=ThisValue" should translate to: List<Mutations>.Where(s => s.{TestField} == "ThisValue" ).FirstOrDefault

the problem is that I do not know how I convert a string, or a name of a property, to the s.{TestField} part..

Fimlore
  • 147
  • 1
  • 9
  • 2
    The keyword you're after is "Reflection", specifically `Type.GetField` – canton7 Apr 07 '21 at 16:18
  • to add to @canton7 comment if you look [here](https://stackoverflow.com/questions/1196991/get-property-value-from-string-using-reflection) the top answer pretty much resume it. You would probably want to keep the return of the `GetProperty` which return a `PropertyInfo` object in order to reflect the type only once instead of every element of the list – Franck Apr 07 '21 at 16:20
  • 1
    Note the difference between fields and properties -- you'll need to use `GetField(..)` to fetch fields, and `GetProperty(..)` to fetch properties – canton7 Apr 07 '21 at 16:22
  • You can use [expression tree](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/) – Magnetron Apr 07 '21 at 16:33
  • ```Type myTypeA = typeof(Mutations);FieldInfo MyFieldInfo = myTypeA.GetField("TestField");``` this returns null, also; this does not explain to me how I can then use it in the Linq query... When I explore the object using the debugger,. I can see that its there in the Declared fields for MyTypeA, but it doesnt return any FieldInfo (null) – Fimlore Apr 07 '21 at 16:36
  • Can you post a [mcve]? I.e. the code for your `Mutations` clas – canton7 Apr 08 '21 at 08:02

1 Answers1

1

You can use the Reflection and Expression APIs for this. To start, I am going to assume that you are actually using properties and not fields (you are using properties, right?)

var type = typeof(Mutations);
var member = type.GetProperty("TestProperty");
if (member == null)
   throw new Exception("property does not exist");

var p1 = Expression.Parameter(type, "s");
var equal = Expression.Equal(
  Expression.Property(p1, member),
  Expression.Constant("Test Value", member.PropertyType)
);
var lambda = Expression.Lambda<Func<Mutations, bool>>(equal, p1);

var result = list.AsQueryable().FirstOrDefault(lambda);

If you are actually using public fields (why?!) you can make the following modifications GetProperty->GetField, Expression.Property->Expression.Field and member.PropertyType->member.FieldType. Use caution though; some ORMs only work with properties and thus would reject the otherwise valid Expression.

We can take the above and turn it into a reusable, generic method that returns an Expression:

using System.Linq.Expressions;

public static class ExpressionHelpers {
  public static Expression CreateWhere<T>(string propertyName, string targetValue) {
    var type = typeof(T);
    var member = type.GetProperty(propertyName) ?? throw new Exception("Property does not exist");

    var p1 = Expression.Parameter(type, "s");
    var equal = Expression.Equal(
      Expression.Property(p1, member), 
      Expression.Constant(targetValue, member.PropertyType)
    );
    return Expression.Lambda<Func<T, bool>>(equal, p1);
  }
}

Calling this method might look like:

public static void SomeMethod() {
  var list = new List<Mutations> { /* ... */ };

  Expression clause = ExpressionHelpers.CreateWhere<Mutations>("TestProperty", "TestValue");
  var result = list.AsQueryable().FirstOrDefault(clause);
  if (result != null)
    Console.WriteLine("Result = {0}", result);
}

Note that this doesn't do any sort of validation of the data types--it assumes the property is the same type as the input, which is currently string. If you need to deal with numbers or dates or what have you, you'll need to switch on the data type and provide the appropriate parsed data to the constant:

public static Expression CreateWhere<T>(string propertyName, string targetValue) {
  var type = typeof(T);
  var member = type.GetProperty(propertyName) ?? throw new Exception("Property does not exist");
  var propType = member.PropertyType;

  if ((propType.IsClass && propType != typeof(string)) || propType.IsInterface)
    throw new Exception("Interfaces and Class Types are not supported");

  var p1 = Expression.Parameter(type, "s");

  Expression target = null;
  if (propType == typeof(string)) 
    target = Expression.Constant(targetValue, typeof(string));

  else if (propType == typeof(int) && int.TryParse(targetValue, out var intVal))
    target = Expression.Constant(intVal, typeof(int));

  else if (propType == typeof(long) && long.TryParse(targetValue, out var longVal))
    target = Expression.Constant(longVal, typeof(long));

  else if (propType == typeof(DateTime) && DateTime.TryParse(targetValue, out var dateVal))
    target = Expression.Constant(dateVal, typeof(DateTime));

  else
     throw new Exception("Target property type is not supported or value could not be parsed");

  var equal = Expression.Equal(
    Expression.Property(p1, member), 
    target
  );
  return Expression.Lambda<Func<T, bool>>(equal, p1);
}

As you can see this starts to get fairly complex with the more types you want to support. Also note that if you are using this without an ORM (just LINQ on a list) you are probably going to want add some support for case-[in]sensitive string comparisons. That can be delegated to a string.Equals call that could look something like this:

bool ignoreCase = true; // maybe a method parameter?
var prop = Expression.Property(p1, member);

Expression equal = null;
if (propType != typeof(string))
{
  equal = Expression.Equal(prop, target);
}
else 
{
  var compareType = ignoreCase 
    ? StringComparison.OrdinalIgnoreCase 
    : StringComparison.Ordinal;

  var compareConst = Expression.Constant(compareType, typeof(StringComparison));

  equal = Expression.Call(
   typeof(string), 
   nameof(string.Equals), 
   new[] { typeof(string), typeof(string), typeof(StringComparison) }, 
   prop, 
   target, 
   compareConst
  );
}

return Expression.Lambda<Func<T, bool>>(equal, p1);

Note that depending on their support this may or may not work with an ORM (and may not be necessary since many databases are insensitive comparisons by default). The above also does not handle Nullable<T> (ie. int?) which adds its own set of complexities.

pinkfloydx33
  • 11,863
  • 3
  • 46
  • 63
  • 1
    Thank you for your very extensive answer. I see where the confusion started; as I called my "TestField" variable a Field instead of a property, Sorry - old habits. Your top code sample works like a charm and I am even able to understand it's inner workings. – Fimlore Apr 09 '21 at 09:44