Tutorial: Migrate existing code with nullable reference types

C# 8 introduces nullable reference types, which complement reference types the same way nullable value types complement value types. You declare a variable to be a nullable reference type by appending a ? to the type. For example, string? represents a nullable string. You can use these new types to more clearly express your design intent: some variables must always have a value, others may be missing a value. Any existing variables of a reference type would be interpreted as a non-nullable reference type.

In this tutorial, you'll learn how to:

  • Enable null reference checks as you work with code.
  • Diagnose and correct different warnings related to null values.
  • Manage the interface between nullable enabled and nullable disabled contexts.
  • Control nullable annotation contexts.

Prerequisites

You'll need to set up your machine to run .NET Core, including the C# 8.0 beta compiler. The C# 8 beta compiler is available with Visual Studio 2019, or the latest .NET Core 3.0 preview.

This tutorial assumes you're familiar with C# and .NET, including either Visual Studio or the .NET Core CLI.

Explore the sample application

The sample application that you'll migrate is an RSS feed reader web app. It reads from a single RSS feed and displays summaries for the most recent articles. You can click on any of the articles to visit the site. The application is relatively new but was written before nullable reference types were available. The design decisions for the application represented sound principles, but don't take advantage of this important language feature.

The sample application includes a unit test library that validates the major functionality of the app. That project will make it easier to upgrade safely, if you change any of the implementation based on the warnings generated. You can download the starter code from the dotnet/samples GitHub repository.

Your goal migrating a project should be to leverage the new language features so that you clearly express your intent on the nullability of variables, and do so in such a way that the compiler doesn't generate warnings when you have the nullable annotation context and nullable warning context set to enabled.

Upgrade the projects to C# 8

A good first step is to determine the scope of the migration task. Start by upgrading the project to C# 8.0 (or newer). Add the LangVersion element to both csproj files for the web project and the unit test project:

<LangVersion>8.0</LangVersion>

Upgrading the language version selects C# 8.0, but does not enable the nullable annotation context or the nullable warning context. Rebuild the project to ensure that it builds without warnings.

A good next step is to turn on the nullable annotation context and see how many warnings are generated. Add the following element to both csproj files in the solution, directly under the LangVersion element:

<NullableContextOptions>enable</NullableContextOptions>

Do a test build, and notice the warning list. In this small application, the compiler generates five warnings, so it's likely you'd leave the nullable annotation context enabled and start fixing warnings for the entire project.

That strategy works only for smaller projects. For any larger projects, the number of warnings generated by enabling the nullable annotation context for the entire codebase makes it harder to fix the warnings systematically. For larger enterprise projects, you'll often want to migrate one project at a time. In each project, migrate one class or file at a time.

Warnings help discover original design intent

There are two classes that generate multiple warnings. Start with the NewsStoryViewModel class. Remove the NullableContextOptions element from both csproj files so that you can limit the scope of warnings to the sections of code you're working with. Open the NewsStoryViewModel.cs file and add the following directives to enable the nullable annotation context for the NewsStoryViewModel and restore it following that class definition:

#nullable enable
public class NewsStoryViewModel
{
    public DateTimeOffset Published { get; set; }
    public string Title { get; set; }
    public string Uri { get; set; }
}
#nullable restore

These two directives help you focus your migration efforts. The nullable warnings are generated for the area of code you're actively working on. You'll leave them on until you're ready to turn on the warnings for the entire project. You should use the restore rather than disable value so that you don't accidentally disable the context later when you've turned on nullable annotations for the entire project. Once you've turned on the nullable annotation context for the entire project, you can remove all the #nullable pragmas from that project.

The NewsStoryViewModel class is a data transfer object (DTO) and two of the properties are read/write strings:

public class NewsStoryViewModel
{
    public DateTimeOffset Published { get; set; }
    public string Title { get; set; }
    public string Uri { get; set; }
}

These two properties cause CS8618, "Non-nullable property is uninitialized". That's clear enough: both string properties have the default value of null when a NewsStoryViewModel is constructed. What's important to discover is how NewsStoryViewModel objects are constructed. Looking at this class, you can't tell if the null value is part of the design, or if these objects are set to non-null values whenever one is created. The news stories are created in the GetNews method of the NewsService class:

ISyndicationItem item = await feedReader.ReadItem();
var newsStory = _mapper.Map<NewsStoryViewModel>(item);
news.Add(newsStory);

There's quite a bit going on in the preceding block of code. This application uses the AutoMapper NuGet package to construct a news item from an ISyndicationItem. You've discovered that the news story items are constructed and the properties are set in that one statement. That means the design for the NewsStoryViewModel indicates that these properties should never have the null value. These properties should be nonnullable reference types. That best expresses the original design intent. In fact, any NewsStoryViewModel is correctly instantiated with non-null values. That makes the following initialization code a valid fix:

public class NewsStoryViewModel
{
    public DateTimeOffset Published { get; set; }
    public string Title { get; set; } = default!;
    public string Uri { get; set; } = default!;
}

The assignment of Title and Uri to default which is null for the string type doesn't change the runtime behavior of the program. The NewsStoryViewModel is still constructed with null values, but now the compiler reports no warnings. The null-forgiving operator, the ! character following the default expression tells the compiler that the preceding expression is not null. This technique may be expedient when other changes force much larger changes to a code base, but in this application there is a relatively quick and better solution: Make the NewsStoryViewModel an immutable type where all the properties are set in the constructor. Make the following changes to the NewsStoryViewModel:

#nullable enable
    public class NewsStoryViewModel
    {
        public NewsStoryViewModel(DateTimeOffset published, string title, string uri) =>
            (Published, Title, Uri) = (published, title, uri);

        public DateTimeOffset Published { get; }
        public string Title { get; }
        public string Uri { get; }
    }
#nullable restore

Once that's done, you need to update the code that configures the AutoMapper so that it uses the constructor rather than setting properties. Open NewsService.cs and look for the following code at the bottom of the file:

public class NewsStoryProfile : Profile
{
    public NewsStoryProfile()
    {
        // Create the AutoMapper mapping profile between the 2 objects.
        // ISyndicationItem.Id maps to NewsStoryViewModel.Uri.
        CreateMap<ISyndicationItem, NewsStoryViewModel>()
            .ForMember(dest => dest.Uri, opts => opts.MapFrom(src => src.Id));
    }
}

That code maps properties of the ISyndicationItem object to the NewsStoryViewModel properties. You want the AutoMapper to provide the mapping using a constructor instead. Replace the above code with the following automapper configuration:

#nullable enable
    public class NewsStoryProfile : Profile
    {
        public NewsStoryProfile()
        {
            // Create the AutoMapper mapping profile between the 2 objects.
            // ISyndicationItem.Id maps to NewsStoryViewModel.Uri.
            CreateMap<ISyndicationItem, NewsStoryViewModel>()
                .ForCtorParam("published", opt => opt.MapFrom(src => src.Published))
                .ForCtorParam("title", opt => opt.MapFrom(src => src.Title))
                .ForCtorParam("uri", opt => opt.MapFrom(src => src.Id));
        }

Notice that because this class is small, and you've examined carefully, you should turn on the #nullable enable directive above this class declaration. The change to the constructor could have broken something, so it's worthwhile to run all the tests and test the application before moving on.

The first set of changes showed you how to discover when the original design indicated that variables shouldn't be set to null. The technique is referred to as correct by construction. You declare that an object and its properties cannot be null when it's constructed. The compiler's flow analysis provides assurance that those properties aren't set to null after construction. Note that this constructor is called by external code, and that code is nullable oblivious. The new syntax doesn't provide runtime checking. External code might circumvent the compiler's flow analysis.

Other times, the structure of a class provides different clues to the intent. Open the Error.cshtml.cs file in the Pages folder. The ErrorViewModel contains the following code:

public class ErrorModel : PageModel
{
    public string RequestId { get; set; }

    public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    public void OnGet()
    {
        RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
    }
}

Add the #nullable enable directive before the class declaration, and a #nullable restore directive after it. You'll get one warning that RequestId is not initialized. By looking at the class, you should decide that the RequestId property should be null in some cases. The existence of the ShowRequestId property indicates that missing values are possible. Because null is valid, add the ? on the string type to indicate the RequestId property is a nullable reference type. The final class looks like the following example:

#nullable enable
    public class ErrorModel : PageModel
    {
        public string? RequestId { get; set; }

        public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

        public void OnGet()
        {
            RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
        }
    }
#nullable restore

Check for the uses of the property, and you see that in the associated page, the property is checked for null before rendering it in markup. That's a safe use of a nullable reference type, so you're done with this class.

Fixing nulls causes change

Frequently, the fix for one set of warnings creates new warnings in related code. Let's see the warnings in action by fixing the index.cshtml.cs class. Open the index.cshtml.cs file and examine the code. This file contains the code behind for the index page:

public class IndexModel : PageModel
{
    private readonly NewsService _newsService;

    public IndexModel(NewsService newsService)
    {
        _newsService = newsService;
    }

    public string ErrorText { get; private set; }

    public List<NewsStoryViewModel> NewsItems { get; private set; }

    public async Task OnGet()
    {
        string feedUrl = Request.Query["feedurl"];

        if (!string.IsNullOrEmpty(feedUrl))
        {
            try
            {
                NewsItems = await _newsService.GetNews(feedUrl);
            }
            catch (UriFormatException)
            {
                ErrorText = "There was a problem parsing the URL.";
                return;
            }
            catch (WebException ex) when (ex.Status == WebExceptionStatus.NameResolutionFailure)
            {
                ErrorText = "Unknown host name.";
                return;
            }
            catch (WebException ex) when (ex.Status == WebExceptionStatus.ProtocolError)
            {
                ErrorText = "Syndication feed not found.";
                return;
            }
            catch (AggregateException ae)
            {
                ae.Handle((x) =>
                {
                    if (x is XmlException)
                    {
                        ErrorText = "There was a problem parsing the feed. Are you sure that URL is a syndication feed?";
                        return true;
                    }
                    return false;
                });
            }
        }
    }
}

Add the #nullable enable directive and you'll see two warnings. Neither the ErrorText property nor the NewsItems property is initialized. An examination of this class would lead you to believe that both properties should be nullable reference types: Both have private setters. Exactly one is assigned in the OnGet method. Before making changes, look at the consumers of both properties. In the page itself, the ErrorText is checked against null before generating markup for any errors. The NewsItems collection is checked against null, and checked to ensure the collection has items. A quick fix would be to make both properties nullable reference types. A better fix would be to make the collection a nonnullable reference type, and add items to the existing collection when retrieving news. The first fix is to add the ? to the string type for the ErrorText:

public string? ErrorText { get; private set; }

That change won't ripple through other code, because any access to the ErrorText property was already guarded by null checks. Next, initialize the NewsItems list and remove the property setter, making it a readonly property:

public List<NewsStoryViewModel> NewsItems { get; } = new List<NewsStoryViewModel>();

That fixed the warning but introduced an error. The NewsItems list is now correct by construction, but the code that sets the list in OnGet must change to match the new API. Instead of an assignment, call AddRange to add the news items to the existing list:

NewsItems.AddRange(await _newsService.GetNews(feedUrl));

Using AddRange instead of an assignment means that the GetNews method can return an IEnumerable instead of a List. That saves one allocation. Change the signature of the method, and remove the ToList call, as shown in the following code sample:

public async Task<IEnumerable<NewsStoryViewModel>> GetNews(string feedUrl)
{
    var news = new List<NewsStoryViewModel>();
    var feedUri = new Uri(feedUrl);

    using (var xmlReader = XmlReader.Create(feedUri.ToString(),
           new XmlReaderSettings { Async = true }))
    {
        try
        {
            var feedReader = new RssFeedReader(xmlReader);

            while (await feedReader.Read())
            {
                switch (feedReader.ElementType)
                {
                    // RSS Item
                    case SyndicationElementType.Item:
                        ISyndicationItem item = await feedReader.ReadItem();
                        var newsStory = _mapper.Map<NewsStoryViewModel>(item);
                        news.Add(newsStory);
                        break;

                    // Something else
                    default:
                        break;
                }
            }
        }
        catch (AggregateException ae)
        {
            throw ae.Flatten();
        }
    }

    return news.OrderByDescending(story => story.Published);
}

Changing the signature breaks one of tests as well. Open the NewsServiceTests.cs file in the Services folder of the SimpleFeedReader.Tests project. Navigate to the Returns_News_Stories_Given_Valid_Uri test and change the type of the result variable to IEnumerable<NewsItem>. Changing the type means the Count property is no longer available, so replace the Count property in the Assert with a call to Any():

// Act
IEnumerable<NewsStoryViewModel> result =
    await _newsService.GetNews(feedUrl);

// Assert
Assert.True(result.Any());

You'll need to add a using System.Linq statement to the beginning of the file as well.

This set of changes highlights special consideration when updating code that includes generic instantiations. Both the list and the elements in the list of non-nullable types. Either or both could be nullable types. All the following declarations are allowed:

  • List<NewsStoryViewModel>: nonnullable list of nonullable view models.
  • List<NewsStoryViewModel?>: nonnullable list of nullable view models.
  • List<NewsStoryViewModel>?: nullable list of nonnullable view models.
  • List<NewsStoryViewModel?>?: nullable list of nullable view models.

Interfaces with external code

You've made changes to the NewsService class, so turn on the #nullable enable annotation for that class. This won't generate any new warnings. However, careful examination of the class helps to illustrate some of the limitations of the compiler's flow analysis. Examine the constructor:

public NewsService(IMapper mapper)
{
    _mapper = mapper;
}

The IMapper parameter is typed as a nonnullable reference. It's called by ASP.NET Core infrastructure code, so the compiler doesn't really know that the IMapper will never be null. The default ASP.NET Core dependency injection (DI) container throws an exception if it can't resolve a necessary service, so the code is correct. The compiler can't validate all calls to your public APIs, even if your code is compiled with nullable annotation contexts enabled. Furthermore, your libraries may be consumed by projects that have not yet opted into using nullable reference types. Validate inputs to public APIs even though you've declared them as nonnullable types.

Get the code

You've fixed the warnings you identified in the initial test compile, so now you can turn on the nullable annotation context for both projects. Rebuild the projects; the compiler reports no warnings. You can get the code for the finished project in the dotnet/samples GitHub repository.

The new features that support nullable reference types help you find and fix potential errors in how you handle null values in your code. Enabling the nullable annotation context allows you to express your design intent: some variables should never be null, other variables may contain null values. These features make it easier for you to declare your design intent. Similarly, the nullable warning context instructs the compiler to issue warnings when you have violated that intent. Those warnings guide you to make updates that make your code more resilient and less likely to throw a NullReferenceException during execution. You can control the scope of these contexts so that you can focus on local areas of code to migrate while the remaining codebase is untouched. In practice, you can make this migration task a part of regular maintenance to your classes. This tutorial demonstrated the process to migrate an application to use nullable reference types. You can explore a larger real-world example of this process by examining the PR Jon Skeet made to incorporate nullable reference types into NodaTime.