0Pricing
C# Academy · Lesson

Migrating a Codebase to NRT

Apply a phased migration strategy: enable warnings, annotate APIs, fix issues, and avoid false-positives.

Migrating a Codebase to NRT is a free C# Academy lesson on CoddyKit — lesson 4 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The Migration Challenge

Enabling NRT on an existing codebase typically produces hundreds of warnings. A big-bang approach is risky. Instead, use a phased migration: enable warnings incrementally, fix them file by file, and never lose momentum.

Step 1: Enable Warnings Only

Start with <Nullable>warnings</Nullable> instead of enable. This activates warnings without treating non-annotated code as errors — a safe starting point.

<!-- Phase 1: warnings only, no breaking change -->
<PropertyGroup>
  <Nullable>warnings</Nullable>
</PropertyGroup>

<!-- Phase 2: full enable per file as you migrate -->
<!-- Phase 3: switch to enable globally when done -->

Step 2: Enable Per File

Add #nullable enable at the top of each file as you work on it. This confines changes to files you're actively editing, making reviews manageable.

#nullable enable
// Now this file has full NRT analysis

public class OrderService
{
    private readonly IOrderRepository _repo;
    // Compiler now warns about uninitialized non-nullable fields,
    // unsafe dereferences, and assignment to non-nullable

    public OrderService(IOrderRepository repo) => _repo = repo;
}

// Other files without #nullable enable are still unchecked

Categorizing Warnings

Warnings fall into two categories: safe to suppress (ORM entities, DI-injected fields) and real bugs (actually null values being dereferenced). Distinguish these before suppressing anything.

// Category 1: safe to suppress with null!
// EF Core navigation properties — set by EF, never null in practice
public class Order
{
    public Customer Customer { get; set; } = null!;
}

// Category 2: real bug — must fix
public string GetFullName()
{
    return FirstName + " " + LastName; // LastName was string? -- BUG!
}

Fixing Constructor Warning CS8618

CS8618 fires when a non-nullable property isn't set in the constructor. The preferred fix is to require it in the constructor. Use = null! only for framework-set values.

// BEFORE (CS8618)
public class Product
{
    public string Name { get; set; }   // warning
    public Category Category { get; set; } // warning
}

// AFTER — constructor required:
public class Product
{
    public string Name { get; set; }
    public Category Category { get; set; }

    public Product(string name, Category category)
    {
        Name = name;
        Category = category;
    }
}

Handling Legacy APIs

Third-party or legacy APIs may not be annotated. Their return types are oblivious (neither nullable nor non-nullable). Assign their results to nullable variables to be explicit.

// Legacy API returns 'string' but might be null (oblivious type)
string? legacyResult = OldLibrary.GetValue(); // store as nullable
if (legacyResult is null) return;

// Or convert at the boundary:
string safe = OldLibrary.GetValue() ?? "";

// For third-party types, check if they have NRT annotations:
// NuGet packages often add nullable annotations in newer versions

Using #pragma to Suppress Specific Warnings

When a warning is genuinely a false positive and = null! feels too noisy, use #pragma warning disable scoped to the specific line.

// Suppress for a specific case with explanation:
#pragma warning disable CS8618 // ORM populates this via reflection
public DbSet<Product> Products { get; set; }
#pragma warning restore CS8618

// Or inline with a comment:
public DbSet<Order> Orders { get; set; } = null!; // set by EF Core

Treating NRT Warnings as Errors

Once all warnings are fixed in a file, add <WarningsAsErrors>Nullable</WarningsAsErrors> (or use CI enforcement) to prevent regression — any new nullable issue fails the build.

<!-- After full migration: treat nullable warnings as build errors -->
<PropertyGroup>
  <Nullable>enable</Nullable>
  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
  <!-- Or selectively: -->
  <!-- <WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors> -->
</PropertyGroup>

Annotating Public APIs

When your library is consumed by others, NRT annotations become part of your public API contract. Return T? when the result can be null; return T when it's guaranteed.

public interface IProductService
{
    // Contract: FindById MAY return null, GetById never does
    Product? FindById(int id);
    Product  GetById(int id);  // throws if not found

    // Collection: never null (may be empty)
    IReadOnlyList<Product> GetAll();

    // String: may be empty but not null
    string GetSummary(int id);
}

Migration Metrics and Tracking

Track progress by counting files with #nullable enable or by running dotnet build 2>&1 | grep CS86 in CI. Set a target date for full project migration.

# Count NRT warnings in current build
dotnet build 2>&1 | grep -c 'CS860[0-9]\|CS861[0-9]\|CS862[0-9]'

# List files still missing #nullable enable
grep -rL '#nullable enable' src/ --include='*.cs'

# Track in CI: fail if warning count increases
# Set a budget: warnings <= N, where N decreases each sprint

Quick Check

What does the = null! assignment on a non-nullable property communicate?

Recap: Migrating to NRT

Key takeaways:

  • Use phased migration: warnings mode → per-file enable → global enable
  • Distinguish real bugs (fix them) from ORM/DI patterns (use = null!)
  • Fix CS8618 by requiring values in constructors, not by suppressing
  • Assign legacy API results to T? variables to be explicit
  • Treat nullable warnings as errors in CI to prevent regression
  • Annotated public APIs become clear contracts for consumers

Frequently asked questions

Is the “Migrating a Codebase to NRT” lesson free?

Yes — the full text of “Migrating a Codebase to NRT” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.

What will I learn in “Migrating a Codebase to NRT”?

Apply a phased migration strategy: enable warnings, annotate APIs, fix issues, and avoid false-positives. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start C# Academy?

No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 4 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Migrating a Codebase to NRT” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this C# Academy lesson?

Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Enabling & Understanding NRT
  2. Annotations: ?, !, MaybeNull & NotNull
  3. Null-Conditional & Null-Coalescing Operators
  4. Migrating a Codebase to NRT
← Back to C# Academy