-2

My understanding of C#'s null safety features (from C# 8.0 onwards) is that (possibly) null variables must be typed as nullable and that the compiler will statically detect any parts of the code which may involve a null reference dereference and issue a warning. Coupled with <WarningsAsErrors>Nullable</WarningsAsErrors>, I would expect this to make it impossible for null reference exceptions to occur.

However, the following code compiles but when a Get request is sent to /endpoint, it returns a null reference exception:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

var connString = "User ID=postgres;Password=password;Host=localhost;Port=5432;Pooling=true;Connection Lifetime=0;Database=MinExample";
builder.Services.AddDbContext<AppDbContext>(options => options.UseNpgsql(connString));
var app = builder.Build();


app.MapGet("/endpoint", long (AppDbContext context) =>
{
    var id = new Service(context).GetBId();
    return id;
});

app.Run();

public record A
{
    public long AId { get; init; }
    public required B B { get; set; }
}

public class B
{
    public required long BId { get; init; }
}

public class Service
{
    private readonly AppDbContext _context;

    public Service(AppDbContext cx)
    {
        _context = cx;
    }

    public long GetBId()
    {
        var a = _context.A.First();
        return a.B.BId;
    }
}

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) 
        : base(options) { }

    public DbSet<A> A { get; set; }
}

My database has a single entry in the A table and a single related entry in the B table.

In fact, the compiler is so confident that id is not null that if I insert Console.WriteLine(id is null) just before the return statement of the API, the compiler fails with an error

Cannot convert null to 'long' because it is a non-nullable value type

On the other hand, Console.WriteLine(id == null) compiles and runs fine (and writes True to the console) although JetBrains warns me that

The result of the expression is always 'false' because a value of type 'long' is never equal to 'null' of type 'long?'

Why does the compiler not throw an error that the variable I am returning is null? Is this a null-safety violation? What am I missing about how null-safety works in C# 8.0 and up?

(The "solution" to this problem is to replace the query with

var a = _context.A.Include(a => a.B).First();

but my question is about why the compiler does not detect that something is wrong, not about how to fix the problem itself).

All of the above has been build with .NET 9.0.112.

5
  • 1
    These language rules are only implemented in the compiler. There is no guarantee at runtime. Particularly for anything that uses reflection, like serialising an instance of a type from an external source. Though you can tell EF Core that a navigation should always be included learn.microsoft.com/en-us/ef/core/querying/related-data/… Commented 6 hours ago
  • IMHO most navigation properties should be nullable. Not because the relationship isn't required. But because you only want to load a subset of the database into memory. And perhaps EF Core should raise a diagnostic if a navigation is required, but not auto-included. Commented 6 hours ago
  • @JeremyLakeman I'm not sure I understand what you mean by " These language rules are only implemented in the compiler". My understanding was that whenever the compiler could not guarantee that a dereference wasn't of a null variable, it would issue a warning. Are you saying that's not the case? Commented 6 hours ago
  • learn.microsoft.com/en-us/dotnet/csharp/nullable-references "Nullable reference types are a compile time feature. That means it's possible for callers to ignore warnings...". There are no runtime tests to enforce that a field has been assigned. And EF Core is all about creating instances of your types at runtime. EF Core does not know that much about C#. It only complies with the rules of the dotnet runtime, and what extra metadata you have defined in your model. Commented 6 hours ago
  • Specifically learn.microsoft.com/en-us/ef/core/miscellaneous/… Though perhaps that documentation should also reference .AutoInclude(). Commented 6 hours ago

1 Answer 1

4
My understanding was that whenever the compiler could not guarantee that a dereference wasn't of a null variable, it would issue a warning. Are you saying that's not the case?

Yes, this is not the case.

If you check the Nullable reference types docs, the first sentence is (emphasis is mine):

Nullable reference types are a group of features that minimize the likelihood that your code causes the runtime to throw System.NullReferenceException.

This feature works at compile time by tracking null-state (including analyzing special annotations):

That means it's possible for callers to ignore warnings, intentionally use null as an argument to a method expecting a non nullable reference.

There is even a way to say "shut up, I'm smarter" to the compiler, for example (the infamous null-forgiving operator):

object o = null!;
Console.WriteLine(o.ToString()); // Boom

Also please check out the known pitfalls section. There are cases when without "malicious" null-forgiving operator the analysis fails. For example:

PrintStudent(default); 

static void PrintStudent(Student student)
{
        Console.WriteLine($"First name: {student.FirstName.ToUpper()}"); // Boom!
        Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}");
        Console.WriteLine($"Last name: {student.LastName.ToUpper()}");
}

public struct Student
{
    public string FirstName;
    public string? MiddleName;
    public string LastName;
}

So in the nutshell - this is statical analysis tool which can't guarantee that there will be nulls and works on assumption that every library and piece of code honors the nullable contract (which can't be enforced).

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.