Wednesday 27 January 2021

Stack Overflow post led to this

Several months ago, I read a question on our beloved StackOverflow. OP wanted functionality to search 
for a keyword in all properties of an object.
Generally, when you create a search feature in an application, the logic is to search in one/few known
set of properties. Like, if you search for Steve, the application will search fields like Name or Surname 
being Steve.
However, this question was interesting as it plain and simple wants to search in all properties.
To design the below code (in C#), I used the following assumptions:

  1. Datatype of the search item will decide the properties that will be searched
  2. Match is Exact match

What I mean here is that if user searches for string "Steve", code looks for it in all string fields of the object.
If user searches for integer 2, we try to find it in all integer fields in the object.

Enough Talk, here comes the code:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

class Program
   {
      static void Main()
      {
         var cust = new Customer
         {
            Id = 111,
            Name = "Martin",
            Age = 22,
            Address = "Somewhere on Earth",
            Contracts = new List<Contract>(),
            Tags = new List<string>() { "Hello", "World", "Martin" },
            Test = new JustForTest { TestId = 420, TestName = "Nothing Martin" }
         };
         cust.Contracts.Add(new Contract
         {
            Id = 1,
            From = DateTime.Now.AddMonths(-1),
            To = DateTime.Now.AddMonths(1),
            Comment = "Signed by Martin",
            CustomerId = 111,
            Customer = cust
         });
         cust.Contracts.Add(new Contract
         {
            Id = 2,
            From = DateTime.Now.AddMonths(-2),
            To = DateTime.Now.AddMonths(3),
            Comment = "Thank the best answer on StackOverflow Martin",
            CustomerId = 111,
            Customer = cust
         });
         HashSet<object> coll = new HashSet<object>();
         var result = Search<string, Customer>("Martin", cust, coll);
      }
      private static List<string> Search<T, O>(T keyword, O obj, HashSet<object> processedObjects)
      {
         var retVal = new List<string>();
         if (processedObjects.Contains(obj))
            return retVal;
         // Search Direct Properties
         var properties = obj.GetType().GetProperties().Where(p => p.PropertyType == typeof(T));
         foreach (var prop in properties)
         {
            var propValue = prop.GetValue(obj);
            if (propValue != null && propValue.ToString().Contains(keyword.ToString()))
            {
               retVal.Add($"{prop.DeclaringType?.FullName}.{prop.Name} - contains - {keyword}");
            }
         }
         // Search Complex Properties
         var complexProperties = obj.GetType().GetProperties().Where(p => p.PropertyType.IsClass && typeof(IEnumerable).IsAssignableFrom(p.PropertyType) == false);
         foreach (var prop in complexProperties)
         {
            retVal.AddRange(Search(keyword, prop.GetValue(obj), processedObjects));
            processedObjects.Add(obj);
         }

         // Search Collections
         var collectionProperties = obj.GetType().GetProperties().Where(p => (p.PropertyType.IsClass || p.PropertyType.IsAnsiClass) && p.PropertyType != typeof(string) && typeof(IEnumerable).IsAssignableFrom(p.PropertyType));
         foreach (var collProperty in collectionProperties)
         {
            if (collProperty.PropertyType.IsGenericType)
            {
               var paramType = collProperty.PropertyType.GetGenericArguments().FirstOrDefault();
               if (paramType != null && paramType.IsClass && paramType != typeof(string))
               {
                  var enumColl = collProperty.GetValue(obj) as IEnumerable;
                  foreach (var item in enumColl)
                  {
                     retVal.AddRange(Search(keyword, item, processedObjects));
                     processedObjects.Add(item);
                  }
               }
               else
               {
                  var enumColl = collProperty.GetValue(obj) as IEnumerable;
                  foreach (var item in enumColl)
                  {
                     if (item != null && item.ToString().Contains(keyword.ToString()))
                     {
                        retVal.Add($"{collProperty.DeclaringType?.FullName}.{collProperty.Name} - contains - {keyword}");
                     }
                  }
               }
            }
         }
         return retVal;
      }
   }


   public class Customer
   {
      public int Id { get; set; }
      public string Name { get; set; }
      public int Age { get; set; }
      public string Address { get; set; }
      public JustForTest Test { get; set; }
      public List<string> Tags { get; set; }
      public ICollection<Contract> Contracts { get; set; }
   }

   public class Contract
   {
      public int Id { get; set; }
      public DateTime From { get; set; }
      public DateTime To { get; set; }
      public string Comment { get; set; }
      public int CustomerId { get; set; }
      public Customer Customer { get; set; }
   }

   public class JustForTest
   {
      public int TestId { get; set; }
      public string TestName { get; set; }
   }


This function can be used as Generic Search over class members.