In recent articles, I have tried to explain about nullable reference types feature. First article explains what are nullable reference types and how to enable the feature. There is another article too which talks about how to fix the nullable reference types warnings.
Both the previous articles focus on technical aspects from knowing perspective. They may be helpful to understand what the feature is and what to expect if the feature is enabled. This article is going to be focused from the implementation perspective. This article will be helpful for more thoughts and information about how to estimate and execute nullable reference migration.
What are possible approaches for migration ?
Every application is physically organized into various code files (
.cs or .vb), projects (
.csproj) and solutions (
.sln). Every application also can be logically divided into multiple areas by functionalities / features.
Interesting thing is, nullable reference types also provides two ways for enabling that feature:
- We can either use
#nullabledirective to enable the feature in specific code files (or parts of code files).
- Another option is to enable the nullable reference types feature at project levels.
We can easily guess that there are two ways for us if we want to enable nullable reference types
- First option is to enable nullable reference types, by business functionality areas.
- Another option is to enable nullable reference types, “project by project”.
Let’s have a look at these approaches in detail, before
Option 1 – migrating by business functionality areas
If this path is chosen for nullable reference migration, then you may mostly need to use combination of compiler directive (#nullable) approach. The nullable reference feature would be enabled for only the snippet which is enclosed between
#nullable enable and
One of the advantage of using this approach is you can be completely sure about the functional areas in which code modifications happened. Note that nullable reference migration will not change the runtime behavior as long as you are just using annotations on variables and APIs. But for null checks, if you have added any conditional logic, then it may alter the runtime behavior.
SO, if this approach is used, the impacted functional areas are clear to everyone in the team. And the idea is, gradually whole codebase should have nullable reference types enabled. So the last step would be to remove any #nullable directives from the code and enable the setting at project level.
One of the drawback of this approach is – if some of the areas / files / code blocks were missed due to any reasons, then it can be identified only at the last story (assuming you do not enable project level setting till then).
Option 2 – migrating via project settings
This approach is bit straight forward from developer’s perspective. Start on one or multiple project. Set nullable reference types feature as enabled and enable setting to treat nullable warnings as errors. Then fix all those errors.
The advantage of this approach is – team is always confident about which projects have this new feature enabled and which projects does not.
The disadvantage of this approach is – a project file may contain code which impacts more than one functionalities. Hence, if any new logic was added to handle nulls, it may affect multiple functional areas.
Which factors may affect estimation ?
I think before looking at strategies which can be used for estimation of this migration, let’s have a look at some of the major things which will contribute towards the efforts.
One of the major factor that plays important role in fixing the nullable warnings is deciding nullability of all parameters and return types in all methods constructors, fields, properties, etc.
Some of the important factors are mentioned below:
- If application is using code first entity framework (or any other code first ORM)
- If application has private nuget packages repository for catering for some cross cutting concerns
- If application uses optional arguments, then you might have to set the appropriate default values
- If application uses fluent validations
In these cases, you will have to take extra care about when to mark a type as nullable and when to use null-forgiving operators.
What are my thoughts about strategy for migration?
In my opinion, it is better to use “project by project” migration approach for following reasons.
In “project by project” migration, separate final user-story is not required to clean up directives. There will be no surprizes at the end because team is always sure about state of migration at the end of every project’s migration.
Below is one example of how this migration can be executed:
- Start with private nuget package repository. It is because, mostly these packages would not have dependency on database or any internal APIs. So these packages can be enabled
- Then you can begin data access layer migration. It is because it is easier to decide nullability for data layer objects. You can assume current database design as source of truth. If you find any discrepancies or improvements to be done in database design / data layer objects, I would suggest to create separate story for that. This can be a single story or multiple stories depending on amount of work / number of projects.
- Then Business layer can be migrated and all nullable warnings can be resolved. Deciding nullability of properties / fields in business objects may be more time-consuming, if there is no one-to-one mapping from business object to data layer objects. And that’s why this step may take more time than other steps mentioned here.
- Then presentation layer logic / API layer logic should be migrated. Here, mandatory fields on UI / API input messages, can be helpful to decide nullability.
I want to mention it again, that the above strategy is a basic and simplistic strategy just to give an idea about how the migration can be executed. Of course you know more about your application’s architecture, and hence you are in better position to decide how you want to proceed on this activity.
Approach for estimating this migration
If you have followed this article till this section, then it means you have fair idea about the kind of work that needs to be done in this activity. Now, let’s discuss how this work can be estimated.
You can create a timeboxed investigation task to check how this migration can be done. You can try to enable this feature and see how many warnings do you see. Do not directly estimate based on number of warnings. This is because, sometimes fixing a warning may generate some new warnings. Or sometimes, fixing one warning can reduce multiple warnings.
Try to resolve some of the warnings in one of the small projects. That should
After time boxed activity is complete, you should have sufficient information about how much time is generally needed to fix all the warnings. Then you can create the user stories and assign appropriate efforts.
Sometimes, application may be divided into multiple solutions / processes / services etc. In that case, you can obviously learn lessons while performing these migrations, which will provide you good insights about estimation of this task.
How to perform testing and verifications ?
Whenever it comes to verifying technical refactoring / modifications, I would recommend you to rely on your unit tests / integration tests / E2E tests. If there are any areas where behaviour of code is modified by introducing additional null checks, make sure that you have added / modified appropriate tests.
After migration is complete…
This migration can be a huge task , obviously depending on the size of your codebase. Hence, practically, it is very much possible, that some nullability assignments were incorrect at the time of migration or they were correct at that time, but now the semantics have changed.
In both cases, developers should keep their eyes open to make sure that nullability assignments are appropriate. Note that this is not silver bullet, which will ensure that you do not get null reference exceptions at all after adopting this feature. But if nullability assignments are appropriate, then it would certainly help in reducing those occurrences and thereby will help organizations to save some time and money which otherwise would have been spent on those exceptions.
So, these are my thoughts about how migrations can be estimated and executed. Of course, these are my opinions and these opinions may not fit to all domains and all types of application designs / architectures. Do you have any ideas that you want to share ? Let me know your thoughts.