Skip to Content
Course content

229: Nullable Annotations on APIs

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

Imagine you're ordering from a restaurant menu. Most items are straightforward: "Cheeseburger" means you get a cheeseburger. But then you see "Seasonal Soup (subject to availability)." That little parenthetical is a contract. It's the restaurant telling you, "Look, we'll try to give you soup, but there's a real chance the waiter is going to come back and tell you we're out." Because of that warning, you don't just sit there blindly expecting soup; you have a backup plan—maybe you'll order a side salad instead.

In C#, nullable annotations on your public APIs are exactly like that "subject to availability" note. Without them, a developer using your library is guessing. They see a method that returns a string and wonder, "Will this actually return null if the record isn't found, or will it throw an exception? Do I need to wrap this in a null check, or am I being overly cautious?"

The Contract Between You and Your Caller

When you're writing a library or a service that other people (or even just other teams) will use, your method signatures are your primary form of communication. By using Nullable Reference Types (NRTs), you move the conversation from "I hope the documentation is up to date" to "the compiler will literally tell you if you're forgetting something."

Take this simple API for a user profile service. Notice how the annotations change the expectations for the person calling the code:

public class UserProfileApi
{
    // The '?' tells the caller: "Prepare for the possibility that this user doesn't exist."
    public User? GetUserById(int id) 
    {
        return _database.Users.Find(id);
    }

    // No '?' here. The caller can trust that they will ALWAYS get a string back.
    public string GetSystemVersion() 
    {
        return "v2.4.1";
    }
}

If I'm the developer consuming this API, the User? tells me I must handle the null case. If I try to access GetUserById(123).Name without a check, the compiler will give me a warning. Meanwhile, I can call GetSystemVersion().Length with total confidence. I've offloaded the mental burden of "what if this is null?" onto the type system.

Signaling Intent with the Question Mark

It's not just about return values; it's about what you're asking for in your parameters. This is where I see a lot of developers slip up. If your method can handle a null input, mark it as nullable. If a null input will cause the method to fail or throw an ArgumentNullException, keep it non-nullable.

I've spent way too many hours debugging production crashes because someone passed a null into a "required" field that wasn't marked as such. Here is how you should distinguish them:

public void UpdateUserEmail(int id, string newEmail) 
{
    // The caller knows newEmail MUST be provided.
    // I don't even need to check for null if I trust my static analysis.
    _database.Users.UpdateEmail(id, newEmail);
}

public void UpdateUserBio(int id, string? bio) 
{
    // The '?' tells the caller: "It's okay to pass null if you want to clear the bio."
    _database.Users.UpdateBio(id, bio);
}

The Danger of Lying to Your Users

Here is my biggest piece of advice: Don't lie in your signatures.

It's tempting to mark a return type as string just to avoid making the caller deal with nulls. But if your code actually returns null at runtime, you've just created a landmine. The caller's compiler told them it was safe, so they didn't write a null check, and now they're hitting a NullReferenceException in production. That's actually worse than not using annotations at all, because you've given them a false sense of security.

If there is even a 1% chance that a value could be null, mark it with a ?. It is always better to force a developer to handle a null they didn't expect than to let them ignore a null that eventually crashes their app.




📋 Practical Task

Refactoring the LibraryCatalog API for Null Safety

You have been handed a legacy LibraryCatalog class. Currently, it uses standard reference types, leaving the API consumers guessing about nullability. Your task is to apply nullable annotations to the API to make the contracts explicit.

Requirements:

  • GetBookByIsbn(string isbn): Should be marked as returning a nullable Book? because a book might not exist in the catalog.
  • GetCategoryName(int categoryId): Should return a non-nullable string because the system guarantees every category ID has a name.
  • SearchBooks(string? query): The query parameter should be nullable, as passing null should be interpreted as "return all books."
  • AddBook(Book book): The book parameter must be non-nullable; the method cannot function without a book object.
public class Book { public string Title { get; set; } }

public class LibraryCatalog
{
    public Book GetBookByIsbn(string isbn) 
    { 
        /* implementation */ 
        return null; 
    }

    public string GetCategoryName(int categoryId) 
    { 
        /* implementation */ 
        return "Fiction"; 
    }

    public List<Book> SearchBooks(string query) 
    { 
        /* implementation */ 
        return new List<Book>(); 
    }

    public void AddBook(Book book) 
    { 
        /* implementation */ 
    }
}

Modify the signatures of the LibraryCatalog class to correctly reflect these API contracts using nullable annotations.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.