Intermediate

C-Sharp Design Patterns Null Object

This course is part of a series of training courses on design patterns in C#. The central objective is to learn the Null Object design pattern: when to use it and how to apply it correctly.

Twitter: @ElegantCoder


Table of Contents

  1. Course Overview
  2. Introduction to the Null Object pattern
  1. Demonstration: Code before the pattern
  1. Demonstration: Code after the pattern
  1. Before/after comparison
  2. When to use the Null Object Pattern?
  3. Summary and good practices

1. Course Overview

This course is part of a series of training courses on design patterns in C#. The central objective is to learn the Null Object design pattern: when to use it and how to apply it correctly.

Course Promise

The promise is simple and bold: you can stop writing null checks. Thanks to this pattern, it is possible to write far fewer null checks while remaining completely safe.

Prerequisites

  • Basic knowledge of C# or other object-oriented language

Topics covered

  • The origins of null in programming languages
  • Why null checks invade codebases
  • How the modern IDE encourages writing null checks (wrongly)
  • How to eliminate the need for null checks using the Null Object Pattern

2. Introduction to the Null Object pattern

2.1 What is a design pattern?

A design pattern is essentially a known solution to a recurring and common problem. Patterns are useful for two main reasons:

  1. Communication: they provide a shared vocabulary between developers. When we say “I’m using a Null Object Pattern here”, any developer familiar with patterns immediately understands the intent.
  2. Problem Solving: They address problems that are encountered again and again in the code, providing a proven solution rather than an ad hoc solution reinvented each time.

2.2 The origins of null: the “billion-dollar mistake”

To talk about null, we need to introduce Sir Tony Hoare, a computer scientist whose contributions to the field are legendary. He is particularly renowned for his invention of the Quicksort algorithm, but his contributions go well beyond that.

He is the one who created the null reference in 1965. While working on creating new languages, he decided to expose a null memory location to the programmer — and thus the null reference was born.

Tony Hoare himself refers to this decision as his “billion-dollar mistake.” This figure represents the money wasted debugging, finding and fixing null reference issues, in terms of time and money wasted globally over decades.

“I call it my billion-dollar mistake. It was the invention of the null reference in 1965.” — Sir Tony Hoare

2.3 Problems caused by null in code

How often do we write this type of code, particularly in view constructors for web applications, or in controllers?

public class PersonView
{
    private readonly Person _person;

    public PersonView(Person person)
    {
        if (person == null) throw new ArgumentNullException();
        
        _person = person;
    }
}

A null check is practically a requirement when accepting nullable arguments in functions or constructors. It has become a reflex, often supported by the IDE (Visual Studio automatically suggests adding null checks).

And if a property of an object can also be null, we quickly end up with this:

public class PersonView
{
    private readonly Person _person;

    public PersonView(Person person)
    {
        if (person == null) throw new ArgumentNullException();
        if (person.FirstName == null) throw new ArgumentNullException();
        if (person.LastName == null) throw new ArgumentNullException();
        
        _person = person;
    }
}

Our code is getting more and more complex, and we haven’t written anything useful yet.

The program that calls Person.Render may not know that the Person object needed for rendering has not been initialized. This is the perfect recipe for a NullReferenceException to be thrown by the PersonView class, or for having to implement complex workarounds.

2.4 Cyclomatic complexity

Each null check increases a metric known as cyclomatic complexity. It is simply a measure of the number of logical branches in the code.

The higher this number, the worse the code. From just two null checks, we already have a cyclomatic complexity of 2, and we haven’t written anything useful yet.

Cyclomatic complexity negatively impacts:

  • The readability of the code
  • Maintainability
  • Testability (each branch should ideally be covered by a test)
  • Understanding of the code by a new developer

2.5 Presentation of the Null Object Pattern

What would happen if there was a default implementation of the Person class? Let’s call it NullPerson. This NullPerson would still be used in place of a null value, instead of letting that value be null in the PersonView.

In other words, PersonView can use the NullPerson type in its Render method without ever fearing a NullReferenceException.

Here is the essence of the Null Object Pattern:

Instead of returning null when an object cannot be found, we return a “null” object — that is, an object that implements the same interface as the expected object, but with harmless default values.

The trainer notes with humor that he finds this pattern better named “Default Object Pattern” (default object pattern), because what is returned is actually an object with default values. But the official name is “Null Object Pattern” and it is under this name that all developers will recognize it.

Concept illustrated with Person:

// Avant : code appelant avec null check obligatoire
PersonView view = new PersonView(person); // peut crasher si person == null

// Avec le Null Object Pattern :
// - Si la personne est trouvée → on retourne l'objet Learner réel
// - Si la personne n'est pas trouvée → on retourne NullPerson avec des valeurs par défaut
// Dans les deux cas, l'objet retourné n'est JAMAIS null

With this approach:

  • No more need for null checks in consumer code
  • Cyclomatic complexity decreases
  • Code readability increases
  • Fewer lines of code are required

2.6 Implementation by interface or by inheritance

There are two ways to implement a Null Object:

By inheritance

public class Person
{
    public virtual string FirstName { get; set; }
    public virtual string LastName { get; set; }
    public virtual void Render() { }
}

public class NullPerson : Person
{
    public override string FirstName => "David";  // valeur par défaut
    public override string LastName => "Starr";   // valeur par défaut
    public override void Render() { /* ne fait rien */ }
}

By interface (instructor’s preferred method)

public interface IPerson
{
    string FirstName { get; }
    string LastName { get; }
}

public class Person : IPerson
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class NullPerson : IPerson
{
    public string FirstName => "David";   // toujours retourne quelque chose
    public string LastName => "Starr";    // toujours retourne quelque chose
}

Both approaches typically give the same result. The interface method is preferred because:

  • It produces shallower code (fewer indentation levels)
  • It is less complex to read
  • It avoids inheritance hierarchy

3. Demonstration: Code before pattern

The demonstration scenario is as follows: we emulate a typical MVC pattern of a website. A learner is browsing the Pluralsight site, and we want to retrieve information about this learner to display it in the upper right corner (where we normally click to access their account).

The views in this demo do not display web pages — they print directly to the command line for simplicity.

3.1 Project structure

NullObjectPattern/
├── NullObjectPattern.sln
├── global.json
└── NullObject/
    ├── NullObject.csproj
    ├── Program.cs
    ├── Entities/
    │   ├── ILearner.cs
    │   └── Learner.cs
    ├── Services/
    │   └── LearnerService.cs
    └── View/
        └── LearnerView.cs

Project configuration (NullObject.csproj):

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>netcoreapp3.0</TargetFramework>
    </PropertyGroup>
</Project>

3.2 The ILearner interface

The ILearner interface defines the contract that all implementations of a learner must respect:

namespace NullObject.Entities
{
    public interface ILearner
    {
        int Id { get; }
        string UserName { get; }
        int CoursesCompleted { get; }
    }
}

This interface exposes three properties:

  • Id: the unique identifier of the learner
  • UserName: the user name
  • CoursesCompleted: the number of courses completed

3.3 The Learner class

The Learner class implements the ILearner interface and represents a real learner in the system:

using System.Diagnostics.CodeAnalysis;

namespace NullObject.Entities
{
    public class Learner : ILearner
    {
        public Learner(int id, string userName, int coursesCompleted)
        {
            Id = id;
            UserName = userName;
            CoursesCompleted = coursesCompleted;
        }

        public int Id { get; private set; }

        public string UserName { get; private set; }

        public int CoursesCompleted { get; private set; }
    }
}

This class:

  • Takes id, userName and coursesCompleted as constructor parameters
  • Expose these values via read-only properties (private setter)
  • Implements ILearner correctly

3.4 The LearnerService service (before)

The LearnerService is responsible for retrieving the current learner. It also contains an internal LearnerRepo (nested class) which simulates an in-memory database:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using NullObject.Entities;

namespace NullObject.Services
{
    public class LearnerService
    {
        readonly LearnerRepo _repo = new LearnerRepo();
        
        public ILearner GetCurrentLearner()
        {
            // go get the Learner's id from a JWT token cookie
            // or by some other appropriate means
            
            int learnerId = 1;
            
            var learner = _repo.GetLearner(learnerId);
            
            if (learner == null) throw new NullReferenceException();

            return learner;
        }

        class LearnerRepo
        {
            readonly IList<Learner> _learners = new List<Learner>();

            internal LearnerRepo()
            {
                _learners.Add(new Learner(1, "David", 83));
                _learners.Add(new Learner(2, "Julie", 72));
                _learners.Add(new Learner(3, "Scott", 92));
            }

            internal ILearner GetLearner(int id)
            {
                bool learnerExists = _learners.Any(l => l.Id == id);

                if (learnerExists)
                    return _learners.FirstOrDefault(l => l.Id == id);

                return null;
            }
        }
    }
}

Important points:

  • Comments represent code that would normally read the learner ID from a JWT token or other authentication mechanism
  • The internal LearnerRepo simulates a mini-database with three learners (David, Julie, Scott)
  • If the learner is not found (learnerExists == false), the repo returns null
  • The service checks if the result is null and throws a NullReferenceException in this case
  • With learnerId = 1 everything works fine (David with 83 courses)

3.5 The LearnerView (before)

The LearnerView is responsible for displaying learner information:

using System;
using NullObject.Entities;

namespace NullObject.View
{
    public class LearnerView
    {
        private readonly ILearner _learner;

        public LearnerView(ILearner learner)
        {
            if(learner == null) throw new ArgumentNullException();
            if(learner.UserName == null) throw new ArgumentNullException();
            
            _learner = learner;
        }

        public void RenderView()
        {
            Console.WriteLine("User Name: " + _learner.UserName);
            Console.WriteLine("Courses Completed: " + _learner.CoursesCompleted);
        }
    }
}

Problem identified: Constructor contains two null checks:

  1. Checking that learner itself is not null
  2. Checking that learner.UserName is not null

These checks increase cyclomatic complexity and make the code heavier — without this being useful business logic.

3.6 The main program (before)

using System;
using NullObject.Entities;
using NullObject.Services;
using NullObject.View;

namespace NullObject
{
    class Program
    {
        static void Main(string[] args)
        {
            LearnerService learnerService = new LearnerService();
            ILearner learner = learnerService.GetCurrentLearner();
            
            LearnerView view = new LearnerView(learner);
            view.RenderView();
        }
    }
}

The flow is simple:

  1. Instantiation of LearnerService
  2. Call to GetCurrentLearner() which returns an ILearner
  3. Passing the learner to LearnerView
  4. Calling RenderView() to display information

With learnerId = 1:

User Name: David
Courses Completed: 83

With learnerId = 2:

User Name: Julie
Courses Completed: 72

With learnerId = 3:

User Name: Scott
Courses Completed: 92

3.7 Troubleshooting

Now, what happens if someone browses the site without being logged in? The learner ID will probably be -1 (sentinel value indicating “no user logged in”). Let’s change the learnerId to -1:

// Dans LearnerService.GetCurrentLearner()
int learnerId = -1; // simule un utilisateur non connecté

Result: A NullReferenceException is thrown, because no learner with ID -1 exists in the repo, so GetLearner(-1) returns null, and the service throws the exception.

The formatter identifies three places in the code where null-related exceptions can be thrown:

  1. In LearnerService.GetCurrentLearner(): if (learner == null) throw new NullReferenceException();
  2. In LearnerView: if(learner == null) throw new ArgumentNullException();
  3. In LearnerView: if(learner.UserName == null) throw new ArgumentNullException();

These failure points are a direct result of defensive handling around null. This is the problem that the Null Object Pattern will solve.


4. Demonstration: Code after the pattern

The transformation consists of:

  1. Create a NullLearner class that implements ILearner with default values
  2. Modify LearnerService to return NullLearner instead of null
  3. Remove all null checks from LearnerView

4.1 Creating the NullLearner class

By convention, the Null Object is named with the prefix Null followed by the name of the class or interface it represents (NullLearner for ILearner/Learner).

namespace NullObject.Entities
{
    public class NullLearner : ILearner
    {
        public int Id { get; } = -1;

        public string UserName => "Just Browsing";

        public int CoursesCompleted { get; } = 0;
    }
}

Parsing NullLearner:

PropertyDefaultRationale
Id-1Sentinel value indicating “no real user”
UserName"Just Browsing"Clear and identifiable message, will never be null
CoursesCompleted0No courses completed for unidentified user

Key points:

  • The class implements ILearner, so it is polymorphically compatible with any code that expects an ILearner
  • All properties return always a value — never null
  • The name NullLearner is the standard convention for this pattern

The instructor recommends placing NullLearner in its own file, following C# conventions.

4.2 The LearnerService service (after)

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using NullObject.Entities;

namespace NullObject.Services
{
    public class LearnerService
    {
        readonly LearnerRepo _repo = new LearnerRepo();
        
        public ILearner GetCurrentLearner()
        {
            // go get the Learner's id from a JWT token cookie
            // or by some other appropriate means
            
            int learnerId = 0;

            return _repo.GetLearner(learnerId); 
        }

        class LearnerRepo
        {
            readonly IList<Learner> _learners = new List<Learner>();

            internal LearnerRepo()
            {
                _learners.Add(new Learner(1, "David", 83));
                _learners.Add(new Learner(2, "Julie", 72));
                _learners.Add(new Learner(3, "Scott", 92));
            }

            internal ILearner GetLearner(int id)
            {
                bool learnerExists = _learners.Any(l => l.Id == id);

                if (learnerExists)
                    return _learners.FirstOrDefault(l => l.Id == id);

                return new NullLearner();
            }
        }
    }
}

Important changes:

  1. In GetCurrentLearner: the code is simplified. No more if (learner == null) throw new NullReferenceException(). We directly return the result of the repo.

  2. In LearnerRepo.GetLearner: instead of return null; when the learner does not exist, we now return return new NullLearner();.

  3. learnerId = 0 is used to simulate a user not logged in (no learner with ID 0 exists in the repo).

The LearnerRepo never returns null — it returns either an actual Learner or a NullLearner. This is the fundamental guarantee of the Null Object Pattern.

4.3 The LearnerView (after)

using System;
using NullObject.Entities;

namespace NullObject.View
{
    public class LearnerView
    {
        private readonly ILearner _learner;

        public LearnerView(ILearner learner)
        {
            _learner = learner;
        }

        public void RenderView()
        {
            Console.WriteLine("User Name: " + _learner.UserName);
            Console.WriteLine("Courses Completed: " + _learner.CoursesCompleted);
        }
    }
}

Radical transformation of the manufacturer:

Before:

public LearnerView(ILearner learner)
{
    if(learner == null) throw new ArgumentNullException();
    if(learner.UserName == null) throw new ArgumentNullException();
    
    _learner = learner;
}

After:

public LearnerView(ILearner learner)
{
    _learner = learner;
}

The two null checks have disappeared! The constructor now only does one thing: assign the learner. This is possible because:

  • We have the guarantee that learner will never be null (it is either a real Learner or a NullLearner)
  • We have the guarantee that learner.UserName will never be null (the NullLearner always returns "Just Browsing")

4.4 The main program (after)

using System;
using NullObject.Entities;
using NullObject.Services;
using NullObject.View;

namespace NullObject
{
    class Program
    {
        static void Main(string[] args)
        {
            LearnerService learnerService = new LearnerService();
            ILearner learner = learnerService.GetCurrentLearner();
            
            LearnerView view = new LearnerView(learner);
            view.RenderView();
        }
    }
}

The Program.cs is identical before and after — this is a major advantage of the pattern. The consumer code does not change, and it does not need to know whether it is processing an actual Learner or a NullLearner. Polymorphism does all the work.

4.5 Execution result

With learnerId = 0 (user not logged in):

User Name: Just Browsing
Courses Completed: 0

No more exceptions! The NullLearner is returned by default and the view properly displays default values.

With learnerId = 1:

User Name: David
Courses Completed: 83

5. Comparaison avant / après

File structure

FileBeforeAfter
ILearner.csInterface (unchanged)Interface (unchanged)
Learner.csConcrete class (unchanged)Concrete class (unchanged)
NullLearner.cs❌ Does not exist✅ New class added
LearnerService.csThrow exception if nullReturn NullLearner if not found
LearnerView.cs2 null checks + assignmentAssignment only
Program.csSameSame

LearnerView Builder Comparison

Before (3 lines of logic, including 2 null checks):

public LearnerView(ILearner learner)
{
    if(learner == null) throw new ArgumentNullException();       // null check #1
    if(learner.UserName == null) throw new ArgumentNullException(); // null check #2
    
    _learner = learner;  // logique utile
}

After (only 1 line of logic):

public LearnerView(ILearner learner)
{
    _learner = learner;  // seule la logique utile reste
}

Comparison of LearnerRepo.GetLearner

Before (returns null):

internal ILearner GetLearner(int id)
{
    bool learnerExists = _learners.Any(l => l.Id == id);

    if (learnerExists)
        return _learners.FirstOrDefault(l => l.Id == id);

    return null; // ← source de tous les problèmes
}

After (returns NullLearner):

internal ILearner GetLearner(int id)
{
    bool learnerExists = _learners.Any(l => l.Id == id);

    if (learnerExists)
        return _learners.FirstOrDefault(l => l.Id == id);

    return new NullLearner(); // ← jamais null, toujours sûr
}

Impact on cyclomatic complexity

ComponentBeforeAfter
LearnerView constructor3 (2 branches null check + 1)1
LearnerService.GetCurrentLearner2 (1 null check)1
Total component complexity52

6. When to use the Null Object Pattern?

The Null Object Pattern is particularly appropriate in the following situations:

Ideal scenarios

  1. Unauthenticated user: As in the course example, when a user is browsing without being logged in, returning a NullUser with default values ​​is more elegant than returning null.

  2. Optional resource: when a dependency is optional (for example, a logger), a NullLogger which does nothing is preferable to null checks on each use.

  3. Configuration missing: When a configuration is not defined, a default configuration object can be used.

  4. Empty search result: when a search returns nothing, rather than null, return an “empty” object with default behaviors.

Additional Examples

Optional logger:

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message) => Console.WriteLine(message);
}

// Null Object : ne fait rien mais ne crashe pas
public class NullLogger : ILogger
{
    public void Log(string message) { /* intentionnellement vide */ }
}

// Utilisation sans null check
public class OrderService
{
    private readonly ILogger _logger;
    
    public OrderService(ILogger logger)
    {
        _logger = logger; // peut être NullLogger — aucun null check nécessaire
    }
    
    public void ProcessOrder()
    {
        _logger.Log("Processing order..."); // toujours sûr
        // logique métier
    }
}

Empty shopping cart:

public interface ICart
{
    IEnumerable<Item> Items { get; }
    decimal Total { get; }
}

public class Cart : ICart
{
    public IEnumerable<Item> Items { get; set; }
    public decimal Total => Items.Sum(i => i.Price);
}

// Null Object : panier "vide" avec comportement par défaut
public class NullCart : ICart
{
    public IEnumerable<Item> Items => Enumerable.Empty<Item>();
    public decimal Total => 0m;
}

When NOT to use this pattern

  • When null has an important business meaning that must be distinguished from a default value
  • When null object defaults could hide real errors
  • When performance is critical and object allocation must be minimized (in these cases consider a Null Object singleton)

7. Summary and best practices

What the course covered

  1. Origins of null: It was Tony Hoare who introduced the null reference in 1965, and he himself considers it his biggest mistake.

  2. The problem of null checks: Null checks multiply in the code, increase cyclomatic complexity, reduce readability and have become a quasi-automatic reflex that is often unjustified.

  3. The Null Object Pattern: Rather than returning null, we return an object that implements the same interface with harmless default values. The consumer never has to check if the object is null again.

The four key principles

1. Use the Null Object to avoid null checks

// ❌ Avant : null check obligatoire partout
if (learner != null && learner.UserName != null)
{
    // utilisation de learner
}

// ✅ Après : utilisation directe sans vérification
_learner.UserName; // toujours sûr

This simplifies the code, makes it easier to read and write, and reduces its complexity.

2. The returned object is never null

When we ask for something, we can be sure that what we receive is not null. This is a contractual guarantee provided by the pattern.

3. Use interface or inheritance depending on context

Both techniques typically give the same result. The interface technique is preferred because:

  • Code is shallower (less nested)
  • Less complexity to read
  • No strong coupling to a class hierarchy
// Par interface (recommandé)
public class NullLearner : ILearner
{
    public int Id { get; } = -1;
    public string UserName => "Just Browsing";
    public int CoursesCompleted { get; } = 0;
}

// Par héritage (également valide)
public class NullLearner : Learner
{
    public NullLearner() : base(-1, "Just Browsing", 0) { }
}

4. Cleaner code

The end result is cleaner code — always a desirable goal. Fewer lines, fewer conditional branches, more readability.

Naming convention

By convention, the Null Object is named with the prefix Null followed by the entity name:

  • NullLearner for ILearner / Learner
  • NullLogger for ILogger / Logger
  • NullUser for IUser / User
  • NullCart for ICart / Cart

This convention allows any developer to immediately recognize that this is a Null Object implementation.

Summary of demo files

FileRole
ILearner.csContract/interface common to Learner and NullLearner
Learner.csReal implementation with authentic data
NullLearner.csNull Object implementation with default values ​​
LearnerService.csService that always returns an ILearner (never null)
LearnerView.csView that consumes ILearner without any null check
Program.csEntry point — same before and after refactoring

Conceptual diagram of the pattern

              ┌──────────────┐
              │  <<interface>> │
              │   ILearner    │
              │  + Id         │
              │  + UserName   │
              │  + Courses... │
              └──────┬───────┘
                     │ implements
          ┌──────────┴──────────┐
          │                     │
   ┌──────┴──────┐      ┌───────┴──────┐
   │   Learner   │      │  NullLearner  │
   │  id=1       │      │  id=-1        │
   │  name=David │      │  name="Just   │
   │  courses=83 │      │   Browsing"   │
   └─────────────┘      │  courses=0    │
                        └──────────────┘
                              ↑
                  Retourné quand l'apprenant
                  n'est pas trouvé
                  (au lieu de null)

Trainer’s Note: This pattern might as well be called “Default Object Pattern”, because what we return is actually an object with default values. But “Null Object Pattern” is the official name recognized by all developers — it is this name that must be used to be understood.

Thank you for exploring the Null Object Pattern. The author hopes that this pattern will help you, as it helped him, to write cleaner and more concise code.


Search Terms

c-sharp · design · patterns · null · object · testing · architecture · c# · .net · development · pattern · interface · inheritance · learnerview · class · comparison · complexity · covered · cyclomatic · demonstration · learnerservice · program · service

Interested in this course?

Contact us to book it or get a custom training plan for your team.