With ASP.NET Core and Entity Framework
So you plan on switching to .NET Core 3.0 but you’re not sure if you also want to make use of the new feature called “nullable references”. Yes, it is a great feature in terms of preventing bugs and crashes so you should be using it, but how should you approach switching to it?
But Weren’t References Already Nullable?
I know, “nullable references” is a confusing term because you have always been able to assign null to a reference in C#. As I understand, C# team implies that if the value can be encapsulated inside a Nullable<T>
, it’s nullable, not that if it can be assigned null or not. Because the previous feature to make value types nullable was called nullable value types.
Think of it this way: now with C# 8.0, you can’t assign references null or nullable values anymore. So, all references are non-nullable by default which means that we have a new construct called “nullable references”. I hope that clears some confusion.
string s; // non-nullable reference
string? s; // nullable reference
Nullable References Don’t Save You From (All) Null Checks
The idea that C# 8.0 will save you from all those pesky null checks is a misconception. It makes the developer believe that the null-checking code can finally go away and we can deal with correct types directly. Unfortunately, it’s not true. First, you still have to check for null values in your public functions that are open to the third-party callers, like in a library code, because the caller isn’t guaranteed to use nullable references themselves. So, for a code that doesn’t use nullable references, your function signatures are just as nullable as a C pointer. It doesn’t magically make all the third-party code “null-aware”. Your externally facing functions in your libraries will still have to check for null values and throw ArgumentNullException
accordingly.
Second, you’ll still have to account for nulls when you use nullable types, duh. The compiler will make you do those checks gracefully. Nullable references don’t make the dreaded null go away; on the contrary, it lets you use them whenever you see fit, which brings to my next point:
Nullable References Are Not Free
Yes, I know, your compiler will take care of many of the null checks in your code and gladly prevent you from passing a nullable value to a function, keeping you safe. However, there is still a cost: Now, you have to think about every reference you use about its nullability semantics, something a C# programmer isn’t very accustomed to. It’s important because not thinking about it can make the whole feature useless. You can easily devolve into that spiral of “Hey I’ll just add a question mark or a bang to that to make the compiler shut up” and blindly make everything nullable or ignore the nullability because it creates so much friction to you.
Remember, nullability errors are there to help you but they don’t work without your investment.
I’ve got some tricks to help you in deciding on a variable’s or a property’s nullability easily:
- Try to keep a mindset of making everything NON-NULLABLE by default unless they are, in fact, optional. Creators of C# 8.0 already did some of the work by making you show extra effort by putting a question mark at the end of the type but it’s still not that hard.
- Make sure that nullable members are nullable for good reason.
- One question to ask yourself when deciding on nullability is “do I have any plans for the absence of this value?”. If your answer is no, keep it non-nullable.
- Always use nullability as a mnemonic to signify what’s optional or not in your domain model. If your code expects the argument to always have a valid value, it simply can’t be null.
- Don’t fix compiler errors by changing your understanding of what’s optional or what’s required. Fix your model instead.
and then comes the migration part, aka “the fun part”.
Don’t Do Everything At The Same Time
Do NOT switch to .NET Core 3.0 and nullable references at the same time. Switch to .NET Core 3.0 and make sure your code builds and runs fine first. Build and test your application. Then you can start switching to nullable references. You should always follow a minimal/incremental approach when refactoring large chunks of code. The other way never works.
Enabling Nullable References
You can enable nullable references partially or altogether. I usually prefer altogether because the warnings in a small project are so few that it’s not worth to do it incrementally. In order to enable it globally, add this to your project file in a PropertyGroup section:
<Nullable>enable</Nullable>
If you want to disable or enable nullable settings you can use compile-time directives:
#nullable enable
// This part of code will make use of nullable references
#nullable restore
// This part of code will run according to the global setting
#nullable disable
// This part of code will have nullable references disabled
#nullable restore
// This code will use global settings again
Tools Of The Trade
Nullable references come with some new syntactic elements to help you declare your intent better when handling potentially null values.
The “It’s Okay, I Know What I’m Doing” Operator
When you append “!” to an expression, it means that “I know that this expression can have a null value theoretically but I’m sure that it will never be null and I bet my application’s lifetime on it, because I know that my app will crash if it ever becomes null”. It’s especially useful in class and struct declarations where compiler complains about property or field not being assigned a value in the constructor.
Example:
string? s1 = "Hello";
string s2 = s1!; // compiler error avoided
The “This Parameter Will Never Be Null” Code Contract
Sometimes, you have to declare your function accepting nullable parameters because of the interface you’re implementing or because of the base class you inherited from but you are actually sure that it will never be null. The compiler will complain about you not checking the nullability of the value yet you know that it’s impossible. So you add an assumption contract:
public void Process(string? s)
{
Contract.Assume(x != null);
Console.WriteLine(s);
}
Entity Framework
Uninitialized Properties in EF Entities
The compiler will complain about your EF entity declarations that they don’t initialize their properties. If you don’t want to add a custom constructor code, you can simply bypass the compiler error by adding “= null!;
” to non-nullable properties.
Before nullable references:
[Required]
public string FirstName { get; set; }
With nullable references:
public string FirstName { get; set; } = null!;
The advantage of using non-nullable references over using Required
attributes with nullable references is that the C# compiler will still be checking some illegitimate uses of the null assignment (e.g. you won’t be assigning a nullable version of the same data type to this field).
I just learned that EF Core also supports custom constructors for object instantiation as long as the parameter names in the constructor matches with the properties by name sans casing, which is great news!
Before nullable references:
[Required]
public string FirstName { get; set; }public string MiddleName { get; set; }
With custom constructors:
public string FirstName { get; set; }
public string? MiddleName { get; set; }
public Person(string firstName)
{
FirstName = firstName;
}
Although we don’t have middleName
in the parameter list, EF will still assign MiddleName property correctly after calling the custom constructor. So you can limit your custom construction code to required properties only.
This approach can be more involved than adding null!
to properties but definitely the most correct way because it’s impossible to make a mistake in the code.
The [Required] Attribute
The use of the Required attribute isn’t required (!) anymore with EF Core 3.0. Nullability is decided by the existence of “?” at the end of the data type.
Before nullable references:
public class Person
{
[Required]
public string FirstName { get; set; } // NOT NULL
public string MiddleName { get; set; } // NULL
}
With Nullable references enabled:
public class Person
{
public string FirstName { get; set; } = null!; // NOT NULL
public string? MiddleName { get; set; } // NULL
}
The = null!
allows the compiler to bypass the “property not initialized” errors for EF entities. Use this only with EF entities. For other classes, create a constructor instead (see the part about uninitialized properties).
Dereference of a possible null reference
C# compiler shows this warning when you define a query with nullable relationships in it. Consider this code:
var parentPosts = db.Posts
.Where(p => p.ParentPost.Id == postId)
.ToList();
Here, p.ParentPost
can in fact be null, but EF Core doesn’t actually care about this. So you just need to make the compiler happy by saying “this won’t be an issue, I guarantee you”:
var parentPosts = db.Posts
.Where(p => p.ParentPost!.Id == postId)
.ToList();
ASP.NET Core MVC
The [Required] Attribute On Models
Since ASP.NET Core also needs to initialize the models out of thin air, you need to follow a pattern similar to Entity Framework, you get rid of the Required attributes and use nullability as the indicator of optionality.
Before nullable references:
public class RegistrationForm
{
[Required]
public string FirstName { get; set; } // required field
public string MiddleName { get; set; } // optional field
}
public IActionResult MyAction([Required]RegistrationForm form)
{
if (form == null || !ModelState.IsValid)
{
return View();
}
// .. blabla
}
After nullable references:
public class RegistrationForm
{
public string FirstName { get; set; } = null!; // required field
public string? MiddleName { get; set; } // optional field
}
public IActionResult MyAction(RegistrationForm form)
{
if (!ModelState.IsValid)
{
return View();
}
// .. blabla
}
What you need to pay attention to is not to apply any rule everywhere blindly. So don’t add != null
to all of your properties, don’t add !
to all the data type that you were supposed to be checking for nullability. That kills the whole purpose of nullable references. Focus on how nullable references help you in avoiding bugs and try to think about nullability more than usual. Let the compiler fail and make sure you’re making the best decision when choosing how to solve problems.
That pretty much sums it up. I haven’t encountered any other issues while migrating to nullable references but I’ll publish more articles in the series if I happen to.