C# Nullable Reference Types: Fix CS8618, CS8602 and More

Last updated: October 10, 2026 · Tested with .NET 10 (SDK 10.0.401, runtime 10.0.12), C# 14, ASP.NET Core 10.0.12, EF Core 10.0.12 (SQLite), Windows 11

Short answer: nullable reference types (C# 8 and later) let you say whether a reference can be null: string means “never null”, string? means “may be null”. Turn them on with <Nullable>enable</Nullable> in the .csproj (new .NET 6+ templates already do) or with #nullable enable in one file. The compiler then reports warnings such as CS8618, CS8602, CS8600 and CS8604. They are compile-time only: nothing stops a null at runtime, and I show below where nulls still get in (JSON, null!) and where the feature quietly changes runtime behavior (ASP.NET Core validation, EF Core columns). Every example was compiled and run on .NET 10; the warnings and output are copied from the real build.

Enable Nullable Reference Types

For a whole project, set Nullable in the .csproj. This is what dotnet new console generates on .NET 10:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

For an old project you migrate file by file, put #nullable enable at the top of one file and leave the project setting alone. #nullable disable turns it off again and #nullable restore goes back to the project setting.

enable vs warnings vs annotations vs disable

The Nullable property has four values, and the difference is easy to get wrong. I built the same file (the warning catalogue in the next section) with each value:

<Nullable>string? allowedNull warnings (CS86xx)What I got
enableyesyesall 12 warnings below
warningsno (CS8632 on every ?)only where the compiler sees a null on its ownCS8602, CS8629, CS8714 + 6 × CS8632
annotationsyesnono nullable warnings at all
disableno (CS8632)no6 × CS8632 only

annotations is useful for a library: you publish correct ? annotations for your callers without fixing every warning inside first. warnings is the opposite first step for an app: the compiler flags dereferences of values it can see are null, but you can’t write ? yet. For anything new, use enable.

Every Nullable Warning, With the Code That Causes It

These are the warnings people search for most. Each line marked with a comment produced exactly the warning shown below it when I ran dotnet build.

public class Customer
{
    public string Name { get; set; }          // CS8618
    public string? MiddleName { get; set; }
}

public static class Cases
{
    static string? FindName(int id) => id == 1 ? "Alice" : null;

    static void Cs8600()
    {
        string name = FindName(2);            // CS8600
    }

    static void Cs8602()
    {
        string? middle = FindName(2);
        Console.WriteLine(middle.Length);     // CS8602
    }

    static void LocalNull()
    {
        string name = null;                   // CS8600 (not CS8625) on a local
        Console.WriteLine(name);
    }

    static void Cs8625(Customer customer)
    {
        customer.Name = null;                 // CS8625
        Greet(null);                          // CS8625
    }

    static string Cs8603(int id)
    {
        return FindName(id);                  // CS8603
    }

    static void Greet(string name) => Console.WriteLine($"Hi {name}");

    static void Cs8604()
    {
        string? name = FindName(2);
        Greet(name);                          // CS8604
    }

    static void Cs8601(Customer customer)
    {
        customer.Name = FindName(2);          // CS8601
    }

    static int Cs8629(int? count)
    {
        return count.Value;                   // CS8629
    }

    static void Cs8714()
    {
        var map = new Dictionary<string?, int>();   // CS8714
    }

    static void Cs8619()
    {
        List<string> names = new List<string?>();   // CS8619
    }
}
Program.cs(5,19): warning CS8618: Non-nullable property 'Name' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
Program.cs(15,23): warning CS8600: Converting null literal or possible null value to non-nullable type.
Program.cs(21,27): warning CS8602: Dereference of a possibly null reference.
Program.cs(26,23): warning CS8600: Converting null literal or possible null value to non-nullable type.
Program.cs(32,25): warning CS8625: Cannot convert null literal to non-nullable reference type.
Program.cs(33,15): warning CS8625: Cannot convert null literal to non-nullable reference type.
Program.cs(38,16): warning CS8603: Possible null reference return.
Program.cs(46,15): warning CS8604: Possible null reference argument for parameter 'name' in 'void Cases.Greet(string name)'.
Program.cs(51,25): warning CS8601: Possible null reference assignment.
Program.cs(56,16): warning CS8629: Nullable value type may be null.
Program.cs(61,34): warning CS8714: The type 'string?' cannot be used as type parameter 'TKey' in the generic type or method 'Dictionary<TKey, TValue>'. Nullability of type argument 'string?' doesn't match 'notnull' constraint.
Program.cs(66,30): warning CS8619: Nullability of reference types in value of type 'List<string?>' doesn't match target type 'List<string>'.

Gotcha I didn’t expect: string name = null; on a local is CS8600, not CS8625. Many tutorials (including the first version of this post) say CS8625. You get CS8625 when you assign the null literal to a non-nullable property or field, or pass it to a non-nullable parameter. Also note that Console.WriteLine(name) gives no warning, because WriteLine accepts string?.

How to fix each warning

WarningUsual causeFix
CS8618Non-nullable property/field never set in the constructorrequired, an initializer (= "";), a constructor parameter, or make it string? if null is a real state
CS8602Member access on a T?Check first (if (x is null) return;), use ?. / ??
CS8600 / CS8601Maybe-null value stored in a non-nullable variable / propertyMake the target T?, or supply a fallback with ??
CS8603Method returns a maybe-null value but is declared non-nullableChange the return type to T? or never return null
CS8604Maybe-null argument to a non-nullable parameterCheck before the call, or make the parameter T? if the method handles null
CS8625null literal into a non-nullable property/parameterDon’t pass null; or annotate the target T?
CS8629.Value on a Nullable<T> (int?)count ?? 0, count.GetValueOrDefault() or if (count is int c)
CS8714T? used where the generic has a notnull constraint (dictionary keys)Use a non-nullable key type
CS8619Generic nullability mismatch (List<string?> vs List<string>)Make both sides agree

To make these warnings fail the build, add <WarningsAsErrors>nullable</WarningsAsErrors> to the PropertyGroup. I checked: the same file then fails with error CS8600, error CS8602 and so on. The word nullable is a shortcut for the whole group, so you don’t have to list codes.

Fixing CS8618 With required

The required modifier (C# 11) moves the check to the caller: the property stays non-nullable and whoever creates the object must set it.

public class StrictUser
{
    public required string Name { get; set; }
    public string? Email { get; set; }
}

Forgetting it is now a compile error, not a warning:

error CS9035: Required member 'StrictUser.Name' must be set in the object initializer or attribute constructor.

Flow Analysis: How the Compiler Narrows string? to string

After a null check the compiler knows the variable is not null, so the warning disappears without any !:

public static int Length(string? value)
{
    if (value is null)
    {
        return 0;
    }
    return value.Length;                   // no warning: value is not null here
}

public static string Shout(string? value) => (value ?? "nothing").ToUpperInvariant();

ArgumentNullException.ThrowIfNull(path) narrows too, because its parameter is marked [NotNull]. After that line, path.Length on a string? parameter compiled with no warning. At runtime it throws a clear message instead of a NullReferenceException deep inside your code:

ArgumentNullException: Value cannot be null. (Parameter 'path')

Nullable Attributes: NotNullWhen, MemberNotNull, NotNullIfNotNull

Sometimes the plain ? can’t express the contract. These attributes from System.Diagnostics.CodeAnalysis fix the three cases I hit most. To be sure each one does something, I built the demo once without the attribute and recorded the warning it removes.

public class Parser
{
    public bool TryParseName(string? input, [NotNullWhen(true)] out string? name)
    {
        name = input?.Trim();
        return !string.IsNullOrEmpty(name);
    }
}

public class Connection
{
    public string? ConnectionString { get; private set; }

    [MemberNotNull(nameof(ConnectionString))]
    public void Open(string connectionString)
    {
        ConnectionString = connectionString;
    }
}

[return: NotNullIfNotNull(nameof(value))]
public static string? Normalize(string? value) => value?.Trim().ToLowerInvariant();
AttributeWithout it, at the call siteWith it
[NotNullWhen(true)] on the outCS8602 on parsed.Length inside if (TryParseName(...))0 warnings
[MemberNotNull] on OpenCS8602 on conn.ConnectionString.Length after Open(...)0 warnings
[return: NotNullIfNotNull]CS8600 on string normalized = Normalize(" Bob ");0 warnings

Output of the demo with the attributes in place:

--- 2. Attributes
Parsed 'Alice' (5 chars)
Normalize(null) = null
Normalize("  Bob ") = 'bob'
Connected to 10-char string

[MemberNotNull] is also the clean fix for CS8618 when a constructor calls a helper to set fields. The compiler doesn’t look inside Reset(), so without the attribute the same class gets CS8618 on the constructor; with it, no warning:

public class ReportBuilder
{
    private string _title;

    public ReportBuilder() => Reset();

    [MemberNotNull(nameof(_title))]
    public void Reset() => _title = "Untitled";
}

The ! Operator Is Not a Runtime Check

The null-forgiving operator only silences the compiler. This compiles with no warning and still crashes:

string notReallySafe = null!;
try
{
    Console.WriteLine(notReallySafe.Length);
}
catch (NullReferenceException ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
NullReferenceException: Object reference not set to an instance of an object.

The two places where = null!; is the accepted pattern are EF Core navigation properties and properties a framework sets after construction. Everywhere else, treat each ! as a claim you have to be able to defend in code review.

Where Nulls Still Get In: System.Text.Json

Deserialization uses reflection, so it ignores your annotations by default. A JSON null lands in a string property:

var json = """{"Name":null,"Email":"a@b.com"}""";
var user = JsonSerializer.Deserialize<User>(json)!;
Console.WriteLine($"Default options: Name is null = {user.Name is null}");
try
{
    var strict = new JsonSerializerOptions { RespectNullableAnnotations = true };
    JsonSerializer.Deserialize<User>(json, strict);
}
catch (JsonException ex)
{
    Console.WriteLine($"RespectNullableAnnotations: {ex.Message}");
}
Default options: Name is null = True
RespectNullableAnnotations: The property or field 'Name' on type 'User' doesn't allow setting null values. Consider updating its nullability annotation. Path: $.Name | LineNumber: 0 | BytePositionInLine: 12.

RespectNullableAnnotations (System.Text.Json 9+) only covers an explicit null. A missing property is not checked: a class with public string Name { get; set; } = null!; deserialized from {"Email":"a@b.com"} still had Name is null = True with the option on. For missing properties use required, which System.Text.Json honors:

required keyword: JSON deserialization for type 'StrictUser' was missing required properties including: 'Name'.

More System.Text.Json behavior changes are in System.Text.Json vs Newtonsoft: .NET 10 migration gotchas.

ASP.NET Core: Non-Nullable Means [Required]

This one surprises people after they turn the feature on in an existing API. MVC controllers treat every non-nullable reference property as if it had [Required]. The model:

public class CreateCustomer
{
    public string Name { get; set; } = "";
    public string? MiddleName { get; set; }
}

public class CreateCustomerV2
{
    public string Name { get; set; } = null!;
}

What the [ApiController] returned when I posted to it (traceId removed):

$ POST /customers {"name":null}
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Name":["The Name field is required."]}}
$ POST /customers {}
Created  (middle: none)
$ POST /customers {"name":""}
Created  (middle: none)
$ POST /customers/v2 {}
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"One or more validation errors occurred.","status":400,"errors":{"Name":["The Name field is required."]}}

Three things to read from that:

  • An explicit null is rejected with “The Name field is required.”, even though the class has no [Required] attribute. If you see that error and don’t know where it comes from, this is it.
  • A missing property passes if the property has an initializer (= ""), and fails if the initializer is = null!. The check runs on the bound value, not on the JSON.
  • An empty string "" passed. Don’t rely on the implicit rule to reject blank input; add [Required] or [MinLength(1)] where blank is invalid.

To switch the implicit rule off (for example while migrating), set the MVC option:

builder.Services.AddControllers(options =>
{
    // set to true by passing --suppress on the command line (test only)
    options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = args.Contains("--suppress");
});

With it set to true, the same {"name":null} request returned 200 and the controller received a null Name.

EF Core: Nullability Decides NULL vs NOT NULL

EF Core reads the annotations to decide whether a column is nullable:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = null!;     // required column
    public string? Description { get; set; }      // optional column
    public Category Category { get; set; } = null!;
    public int CategoryId { get; set; }
}

public class Category
{
    public int Id { get; set; }
    public required string Title { get; set; }
    public List<Product> Products { get; } = [];
}

db.Database.GenerateCreateScript() with <Nullable>enable</Nullable>:

CREATE TABLE "Categories" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Categories" PRIMARY KEY AUTOINCREMENT,
    "Title" TEXT NOT NULL
);
CREATE TABLE "Products" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Products" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NOT NULL,
    "Description" TEXT NULL,
    "CategoryId" INTEGER NOT NULL,

The same model built with <Nullable>disable</Nullable>:

    "Title" TEXT NULL
    "Name" TEXT NULL,
    "Description" TEXT NULL,

Migration trap: turning on nullable reference types in an existing EF Core project changes your model. I tested it: created the database with the feature disabled, inserted a product with Name = NULL, switched to enable and ran dotnet ef migrations add EnableNullable. EF printed “An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.” and generated this for every string property:

            migrationBuilder.AlterColumn<string>(
                name: "Name",
                table: "Products",
                type: "TEXT",
                nullable: false,
                defaultValue: "",
                oldClrType: typeof(string),
                oldType: "TEXT",
                oldNullable: true);

The migration did not fail. On SQLite the generated SQL copies the table with IFNULL("Name", ''), so after dotnet ef database update my NULL had silently become an empty string. “No value” and “empty value” are now the same thing in your data. Mark the columns that really are optional as string? before you add the next migration, and read the migration when EF shows that warning. Also note that required alone did not make Title NOT NULL with the feature disabled: EF Core goes by the nullability annotation. For the rest of the EF Core setup, see Repository and Unit of Work with EF Core.

Generics: T? Doesn’t Always Mean Nullable

On an unconstrained type parameter, T? means “default of T”, not Nullable<T>. For value types you get 0, not null:

public static T? FirstOrNull<T>(IEnumerable<T> items)
{
    foreach (var item in items) return item;
    return default;
}

public static T? FirstOrNullStruct<T>(IEnumerable<T> items) where T : struct
{
    foreach (var item in items) return item;
    return null;
}
FirstOrNull(new int[0])    = 0
FirstOrNull(new string[0]) = null
FirstOrNullStruct(new int[0]) = null

If callers need to tell “empty” from “first item is 0”, add the where T : struct overload, or return a bool with an out value.

Reading Annotations at Runtime

The annotations are stored in metadata, which is how ASP.NET Core, EF Core and System.Text.Json see them. You can read them yourself with NullabilityInfoContext:

var ctx = new NullabilityInfoContext();
foreach (var prop in typeof(User).GetProperties())
{
    var info = ctx.Create(prop);
    Console.WriteLine($"{prop.Name,-6} {prop.PropertyType.Name,-7} ReadState = {info.ReadState}");
}
Name   String  ReadState = NotNull
Email  String  ReadState = Nullable

Both properties are System.String to the runtime; only the annotation differs.

Migrating an Existing Project: What Worked

  • Libraries first, with annotations. Callers get correct signatures right away.
  • Apps file by file, with #nullable enable. Start with the leaf types (DTOs, entities), then services, then controllers.
  • Decide entity nullability before the next EF migration, as shown above.
  • Check your API contract tests for the implicit [Required] change in MVC.
  • Turn on WarningsAsErrors=nullable last, once the warning count is zero, so it stays zero.

FAQ

Are nullable reference types enabled by default? In projects created from the .NET 6 or later templates, yes: the template writes <Nullable>enable</Nullable>. Older projects and projects without the property have it off.

How do I fix CS8618? Add required, give the property an initializer, set it in the constructor, or make it nullable if null is valid. For EF Core navigation properties, = null!; is the usual pattern.

What is the difference between CS8600 and CS8625? CS8600: a maybe-null value (or the null literal) stored in a non-nullable local. CS8625: the null literal assigned to a non-nullable property or field, or passed to a non-nullable parameter.

Do nullable reference types affect performance? No. string and string? are the same runtime type; the annotation is metadata (see the NullabilityInfoContext output).

Why does my API return “The Name field is required.” without [Required]? MVC treats non-nullable reference properties as required. Make the property string? or set SuppressImplicitRequiredAttributeForNonNullableReferenceTypes.

Related: C# primary constructors (IDE0290 and pitfalls) · C# 13 features and what C# 14 added

References: Nullable reference types (Microsoft Learn) · Resolve nullable warnings · Nullable static analysis attributes

This article is part of the C# tutorials guide (language features, patterns, performance and testing).

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *