Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Fighting with nullable reference types in Razor Pages

$
0
0

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 a Post handler
  • A binding model defined as a nested type InputModel which binds to the property Input
  • 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:

The razor page rendered

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 always null in OnGet
  • Input is never null in OnPost

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:

  1. You have to add it to every single page.
  2. 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 is null inside the OnGet 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.


Viewing all articles
Browse latest Browse all 743

Trending Articles