In this post I discuss C#'s nullable reference types in the context of Razor Pages: why the two don't play nicely together, why they probably never will, and how to make the experience a bit nicer.
Nullable reference types: the highs and lows.
I really like working with C#. Recent improvements to pattern matching, for example, can significantly improve the readability of some code (in my opinion), while other improvements like Span<T>
unlock performance that wasn't easily achievable previously. There are valid arguments and concerns about the ever increasing complexity of the language, but putting those concerns aside each new feature feels like a definite improvement. Except one, which I constantly struggle with: nullable reference types.
Nullable reference types were introduced in C# 7.3 as an attempt to work around the "billion dollar problem", null
as the default for C# reference types, and the ever-pervasive NullReferenceException
. This is a laudable goal, as these bugs are often the result of a programming error, misconceptions, broken (implied) API contracts, or a failure to sanitise inputs. Anything that helps with those issues should be a net win!
The trouble is, it's difficult to retrospectively hack that feature into an existing language. The premise of "just add ?
to your type definitions and we'll figure it out" is great when it works, but there are all sorts of edge cases that the compiler just can't figure out, or that require a soup of attributes to teach the compiler what to do.
Despite that, I use nullable reference types wherever I can. Sure, there can be annoying edge cases, but for the most part, they provide an extra level of analysis that it would be silly to ignore.
The big exception to that is libraries that need to multi-target older versions of .NET Core or .NET Framework. The lack of nullability annotations for the BCL there can make enabling nullability painful, to the point where it's just not worthwhile IMO.
Unfortunately, there are some patterns and frameworks which just don't lend themselves well to nullability analysis. In my experience, Razor Pages, my preferred framework for server-rendered page-based apps in ASP.NET Core, is one such framework.
Razor Pages and nullable reference types
The Razor Pages framework was introduced in ASP.NET Core 2.0. It provides a set of conventions and features on top of the existing MVC framework to make building traditional "page-based" websites easier. By page-based I generally mean:
- You navigate to a page
- You click an edit button, which takes you to the "edit" version of a page, with the details displayed in form elements
- You post the form results back to the same URL
- You redirect to another page
This was a very common pattern for years, until the Single Page App (SPA) came along. I'm not going to go into server-rendering vs client-rendering here; the important thing is that Razor Pages can provide a much better developer experience for apps using this pattern than its precursor, MVC controllers.
Razor Pages embraces the fact that the "display form" and "receive POST of form" conceptually are the same page, as opposed to MVC controllers which treats them as two very separate actions. Razor Pages also explicitly ties the UI for a form to the backend logic, which is what you do in MVC 99% of the time.
Anyway, enough background, let's look at the problem of nullable reference types in Razor Pages, by taking an example
The sample page
We'll start with a simple Razor Page. I created a basic page which contains:
- A
Get
handler and aPost
handler - A binding model defined as a nested type
InputModel
which binds to the propertyInput
- An "output" value,
Result
, which is populated after a successful form post - A form for posting back in the HTML
The code behind, which is what we're mostly focused on here, looks like this:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
namespace razor_page_nullability.Pages;
public class IndexModel : PageModel
{
[BindProperty]
public required InputModel Input { get; set; }
public string Result { get; set; }
public void OnGet()
{
}
public void OnPost()
{
if (ModelState.IsValid)
{
Result = $"Hello {Input.FirstName[0]}. {Input.LastName}";
}
}
public class InputModel
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
}
For completeness, the .cshtml file looks like this:
@page
@model IndexModel
<div class="row">
<div class="col-md-6 offset-md-3">
<form id="registerForm" method="post">
<h2>Enter your name.</h2>
<hr />
<div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
<div class="form-floating mb-3">
<input asp-for="Input.FirstName" class="form-control" aria-required="true" />
<label asp-for="Input.FirstName"></label>
<span asp-validation-for="Input.FirstName" class="text-danger"></span>
</div><div class="form-floating mb-3">
<input asp-for="Input.LastName" class="form-control" aria-required="true" />
<label asp-for="Input.LastName"></label>
<span asp-validation-for="Input.LastName" class="text-danger"></span>
</div>
<button id="registerSubmit" type="submit" class="w-100 btn btn-lg btn-primary">Register</button>
</form>
@if(!string.IsNullOrEmpty(Model.Result))
{
<div class="alert">@Model.Result</div>
}
</div>
</div>
which, when run, looks like this:
Nothing wrong or controversial so far…until you look at the build warnings generated by this simple page, summarised below:
> dotnet build
warning CS8618: Non-nullable property 'Input' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
warning CS8618: Non-nullable property 'Result' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
warning CS8618: Non-nullable property 'FirstName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
warning CS8618: Non-nullable property 'LastName' must contain a non-null value when exiting constructor. Consider declaring the property as nullable.
Handling the simple cases.
Some of the complaints here are perfectly valid, Result
, for example. Result
may or may not be set, depending on which handler runs, and on whether the model is valid, so updating that with a nullable reference (string?
) makes perfect sense:
public string? Result { get; set; }
One down, three to go. But this is where things get tricky, and somewhat philosophical.
Analysing model bound properties
Let's think about FirstName
and LastName
in the InputModel
to start with. For simplicity, I've repeated the definition below:
public class InputModel
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
Both properties are described as a string
without the nullable ?
annotation. Logically we know that if the model has been validated correctly, these can't be null, so the string
makes sense there, right?
But that's only if model binding succeeded. If it didn't, or indeed, if we manually create an instance, then these values could be null. So actually, marking them as nullable would make sense? Probably?
The trouble with that is the only place we access those properties directly are in the OnPost
handler after we've validated the model. Which means a bunch of null-conditional access ?
are required in places we know it's not null
:
public void OnPost()
{
if (ModelState.IsValid)
{ // 👇 we know it's not null, but still need this
Result = $"Hello {Input.FirstName?[0]}. {Input.LastName}";
}
}
Or we could use !
of course, which is more "correct" in some sense (as we know it's not null
), and may be better in specific cases. But I normally feels like I'm doing something wrong when I use it…
public void OnPost()
{
if (ModelState.IsValid)
{ // 👇 we know it's not null, so this may be better
Result = $"Hello {Input.FirstName![0]}. {Input.LastName}";
}
}
So this has kind of solved the issue, but it's mostly just a bunch of extra noise to keep the compiler happy because it can't understand how the interaction between the BCL, type system and framework here (which is perfectly reasonable!).
It's a bit unsatisfying, but let's leave this one for now. Let's look at the instance of InputModel
exposed on the PageModel
.
The problematic binding model
The final warning we have is for the InputModel
property on the PageModel
itself;
public class IndexModel : PageModel
{
[BindProperty]
public InputModel Input { get; set; } // Non-nullable property 'Input' must contain a non-null value when exiting constructor
// ...
}
So, this is where things get tricky. We don't declare InputModel
as nullable, because, similar to its properties, it won't be when you use it properly.
Specifically, in the OnPost
handler, Input
is always non-null, because the Razor Pages framework creates an instance of it and sets the value on the model. So in theory, it's always non null.
Except, it will be null
in the OnGet
handler. Razor Pages doesn't bind [BindProperty]
models on GET
unless you specifically tell it to, so the model is left null
. That's completely expected for Razor Pages conventions, but it causes us an issue:
Input
is alwaysnull
inOnGet
Input
is nevernull
inOnPost
Clearly we could mark Input
as nullable using InputModel?
, but then we're going to fighting even more with extraneous ?
and !
for a property we know won't be null. And you have to do this in every single Razor Page. Urgh 😩
Possible solutions
The nullable reference type feature is meant to guide you into catching bugs, by annotating your types so they more accurately represent the possible values they can represent. This is a good thing generally. The problem is, it falls down when you have a framework like Razor Pages which is calling into your methods and instantiating your types independently of the "normal" program flow.
In this situation, marking the Input
property as InputModel?
doesn't really give me any more information. All my Razor Pages use this same pattern, and they all have a null value in OnGet
and a non-nullable value in OnPost
. Adding the ?
annotations doesn't give me any new information, or help to catch bugs; it's just noise.
Coming to the realization finally made me reach out for some other solution. I've described some of the options below, but honestly, I'm not particularly satisfied with any of them. They all have pros and cons, but mostly they add noise for not much extra value.
1. Suck it up
Ok, option 1 is just suck it up and use ?
and !
as appropriate. Mark InputModel
as ?
, and use ?.
or !.
when accessing it. Maybe it's fine. Maybe I'm being overly sensitive.
One of my big complaints with this is that it degrades the whole value proposition of nullable reference types. If we say "yeah, it doesn't work in Razor Pages" then trying to espouse it's benefits becomes harder. Yes it's a different situation in Razor Pages than in your library code, but it still muddies the waters for newcomers.
Plus it's ugly, and annoying. Still, it's an option.
2. #nullable disable
The other option is just to accept that nullable reference types don't work in Razor Pages. That's reasonable, Razor Pages was released before nullable reference types, and they're just not compatible. So why not turn it off?
You can easily disable nullable reference types for a single file by adding #nullable disable
to the top of the file:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
// 👇 Add this
#nullable disable
namespace razor_page_nullability.Pages;
public class IndexModel : PageModel
{
[BindProperty]
public required InputModel Input { get; set; }
// ...
}
This isn't a bad solution. You can leave nullability enabled for the rest of your project, and just disable it in each of your Razor Pages. This is even what the ASP.NET Core Identity scaffolded pages do: they set #nullable disable
at the start of every page.
I have 2 concerns with this approach:
- You have to add it to every single page.
- This may miss nullability bugs when interacting with the rest of your application.
That first point is that this is just annoying cruft again. I tried adding #nullable
to _ViewStart.cshml in the vain hope that it would be applied to every file, but no luck.
The second point is a bigger concern. If your Razor Pages are interacting with some application/domain service, the application/domain service will encode the nullability of the response in the type system. But if you've disabled nullable reference types in your Razor Pages, you could introduce issues that would have been caught if you hadn't added #nullable disable
.
For example, imagine I have a user service defined like this:
public interface IUserManager
{
Task<User?> GetUser(string id);
}
and I use it in a Razor Page like this:
public async Task OnGet(string id)
{
var user = await _userManager.GetUser(id);
Result = $"Hello {user.Name}"; // 💥 potential NullReferenceException
}
I've just introduced a potential NullReferenceException
that would have been a warning previously.
3. It's not null
, dammit!
One option I've seen people using in the wild is setting the property to null!
, which says "set this property to null
and tell the compiler it's definitely not null
". Putting aside the moral question of lying to a computer, this works surprisingly well:
public class IndexModel : PageModel
{
[BindProperty]
public InputModel Input { get; set; } = null!;
// ...
}
This is a pretty good position. The compiler is satisfied, it's relatively minimal changes required in our Razor Page, and we're not overriding nullability checks for any of the other code in our app. This was my go-to approach. But with C# 11 coming soon, there's another possibility…
4. C# 11's solution: required
C# 11 is introducing a new modifier for fields and properties, required
. As per the docs:
The required modifier indicates that the field or property it's applied to must be initialized by all constructors or by using an object initializer.
While not tied to init
, required
is the missing step that actually makes it useful (in my opinion). You can use init
to make a property read-only except during object construction. For example:
var a = new InputModel(); // valid
var b = new InputModel // valid
{
FirstName = "Andrew",
LastName = "Lock",
};
b.LastName = "LOCK"; // valid
b.FirstName = "A"; // ⚠ compiler error
public class InputModel
{
public string FirstName { get; init; } // 👈 init, not set
public string LastName { get; set; }
}
As you can see in the above example, you can set FirstName
only in the object initializer (or in a constructor), after that it's readonly. The trouble is, prior to C# 11, there was no way to force you to set it. That's where required
comes in. required
makes it a compiler error to not set the property in the constructor or in an object initializer. So in the above example, this is a compiler error:
var a = new InputModel(); // ⚠ compiler error
public class InputModel
{
public required string FirstName { get; init; } // 👈 required + init
public string LastName { get; set; }
}
So how does this help us?
If you mark a property as required
, then the compiler knows that the creator of the instance has to set the property, so it doesn't have to worry about the fact we haven't set it.
Long story short, we can mark the Input
property as required
, and all the problems go away:
public class IndexModel : PageModel
{
[BindProperty]
public required InputModel Input { get; set; } // 🎉 no compiler warning
// ...
}
The Razor Pages framework sets the property via reflection, so it doesn't interact with it at all (reflection ignores init
/required
), but we're still getting kind of the best behaviour.
Remember that
Input
isnull
inside theOnGet
handler, so there's still a "correctness" agument to be made here, but that's essentially unavoidable.
We could revisit the InputModel
example, and make them required
instead of string?
too:
public class InputModel
{
[Required]
public required string FirstName { get; set; }
[Required]
public required string LastName { get; set; }
}
I'm unsure which is the best approach on this one, as these could be null
if you're using the values in an "error" path, so I think I prefer to mark those as nullable.
Wrapping up
In summary, the final Razor Page after adding all the extra annotations looks something like this:
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
namespace razor_page_nullability.Pages;
public class IndexModel : PageModel
{
[BindProperty]
public required InputModel Input { get; set; } // 👈 required
public string? Result { get; set; } // 👈 nullable
public void OnGet()
{
}
public void OnPost()
{
if (ModelState.IsValid)
{ // 👇 not null, dammit!
Result = $"Hello {Input.FirstName![0]}. {Input.LastName}";
}
}
public class InputModel
{
[Required]
public string? FirstName { get; set; } // 👈 nullable
[Required]
public string? LastName { get; set; } // 👈 nullable
}
}
I think this gives a reasonable balance of correctness and terseness, but I'm curious which approaches others using Razor Pages have settled on?
Summary
Nullable reference types provide a safety net in C#, to avoid the infamous NullreferenceException
. The problem is that they don't play well with Razor Pages, as the framework instantiates properties using reflection. This causes a lot of compiler warnings in a typical Razor Pages app. To avoid these warnings, you have to decorate your pages with ?
and !
, even if it's impossible for properties to be null thanks to the patterns used by Razor Pages.
In this post I discussed some of the options for working around these issues. I have tried to find the most practical approaches, which minimize typing, while also ensuring correctness wherever possible. My end solution of using required
and ?
is about as good as it's going to get I think.