-2

I'm trying to create a nested Guard method (or find an existing Guard library) that would throw an exception if any object in a nested hierarchy is NULL and include the name of the object hierarchy path up to and including NULL. I also want to avoid fluent API syntax of checking each object. Please assume example below.

Assume the following hierarchy:

Country.State.City.Person

Example of usage I'm trying to achieve:

Guard.ThrowIfNull(Country.State.City.Person);

or

Guard.ThrowIfNull(() => Country.State.City.Person);

EXPECTED RESULT:

if Person is NULL, then throw ArgumentNullException("Country.State.City.Person")

if City is NULL, then throw ArgumentNullException("Country.State.City")

if State is NULL, then throw ArgumentNullException("Country.State")

if Country is NULL, then throw ArgumentNullException("Country")

AlexVPerl
  • 7,652
  • 8
  • 51
  • 83
  • 2
    `Guard.ThrowIfNull(Country.State.City.Person);` is not going to work.... About the second one... I am wondering if an expression might help you out here.... – Stefan Nov 15 '19 at 00:32

1 Answers1

2

At minimum, I believe you'll need to pass an instance to the Guard.ThrowIfNull function in addition to the path you want to check.

I wrote a proof of concept below that exhibits the behavior you describe. It does not handle collections, however. (In the case that State is a collection, this code will fail for "Country.State.City".)

Usage:

Guard.ThrowIfNull(country, "Country.State.City.Person");
// Or
Guard.ThrowIfNull(country, x => x.State.City.Person);

Code:

public static class Guard
{
    /// <summary>
    /// Throws an ArgumentNullException if any portion of a given property path is null.
    /// </summary>
    /// <typeparam name="T">The type to check.</typeparam>
    /// <param name="instanceToCheck">The instance to check</param>
    /// <param name="pathToCheck">The full path of the property to check. The path should include the name of the instance type.</param>
    public static void ThrowIfNull<T>(T instanceToCheck, string pathToCheck)
    {       
        var propsToCheck = pathToCheck?.Split('.');
        if (propsToCheck?.Any() != true)
        {
            throw new ArgumentNullException(nameof(pathToCheck));
        }

        if (typeof(T).Name != propsToCheck.First())
        {
            throw new ArgumentException($"The type of {nameof(instanceToCheck)} does not match the given {nameof(pathToCheck)}.");
        }

        if (instanceToCheck == null)
        {
            throw new ArgumentNullException($"{pathToCheck.First()}");
        }

        object currentObj = instanceToCheck;
        for (var i = 1; i < propsToCheck.Length; i++)
        {
            var prop = currentObj.GetType().GetProperties().FirstOrDefault(x => x.Name == propsToCheck[i]);
            if (prop == null)
            {
                throw new ArgumentException($"The path, '{string.Join(".", propsToCheck.Take(i + 1))}' could not be found.");
            }

            currentObj = prop.GetValue(currentObj);
            if (currentObj == null)
            {
                throw new ArgumentNullException($"{string.Join(".", propsToCheck.Take(i + 1))}");
            }
        }
    }

    /// <summary>
    /// Throws an ArgumentNullException if any portion of a given property path is null.
    /// </summary>
    /// <typeparam name="T">The type to check.</typeparam>
    /// <param name="instanceToCheck">The instance to check</param>
    /// <param name="pathToCheck">The full path of the property to check.</param>
    public static void ThrowIfNull<T, TProp>(T instanceToCheck, Expression<Func<T, TProp>> pathToCheck)
    {
        ThrowIfNull(instanceToCheck, (typeof(T).Name + pathToCheck.ToString().Substring(pathToCheck.ToString().IndexOf("."))));
    }
}
Kei
  • 1,026
  • 8
  • 15