Search code examples
c#reflectionnullreferenceexception

Is there a generic way to recursively check a deserialized object for null fields?


I receive a number of different messages that I deserialize into very dissimilar objects

Most of these objects have non-nullable fields, but across all the message types (From GraphQL, Azure service- and storage-bus, to CosmosSB and probably more that I am forgetting) some of them are happy to deserialize into objects with null fields anyway.

As such, I could very much use a component that will let me do

T objectFromMessage = GetObject(message);
FieldAsserter.AssertAllFieldsSet<T>(objectFromMessage); //Throws if any non-nullable fields are null

Do you know if any such field validation method already exists, either by default or somewhere in Nuget? Alternatively, if I have to write it myself, can you give me any clues re. where to start looking?

What did you try and what were you expecting?

Nothing really, at this point - A cursory search of NuGet and a few thoughts about reflection is as far as I got before thinking "Maybe SO has an answer."

There are a couple of similar questions on SO already, but they appear to work for single objects - the answers are of type "write an extension method to your class that does it" but I would like a generic method that'll work for anything - I have too many message types.


Solution

  • As an alternative, if you only want to check public properties that are NOT nullable (e.g. string rather than string?) you can filter out the nullable properties and check only the non-nullable ones like so:

    (NOTE: This requires .NET 6.0 or later.)

    public static void RequiresPropertiesNotNull(object item, string name)
    {
        if (item is null)
            throw new ArgumentNullException(nameof(item), "Item being checked cannot be null.");
    
        name ??= "<Unknown>"; // Don't want to throw if name is null, because it would hide the thing we're checking for nulls.
    
        var allProps = item.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
    
        var nonNullableProps =
            from prop in allProps
            where
                prop.CanRead
             && !prop.PropertyType.IsValueType
             && !IsNullable(prop)
            select prop;
    
        foreach (var prop in nonNullableProps)
        {
            object? value = prop.GetValue(item);
    
            if (value is null)
                throw new InvalidOperationException($"Property {prop.Name} for {name} of type {item.GetType().FullName} cannot be null.");
            
            RequiresPropertiesNotNull(value, name);
        }
    }
    
    public static bool IsNullable(PropertyInfo prop)
    {
        var context = new NullabilityInfoContext();
        var info    = context.Create(prop);
    
        return info.WriteState is NullabilityState.Nullable;
    }
    

    Note that this is recursive, so it will check all the public properties and (recursively) all of their public properties and so on.

    It does NOT check for self-referential objects, so if you try to use this to check an object graph that contains cycles, you WILL get a stack overflow error.

    You might use it as follows: Given class Outer and class Inner where the Inner class contains a property CanBeNull which is allowed to be null and an InnerName property that is not allowed to be null:

    public sealed class Outer
    {
        public string OuterName { get; set; }
        public Inner  Inner     { get; set; }
    }
    
    public sealed class Inner
    {
        public string  InnerName { get; set; }
        public string? CanBeNull { get; set; }
    }
    

    Then in the following code, the second call to RequiresPropertiesNotNull() will throw an exception:

    public static void Main()
    {
        var innerOk  = new Inner { InnerName = "InnerOK" };
        var innerBad = new Inner();
    
        var outerOk  = new Outer { OuterName = "OuterOK",  Inner = innerOk  };
        var outerBad = new Outer { OuterName = "OuterBad", Inner = innerBad };
    
        RequiresPropertiesNotNull(outerOk,  nameof(outerOk));  // OK
        RequiresPropertiesNotNull(outerBad, nameof(outerBad)); // Throws
    }
    

    If you want to remove the recursive part, just remove the line of code RequiresPropertiesNotNull(value, name); in the RequiresPropertiesNotNull() implementation.