Define notnull constraint

In C#, it is possible to define constraints on type parameters. These inform the compiler about the capabilities a type argument must have. As a result, developers are guided in both usage as in code intent.

If the generically typed argument is checked for not being null (in all code paths), it is safe to assume that the notnull (or class) constraint can be added, which should improve the code.

using System;

namespace DefineNotNullConstraint;

class NonCompliant
{
    public void ThrowArgumentNull<TModel>(TModel obj) // Noncompliant {{TModel can be constrained to 'notnull'}}
    //                            ^^^^^^
    {
        obj = obj ?? throw new ArgumentNullException(nameof(obj));
    }

    public void ThrowIfNull<T>(T obj) // Noncompliant
    {
        ArgumentNullException.ThrowIfNull(obj);
    }

    public void GuardClass<T>(T obj) // Noncompliant
    {
        DefineNotNullConstraints.Guard.NotNull(obj);
    }

    public void GuardMethod<T>(T obj) // Noncompliant
    {
        Guard(obj);
    }    

    private T Guard<T>(T? obj) => obj ?? throw new ArgumentNullException();
}

class Compliant
{
    public void Nullable<T>(T? obj) // Compliant
    {
        obj = obj ?? throw new ArgumentNullException(nameof(obj));
    }

    public void Class<T>(T obj) where T : class // Compliant
    {
        obj = obj ?? throw new ArgumentNullException(nameof(obj));
    }

    public void NotNull<T>(T obj) where T : notnull // Compliant
    {
        obj = obj ?? throw new ArgumentNullException(nameof(obj));
    }

    public void Multiple<T>(T required, T? opti0nal) // Complaint
    {
        required = required ?? throw new ArgumentNullException(nameof(required));
    }
}

static class Guard
{
    public static T NotNull<T>(T? obj) => obj ?? throw new ArgumentNullException();
}

Any caller that is generic will have to add the constraint too (possibly at multiple levels), and if the constraint only applies in the situation where the modified method is called and not always, there is no good solution. Even if code verifies that the constraint holds at run time, C# does not provide an easy way to “bridge” to a constrained method.

For example if you have a public void MultiMode<T>(Mode mode, T foo) with the restriction that foo must not be null if mode is Mode.Mode1 but can be null if mode is Mode.Mode2.

In some of these situations, the [DisallowNull] attribute may be helpful since it can be satisfied with a simple null check and does not require a generic caller to modify its constraints.

By the way, ArgumentNullException.ThrowIfNull has some optimizations that cannot be replicated in user code, so when not stuck on .NET Framework it would be best to use it and not create your own.

The whole point of notnull is of course to acknowledge for the fact that the code can not be null, so that you have to add other constraints is exactly the point. It is the other way arround, I would argue. In your example:


public void MultiMode<T>(Mode mode, T? foo) where T : notnull
{
    if(foo is { }}
    {
        // call code with notnull constriant
    }
} 

This might be extra work, but is not often needed, and it helps a lot as code that you want to call will simply throw if you call it. Better catch it upfront.

By the way, ArgumentNullException.ThrowIfNull has some optimizations that cannot be replicated in user code, so when not stuck on .NET Framework it would be best to use it and not create your own.

That might be true, but is not the relevant here. A rule like this should detect also other ways argument null checking.

Admittedly a rare situation, but this particular example will break if T is a value type and null is supposed to be detectable, since T? is the same at run time as T if T is not constrained to be a value type. With the unconstrained version, it worked correctly (although another Sonar rule may warn that the null check is never true for some possibilities of T).

I don’t mean to say that the notnull constraint should never be used, but that it should not be added blindly. If something is often called from a generic context, the necessary changes may be big and limitations of C# may become problematic. If all callers specify a concrete type for the type parameter, constraints are fine.

The [DisallowNull] attribute is less likely to cause problems, but can be used in fewer situations.

You could argue that [DisallowNull] should also be a good way to resolve the issue.

Hi @Corniel,

Thanks for the detailed write-up and the code examples — this is a well-formed rule idea.

The core premise is sound: if a type parameter T is guarded against null on every code path (via ??, ThrowIfNull, a guard method, etc.), the notnull constraint makes that contract explicit at the API boundary and shifts the check to compile time for constrained callers.

A few thoughts on the implementation challenges that would shape how this rule works in practice:

Determining “always null-checked” requires flow analysis. The rule can’t just look for a single guard call — it needs to confirm that all code paths through the method result in a null check or throw. That’s non-trivial for methods with branching, early returns, or guards hidden inside called helper methods.

Caller impact. As @jilles-sg noted, adding notnull cascades to callers. For methods used primarily with concrete types this is a no-op, but for deeply generic call chains it can require widespread changes. This makes the rule better suited as a suggestion (low severity) rather than a warning, and the fix should be marked as potentially breaking.

[DisallowNull] as an alternative fix. You both touched on this — [DisallowNull] achieves similar null-safety documentation without requiring callers to change their constraints. A complete rule implementation could offer both as fix options (or detect which is more appropriate based on context).

Value types. where T : notnull covers both reference and value types, whereas where T : class is reference-only. The rule would need to be careful not to suggest notnull when the intent is specifically a reference-type constraint.

We don’t currently have this rule, and I’ve noted it as a rule idea on our side. We can’t commit to a timeline, but the feedback and examples here are useful input if we pick it up.

1 Like

Given the low impact solution [DisallowNull], I would argue that a warning would just be fine.