Search code examples
c#.netasp.net-mvclinqdynamic-linq

How to filter LINQ query on dynamic columns and dynamic property in C#?


I am facing trouble in fetching the filtered records in my LINQ Query to retrieve the columns dynamically based on condition and property also based on condition (ex. Contains, Equals, StartsWith, EndsWith).

I have a list of records like below -

 List<Employee> employees = new List<Employee>()
            {

                new Employee()
                {
                    name ="Andy", cityCriteria="Florida West", state   ="NYC"
                },
                new Employee()
                {
                    name = "John", cityCriteria = "West Virginia", state = "Arizona"
                },
                new Employee()
                {
                    name = "Nichole", cityCriteria = "East Florida", state = "NYC"
                }
            };

So, this is just some sample records the data will be coming from database and it will be so many records. Now, what i want to acheive is I have to notify all the persons if any Video posted with the City matching as per the list . So, i can receive NotificationValue as City:Florida:startsWith , City:Florida: Equals , City:Florida:Contains etc and there could be State Criteria too. So, how can i filter the records dynamically in the list like if input is Starts with i should use StartsWith ex

If Input is City:Florida:startsWith --> 
 var result = employees.where(i=>i.CityCriteria.StartsWith("Florida").toList();

If Input is City:Florida:Contains --> 
 var result = employees.where(i=>i.CityCriteria.Contains("Florida").toList();

If Input is City:Florida:EndsWith --> 
 var result = employees.where(i=>i.CityCriteria.EndsWith("Florida").toList();

If Input is City:Florida:Equals --> 
 var result = employees.where(i=>i.CityCriteria.Equals("Florida").toList();

I don't want to use multiple conditions and form the Where clause . I want it to be dynamic like if i receive starts with it should replace LINQ query starts , endswith ,equals etc and also it should be flexible with dynamic column like I have to apply same logic for State,Country,Zip etc'

Please post some sample code if possible


Solution

  • A simple and fast solution to get property value will be to use DynamicMethod. Here is how I did it, and it is a working solution:

    using Newtonsoft.Json;
    using NUnit.Framework;
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Reflection.Emit;
    
    namespace NUnitTestProject1
    {
        public class Tests
        {
            static List<Employee> employees = new List<Employee>()
            {
                new Employee()
                {
                    Name = "Andy", City = "Florida West", State = "NYC"
                },
                new Employee()
                {
                    Name = "John", City = "West Virginia", State = "Arizona"
                },
                new Employee()
                {
                    Name = "Nichole", City = "East Florida", State = "NYC"
                }
            };
    
            public enum Comparison
            {
                StartsWith,
                EndsWith,
                Equals
            }
    
            public struct Condition
            {
                public string PropertyName { get; set; }
                public string PropertyValue { get; set; }
                public Comparison Comparison { get; set; }
            }
    
            [TestCase("City", "Florida", Comparison.StartsWith, "Andy")]
            [TestCase("State", "Arizona", Comparison.Equals, "John")]
            public void TestConditions(string propertyName, string propertyValue, Comparison comparison, string expectedResult)
            {
                string jsonCondition = $"{{\"PropertyName\":\"{propertyName}\",\"PropertyValue\":\"{propertyValue}\",\"Comparison\":{(int)comparison}}}";
                Condition parsedCondition = JsonConvert.DeserializeObject<Condition>(jsonCondition);
                List<Employee> result = new List<Employee>();
                var getter = GetPropertGetter(typeof(Employee).ToString(), parsedCondition.PropertyName);
                switch (parsedCondition.Comparison)
                {
                    case Comparison.StartsWith:
                        result = employees.Where(i => (getter(i) as string).StartsWith(parsedCondition.PropertyValue)).ToList();
                        break;
                    case Comparison.EndsWith:
                        result = employees.Where(i => (getter(i) as string).EndsWith(parsedCondition.PropertyValue)).ToList();
                        break;
                    case Comparison.Equals:
                        result = employees.Where(i => (getter(i) as string).Equals(parsedCondition.PropertyValue)).ToList();
                        break;
                }
    
                Assert.That(result.FirstOrDefault().Name, Does.Match(expectedResult));
            }
    
            Func<object, object> GetPropertGetter(string typeName, string propertyName)
            {
                Type t = Type.GetType(typeName);
                PropertyInfo pi = t.GetProperty(propertyName);
                MethodInfo getter = pi.GetGetMethod();
    
                DynamicMethod dm = new DynamicMethod("GetValue", typeof(object), new Type[] { typeof(object) }, typeof(object), true);
                ILGenerator lgen = dm.GetILGenerator();
    
                lgen.Emit(OpCodes.Ldarg_0);
                lgen.Emit(OpCodes.Call, getter);
    
                if (getter.ReturnType.GetTypeInfo().IsValueType)
                {
                    lgen.Emit(OpCodes.Box, getter.ReturnType);
                }
    
                lgen.Emit(OpCodes.Ret);
                return dm.CreateDelegate(typeof(Func<object, object>)) as Func<object, object>;
            }
        }
    
        internal class Employee
        {
            private string name;
            private string city;
            private string state;
    
            public string Name { get => name; set => name = value; }
            public string City { get => city; set => city = value; }
            public string State { get => state; set => state = value; }
        }
    }