morleyc
morleyc

Reputation: 2431

C# recursively check all values not null

Without wanting to reinvent the wheel, is there a .NET NuGet library to perform checks on a object recursively for argument checking?

If not, how would I convert the code to check if a property is null, and if a type that can hold properties of its own, recursively check that type, and end up with a list of property names that are null.

public static class Assert
{
    public static void AllPropertiesNotNull<T>(T obj)
    {
        if (obj == null)
            throw new ArgumentNullException(nameof(obj));

        var emptyProperties = typeof(T)
                                .GetProperties()
                                .Select(prop => new { Prop = prop, Val = prop.GetValue(obj, null) })
                                .Where(val => IsEmpty((dynamic)val.Val))
                                .Select(val => val.Prop.Name)
                                .ToList();

        if (emptyProperties.Count > 0)
            throw new ArgumentNullException(emptyProperties.First());
    }

    private static bool IsEmpty(object o) { return o == null; }
}

Upvotes: 2

Views: 1240

Answers (2)

Reza Aghaei
Reza Aghaei

Reputation: 125197

Note: You should do null checking on constructor parameters or method parameters and throw exception when the parameter is unexpectedly null. It's better to follow the common best practices.

Anyway, here is an example showing how you can check all properties of an object recursively using an extension method and throw exception of finding null properties...

You can create an extension method ThrowOnNullProperty for object and use it like this:

something.ThrowOnNullProperty();

Here is an implementation of such extension method:

  1. If the passed object is null, throw exception.
  2. If the object is a primitive type or a string, continue.
  3. If the object has been visited before, then continue, otherwise add it to list of visited objects.
  4. Check first level properties of the object and if there are null properties, throw an exception containing name of the null properties.
  5. If the first level properties are not null, the for each property value go to 1.

Here is the code:

using System;
using System.Collections.Generic;
using System.Linq;
public static class ObjectExtensions
{
    public static void ThrowOnNullProperty(this object obj)
    {
        ThrowOnNullProperty(obj, new HashSet<object>());
    }
    private static void ThrowOnNullProperty(object obj, HashSet<object> visitedObjects)
    {
        if (obj == null)
            throw new ArgumentNullException(nameof(obj));
        if (obj.GetType().IsPrimitive || obj.GetType() == typeof(string))
            return;
        if (visitedObjects.Contains(obj))
            return;
        visitedObjects.Add(obj);

        var nullPropertyNames = obj.GetType().GetProperties()
           .Where(p => p.GetValue(obj) == null)
           .Select(p => p.Name);
        if (nullPropertyNames.Any())
            throw new ArgumentException(
                $"Null properties: {string.Join(",", nullPropertyNames)}");

        var notNullPropertyValues = obj.GetType().GetProperties()
            .Select(p => p.GetValue(obj))
            .Where(v => v != null);
        foreach (var item in notNullPropertyValues)
            ThrowOnNullProperty(item, visitedObjects);
    }
}

Upvotes: 1

Sohaib Jundi
Sohaib Jundi

Reputation: 1664

To do so, write a method to check the properties of current object and call it recursively on the non-null properties. I went ahead and wrote some code, which includes looping over dictionaries and enumerables and checking them for nulls also, taking into account circular references as mentioned by @dcg.

static readonly HashSet<Type> excludedTypes = new HashSet<Type>{ typeof(string) };

public static List<string> AllPropertiesNotNull(IDictionary dictionary, string name, HashSet<object> alreadyChecked)
{
  List<string> nullValues = new List<string>();

  foreach(object key in dictionary.Keys)
  {
    object obj = dictionary[key];

    if (!alreadyChecked.Contains(obj))
    {
      string elementName = $"{name}[\"{key}\"]";
      nullValues.AddRange(AllPropertiesNotNull(obj, elementName, alreadyChecked));
    }
  }

  return nullValues;
}

public static List<string> AllPropertiesNotNull(IEnumerable enumerable, string name, HashSet<object> alreadyChecked)
{
  List<string> nullValues = new List<string>();
  int i = 0;

  foreach (object obj in enumerable)
  {
    if (!alreadyChecked.Contains(obj))
    {
      string elementName = $"{name}[{i}]";
      nullValues.AddRange(AllPropertiesNotNull(obj, elementName, alreadyChecked));
    }

    i++;
  }

  return nullValues;
}

public static List<string> AllPropertiesNotNull(object obj, string name, HashSet<object> alreadyChecked, string baseName = "")
{
  List<string> nullValues = new List<string>();
  string basePropertyName;

  if (string.IsNullOrEmpty(baseName))
  {
    basePropertyName = name;
  }
  else
  {
    basePropertyName = baseName + "." + name;
  }

  if (obj == null)
  {
    nullValues.Add(basePropertyName);
  }
  else if (!alreadyChecked.Contains(obj))
  {
    alreadyChecked.Add(obj);

    if (!excludedTypes.Contains(obj.GetType()))
    {
      foreach (PropertyInfo property in obj.GetType().GetProperties())
      {
        object value = property.GetValue(obj);
        string propertyName = basePropertyName + "." + property.Name;

        if (value == null)
        {
          nullValues.Add(propertyName);
        }
        else
        {
          if (typeof(IDictionary).IsAssignableFrom(property.PropertyType))
          {
            nullValues.AddRange(AllPropertiesNotNull((IDictionary)value, propertyName, alreadyChecked));
          }
          else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
          {
            nullValues.AddRange(AllPropertiesNotNull((IEnumerable)value, propertyName, alreadyChecked));
          }
          else
          {
            nullValues.AddRange(AllPropertiesNotNull(value, property.Name, alreadyChecked, basePropertyName));
          }
        }
      }
    }
  }

  return nullValues;
}

I wrote some classes to test with:

class A
{
  public string s1 { set; get; }
  public string s2 { set; get; }
  public int i1 { set; get; }
  public int? i2 { set; get; }
  public B b1 { set; get; }
  public B b2 { set; get; }
}

class B
{
  public string s1 { set; get; }
  public string s2 { set; get; }
  public int i1 { set; get; }
  public int? i2 { set; get; }
  public A a1 { set; get; }
  public Dictionary<int, string> d1 { set; get; }
  public List<A> l1 { set; get; }
}

and tested it as follows:

A a = new A
{
  s1 = "someText"
};
B b = new B
{
  s1 = "someText",
  a1 = a,
  d1 = new Dictionary<int, string>
  {
    { 1, "someText" },
    { 2, null }
  },
  l1 = new List<A>{ null, new A { s1 = "someText" } , a }
};
a.b1 = b;
Console.WriteLine(string.Join("\n", AllPropertiesNotNull(a, nameof(a), new HashSet<object>())));

Output:

a.s2
a.i2
a.b1.s2
a.b1.i2
a.b1.d1["2"]
a.b1.l1[0]
a.b1.l1[1].s2
a.b1.l1[1].i2
a.b1.l1[1].b1
a.b1.l1[1].b2
a.b2

Few points to take note of:

  1. Only public properties are considered, use BindingFlags if you want to consider non-public ones.
  2. Some types might need to be individually considered (e.g.: string) or maybe not (depending on your own case).
  3. As mentioned before, the code loops on dictionaries and enumerables and checks every value for them too. You might or might not want that (depending on your own case).

Upvotes: 1

Related Questions