Skip to Content
Course content

231: Nullable Attributes: NotNull, MaybeNull

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're managing a high-end warehouse. Normally, every box has a standard label: "Contains Glass" or "Standard Cargo." Your staff knows exactly how to handle them based on those labels. But every now and then, you get a weird shipment. Maybe a box is labeled "Standard Cargo," but you happen to know from the supplier's email that it actually contains something fragile. Or, you have a box that looks like it's empty, but you've personally verified there's a tiny, high-value chip inside.

In these cases, the standard label isn't enough. You need a "special handling" sticker—something that overrides the standard label to tell your staff, "I know what the label says, but treat this differently."

That is exactly what NotNull and MaybeNull attributes do for the C# compiler. The "standard labels" are your nullable reference types (string? vs string). The "special stickers" are these attributes, which you use when the compiler's static analysis isn't smart enough to understand the reality of your code.

When the Compiler Stops Trusting You

Most of the time, the C# compiler is great at tracking nulls. But there are "blind spots." This usually happens in complex methods where the return value depends on logic the compiler can't follow, or when you're dealing with legacy APIs. You'll start seeing those annoying warnings: "Possible null reference return."

You could just use the null-forgiving operator (!), but that's a blunt instrument. Attributes are more surgical. They don't just silence a warning; they provide a contract for anyone calling your method.

The 'Trust Me, It's Actually There' Guarantee

Let's look at [NotNull]. Use this when you have a parameter that the compiler thinks could be null, but if the method returns a certain value (like true), that parameter is guaranteed to be non-null from that point forward.

I see this all the time in "Try" patterns. Check this out:

using System.Diagnostics.CodeAnalysis;

public class UserSession
{
    private string? _sessionToken;

    // We tell the compiler: "If this returns true, 'token' is definitely not null"
    public bool TryGetToken([NotNull] out string? token)
    {
        if (_sessionToken != null)
        {
            token = _sessionToken;
            return true;
        }

        token = null;
        return false;
    }
}

// Now, the consumer gets a better experience:
var session = new UserSession();
if (session.TryGetToken(out var token))
{
    // The compiler knows 'token' is not null here because of the [NotNull] attribute.
    // No warning when calling token.Length!
    Console.WriteLine(token.Length); 
}

Without that attribute, the compiler would still see token as string? inside that if block, and you'd be forced to use token!.Length. It's much cleaner to just tell the compiler how the method actually behaves.

Admitting the Type System is Lying

Then we have [MaybeNull]. This is the opposite. You use this when your method signature claims to return a non-nullable type (string), but there are specific edge cases where it might actually return null, and you want to warn the caller (and the analyzer) about it.

I'll be honest: you won't use this as often as NotNull, but it's a lifesaver when wrapping old libraries that don't have nullable annotations. It's like saying, "The documentation says this is a string, but in the real world, it might be null, so don't trust it blindly."

using System.Diagnostics.CodeAnalysis;

public class LegacyDatabaseWrapper
{
    // The signature says it returns a non-nullable string, 
    // but the [MaybeNull] attribute warns us it might still be null.
    [MaybeNull]
    public string GetOldSetting(string key)
    {
        // Imagine some ancient C++ interop happening here 
        // that returns null if the key isn't found.
        return NativeMethods.FetchSetting(key); 
    }
}

By adding [MaybeNull], you're being honest with the rest of your team. You're signaling that while the type system is optimistic, the reality is a bit messier.




📋 Practical Task

Hardening a Legacy User Profile Cache

You've inherited a UserProfileCache class. The compiler is throwing warnings because it can't figure out the nullability of the out parameter in the TryGetProfile method, and it's blindly trusting a legacy FetchRawData method that is known to occasionally return nulls despite its return type.

Your Task:

  • Add the [NotNull] attribute to the profile parameter in TryGetProfile so that the caller doesn't get a warning when accessing the profile after a successful return.
  • Add the [MaybeNull] attribute to the FetchRawData method to signal that the returned string might actually be null.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;

public class UserProfile 
{ 
    public string Name { get; set; } = ""; 
}

public class UserProfileCache
{
    private Dictionary<int, UserProfile> _cache = new();

    // TODO: Apply the correct attribute here
    public bool TryGetProfile(int id, out UserProfile? profile)
    {
        if (_cache.TryGetValue(id, out var p))
        {
            profile = p;
            return true;
        }
        profile = null;
        return false;
    }

    // TODO: Apply the correct attribute here
    public string FetchRawData(int id)
    {
        // Simulating a legacy call that might return null
        return id == 0 ? null! : "User Data";
    }
}

public class Program
{
    public static void Main()
    {
        var cache = new UserProfileCache();
        
        if (cache.TryGetProfile(1, out var profile))
        {
            // This should not produce a warning after your fix
            Console.WriteLine(profile.Name);
        }
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.