How can I hint the C# 8.0 nullable reference system that a property is initalized using reflection
Asked Answered
A

3

47

I ran into an interesting problem when I tried to use Entity Framework Core with the new nullable reference types in C# 8.0.

The Entity Framework (various flavors) allows me to declare DBSet properties that I never initalize. For example:

    public class ApplicationDbContext : IdentityDbContext
      {
    #pragma warning disable nullable
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
          : base(options)
        { }
    #pragma warning restore nullable

        public DbSet<Probe> Probes { get; set; }
        public DbSet<ProbeUnitTest> ProbeUnitTests { get; set; }
    }

The DbContext constructor reflects over the type and initializes all of the DbSet properties, so I know that all the properties will be non-null by the conclusion of the constructor. If I omit the #pragma's i get the expected warnings because my code does not initialize these properties.

 Data\ApplicationDbContext.cs(10,12,10,32): warning CS8618: Non-nullable property 'Probes' is uninitialized.
 Data\ApplicationDbContext.cs(10,12,10,32): warning CS8618: Non-nullable property 'ProbeUnitTests' is uninitialized.

Turning off the warnings seems like a blunt instrument when all I want to do is inform the compiler that a property will not be null?

If turns out I can fool the compiler like this:

     public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
      : base(options)
    {
      Probes = Probes;
      ProbeUnitTests = ProbeUnitTests;
    }

This code has the advantage that it is very narrow -- it only applies to the specific property's initialization and will not suppress other warnings. The disadvantage is that this is nonsense code because assigning a property to itself really ought to do nothing.

Is there a preferred idiom for informing the compiler that it just does not know that the property has been initialized?

Acrobat answered 4/8, 2019 at 0:25 Comment(3)
"The disadvantage is that this is nonsense code because assigning a property to itself really ought to do nothing." With 95% chance the JiT will just detect it is dead code and cut it out. So at least performance is not a thing you have to worry about.Amalbergas
Isn't the Nullable Reference type to 100% about this Compiler Warning? Unlike nullable value types, it does not seem to "do" anything besides allowing the compiler to warn you (wich is pretty usefull). So the obvious solution would be to not tell the compiler "warn me if I forget to assign it."Amalbergas
The solution you came up with is rather fascinating. I can actually sort of understand the logic the compiler must have used to determine that doing Probes = Probes should be okay (you're assigning Probes with something that's non-nullable: Probes), but we know that it's actually not okay, at this point Probes is still null, so you've probably actually found a bug in the compiler's nullable analysis.Mccowyn
M
86

Whenever you want to tell the compiler "shut up, I know what I'm doing" in regards to nullable reference types, use the ! operator. You can fix your issue by declaring your properties like so:

public DbSet<Probe> Probes { get; set; } = null!;
public DbSet<ProbeUnitTest> ProbeUnitTests { get; set; } = null!;
Mccowyn answered 4/8, 2019 at 3:4 Comment(8)
I like your idiom better. It seems a little odd to assert "I know that null is not null" but it works. This idiom has a few advantages. 1) it is local to the property being described, 2) It uses the null forgiving operator which is exactly what I want to do, and 3) it is so meaningless (asserting that null is not null) that it looks exactly like the hack that it is. Thanks for the insightful answer.Acrobat
Whaat? This looks to me like you're force-unwrapping a null, which will always throw an exception. How does this even compile?Felonious
The ! operator doesn’t do anything. It is nothing more than a hint to the compiler. There is no “unwrapping a null” here. Assigning null to a property is perfectly valid and does not result in an exception. How does it compile? Well the whole entire point of ! is to say to the compiler: “hey, I know this looks wrong, but please compile it anyway”Mccowyn
Personally, I prefer the way TypeScript handles this better (myVar!: myType; - the equivalent would be DbSet<Probe>! Probes { get; set; } or DbSet<Probe> Probes! { get; set; }) - not just because it's more concise but because it's an intentional decision in the specification; but it does make me happy that there is a way to do this that's almost as easy.Promiscuity
Another alternative to null! would be public DbSet<Probe> Probes { get; set; } = new();Menis
@Menis that can potentially hurt performance by causing unnecessary allocations, depends on there being a constructor with no parameters for the given type, and requires C#9 or laterMccowyn
@Dave M Good to know. But doesn't = null! store an actual null in the variable / property? Can't that lead to null reference exceptions that you weren't expecting?Menis
@Menis The question is specifically about what to do when you know the property is going to be automatically initialized by some mechanism the compiler is unaware of. In this situation Entity Framework initializes these properties automatically (probably using reflection), so you will never get an NRE. The compiler however doesn’t know that. So “assigning” null! isn’t doing anything at all really, it’s just a way to communicate to the compiler that we know this property will be”magically” initialized.Mccowyn
R
39

Microsoft has 2 recommendations for nullable references in the context of DbContext and DbSet:

1 - "On older version of EF Core"

The common practice of having uninitialized DbSet properties on context types is also problematic, as the compiler will now emit warnings for them. This can be fixed as follows:

    // ...
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();
    // ...

2 - Recommended solution

And the other is what @dave-m answered:

Another strategy is to use non-nullable auto-properties, but to initialize them to null, using the null-forgiving operator (!) to silence the compiler warning.

public DbSet<Customer> Customers { get; set; } = null!;
public DbSet<Order> Orders { get; set; } = null!;

The second one is now the preferred solution because (according to the documentation) the first one only works for older versions of EF Core.

Regimen answered 5/4, 2021 at 15:56 Comment(4)
Please in what namespace can I find Set<T>. I am using Microsoft.EntityFrameworkCore 5.0.9? I have tried looking for it in many places include Microsoft.EntityFrameworkCoreXerophyte
Looking at the documentation, that namespace seems right.Etan
The first is the best solution for net 6. It's the solution MS suggests. The namespace is right. Set<>() is a method of DbContext, it is not a class: "=>"Connors
@Connors the page now says the 1st solution is for older EF core, so basically we now have only 2nd option with null!Opulence
A
3

If you are using .NET 5, you can now use the MemberNotNullAttribute to indicate which members will be not null after the method returns.

public class ApplicationDbContext : IdentityDbContext
{
    [MemberNotNull(nameof(Probes))]
    [MemberNotNull(nameof(ProbeUnitTests))]
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
      : base(options)
    { }

    public DbSet<Probe> Probes { get; set; }
    public DbSet<ProbeUnitTest> ProbeUnitTests { get; set; }
}
Ashleeashleigh answered 29/7, 2021 at 13:37 Comment(4)
Thanks, that is a nice solution, and probably better at expressing the actual semantics that the null! initialization that seems to have gotten the nod from the EF Core Team.Acrobat
MemberNotNull doesn't work in Constructor. See #4299Inconsolable
See #3866 for exact use case of [MemberNotNull]Obel
At this time DbSet properties do not need to be initialized anymore: github.com/dotnet/efcore/issues/26877Globose

© 2022 - 2024 — McMap. All rights reserved.