Tips for deciding nullability
Tips for deciding nullability

Nullable Reference Migration – How to decide the nullability ?

In last few articles about nullable reference migrations, I tried to cover basics about this feature. I also tried to cover some tips for the nullable reference migration in an existing codebase. This article can be considered as an extension to that article.

When there is an existing codebase, which is being migrated to .NET 6, then the first and obvious question is – how to decide if an attribute / property should be marked as nullable ?

In this short article, we will try to see some approaches about deciding nullability of properties.

NRT – Quick Recap

  • The nullable reference types is a feature introduced in C# 8 and it will be mandatorily enabled for newly created projects targeting to .NET 6.
  • It can be enabled either on project level or it can be enabled on file level or on a particular code lines.
  • When enabled, all reference types are by default, non-nullable.
  • If we want to define any nullable property, then a question mark (?) should be used as suffix to the type name.
  • This feature is helpful to reduce null reference exceptions in the codebase.

Refer getting started and resolving nullable warnings articles for more details.

Nullability based on requirements

First approach that may come to our minds is – obviously – we should follow requirements for deciding on nullability. The idea here is –

  • For an object, if a property is required to have a non-null value, then that property is required. That means it should not be marked as nullable.
  • Otherwise, if a property is allowed to be null, then that particular property must be marked as nullable.

This approach works better for new development. But if you have a big existing codebase, then it may not be easy to adopt this approach.

Creating the nullability requirements, for implementations which were completed long in the past, would be a huge and daunting task. Also, based on size of codebase, the analysis of codebase / existing features / implementations may need a lot of time. This time would delay the nullability migrations.

So, for existing codebases, what is the best way to proceed ?

Nullability based on database columns

This is one of the approach that existing codebases can use. Mostly, all of the properties in POCO objects (directly or indirectly) get mapped to some column in the database. As we all know, the database columns have nullability associated with it. Some columns may not allow NULL values at all, while others may allow.

This whole approach is based on a premise that database design has already done the due diligence, to make sure that appropriate constraints are applied to each and every table and columns.

If the database design has some flaws, then this approach brings the risk – the nullability migration would also have the same flaws which are present in the database. But when you have requirements to correct the database constraints, you will also need to ensure that you correct the nullabilities in C# POCO classes.

My personal opinion is – this approach is safer for existing codebases. If any inconsistency is found in database design, it’s better to create a user story to correct the design in future.

I personally would recommend this approach only with SQL databases. But I am not sure how will it work with NoSQL databases. I would love to know your opinions about what approaches you used for nullable warnings migrations with NoSQL database.

Nullability on EF Core Code First properties

As we know, EF Core Code First properties basically corresponds to the database columns. So, if you use the approach of deciding nullability of POCO object properties, based on nullability constraints in database columns, you should ensure that you do not change nullability in database columns – otherwise you may end-up changing database as well as application layers, causing big bang of modifications – making the modifications hard to verify and test. This sentence is very big and it can also cause confusions. So, let me explain by providing an example.

Example

Let’s say, there is an EF Core entity, Student, as shown below. It has one integer property (let’s assume that it is a primary key) and one string property. These two properties basically create two columns in Students table. The integer column is non-nullable, while the string column would basically map to nvarchar(max) type and it would be marked as nullable. (just ignore the column length related best practice for the sake of this example).

EF Core Code First – before applying nullable reference types

Now, let’s say we want to apply the nullable reference type migrations to this code base. Now, let’s say we decide to take the approach of using nullability of database columns. In that approach, we decided to assign nullability of database column to C# objects.

Nullability of EF Core Entities after enabling NRT

In this case, the integer column is non-nullable and hence any properties which directly or indirectly map to int property, does not need any modifications. The string property is bit tricky. The string columns from EF Core code first entities, are marked as nullable columns in database. When you enable nullable reference types on EF Core entities C# project, the type ‘string‘ is non-nullable.

After NRT is enabled, the database table definition not matching with EF Core entity

That means, just after enabling nullable reference types, the EF Core entities nullability does not (or should I say may not! can you guess, why?) match the column definitions in the database. More specifically, the EF Core entities and EF Core migrations are not in sync just after enabling the nullable reference types on the project (or on all EF Core entities code files).

So, what can be done ?

Whenever you are using EF Core, it is better to start the nullable reference types migration from data access layer. Enable the feature on data access layer and then you will need to adjust datatypes of all nullable columns to ensure that there are no schema modifications. How can you verify that there are no modifications to the database ? Try to generate migration after adjusting the EF Core entities and it should be empty.

For instance, in the above example, if we do not mark string name property as nullable and try to generate migration, it would contain a statement to make name column non-nullable.

But if we make changes to the EF Core entity and use ‘string?‘ type instead of ‘string‘, and then, if we generate the migrations, the migration would not really contain anything as the C# type definitions are matching with database table definitions.

You can also specify nullability in entity configurations by marking a column as Required (or Optional). But, if you adjust the type itself after enabling nullable reference types, there is an additional advantage. It will always make sure that POCOs from all layers will have to follow those nullability constraints after the feature is enabled on the respective layers.

Adjusting EF Core entities to match database design

So, the crux here is, EF Core entities may be required to be adjusted to match existing database schema after enabling the nullable reference types. This will ensure that no new migrations are required. Alternatively, if you think EF Core entities are correct and database schema is wrong at some places, then migrations would anyway be needed. I would suggest to handle those migrations as a part of separate story as correcting database schema is completely independent from nullable reference type migration task.

Nullability based on API layer validations

If your application has web APIs or if it is a web application, it may have validations defined in the web layer. The validations might have been applied by using either fluent validation library, or the validations might have been applied by using attributes or by using some other approaches.

We can refer those validations and figure out which properties are marked as Required (or mandatory). Now those properties would be flowing from Web application layer to business layer to database layer. Based on those validations, nullability can be assigned to the corresponding properties in different layer.

For example, if StudentController has a HTTP POST endpoint, to create a student, it would accept a student entity as input parameter. This entity may have some validations defined. Some common validations can be:

  • a student must have a birthdate
  • a student must have a valid first name and valid last name
  • a student must provide his / her email address
  • a student may or may not provide his / her phone number

So, based on these validations, we can infer nullability of many properties which map to above mentioned properties. We can mark student birthdate to be non-nullable, student names should be non-nullable strings, student email address should be non-nullable string and student’s phone number should be a nullable string.

Based on the validations, the nullability sometimes can be inferred

I hope this example clarifies this approach.

Nullability for third party service calls

If the application code is calling some third party APIs, then that application may also have defined some objects to interact with those third party APIs. If you want to enable nullable reference types for such objects, it is easy if the third party API’s documentation is good. Based on the documentation, the object properties can be configured to allow (or disallow) NULLs.

Wrapping Up

This article can be very much overwhelming if you are new to the nullable reference types feature in .NET 6. I have tried my best to answer the question with which we started this article. What do you think ? Did you find these approaches helpful ? Or do you have any other ideas that I have not discussed in this article ?

Do not hesitate to comment and let me know your thoughts.

Leave a ReplyCancel reply