.NET Tools
Essential productivity kit for .NET and game developers
Refactor code to use new C# language features
Modernizing, or migrating to modern language features in any language can help make code more readable, efficient and secure. In this blog post, we’ll look at ways to modernize code so it’s better than before.
Take advantage of modern language features
Why refactor code just to modernize it? Why fix it if it’s not broken? Sometimes, there are good reasons to upgrade or migrate to a newer version of a platform or language. For example, languages evolve to add features that take advantage of operating system features, device and browser capabilities, or cloud or other technologies. As well, language updates contain bug fixes, cleaner syntaxes and syntactic sugar, and more efficient ways of working with objects and data.
Refactoring that modernizes code can drastically improve its quality by improving readability, and maintainability. This is because as languages evolve, the newly added syntaxes tend to favor fewer lines of code and simpler code you must write.
Tools like ReSharper and Rider can help with modernizing your code base. They are up-to-date with new features in .NET languages, and can suggest opportunities to make the best use of them. As an added benefit, these suggestions help keep your team’s C# skills up-to-date.
Top-level Statements
Top-level statements, introduced in C# 9, let you start writing code immediately without the ceremony of explicitly defining namespaces or classes. Before top-level statements, even the most basic of apps that prints a single “Hello World” style message comes with three levels of nested brackets to support one single line of actual running code. That’s a lot of unnecessary boilerplate code. Do we really need it?
using System; namespace HelloWorld { class Program { static void Main(string[] args) { Console.WriteLine("Hello, modernized world!"); } } }
Nope! We don’t need all that extra code at all. With top-level statements we can delete every line of code in the Hello World app except the one line of code that actually does something.
System.Console.WriteLine("Hello, modernized world!");
For those who want to use short names, the code looks like this:
using static System.Console; WriteLine("Hello, modernized world!");
Using the new project template (File | New Project) uses top-level statements by default. To migrate your code to use top-level statements, delete the namespace and class declarations as well as the main function. Leave only the main function’s body. It’s fun to delete code but it’s easy to miss a bracket, so use Alt+Enter on the class that contains Main
and choose Convert to top-level code. Voilá! Everything is updated.
Top-Level statements were first introduced in C# 9.
File-Scoped Namespaces
Namespaces are a great way to name, organize, and access objects, but they don’t have to be in your face all the time. A common file organization pattern in .NET is to use one class per file but is it really necessary to enclose a single class in a namespace? This is where file-scoped namespaces come in handy. Much like with top-level statements, you’ll have one less level of indentation to worry about. For the syntax: Declare a namespace at the top of a file and immediately terminate with a semicolon and you’re done. No curly brackets! Like so:
namespace MealPlanner.Menu;
And that’s it. This single line of code tells the compiler to include everything in the file as part of the MealPlanner.Menu
namespace. Converting to a file-scoped namespace using Rider is a matter of pressing Alt+Enter on the namespace declaration and choosing To file-scoped namespace. Select whether file-scoped namespaces should apply to only this file or upgrade the entire solution.
File-Scoped namespaces made their debut into C# in version 10.
Init-only setters and indexers
There are times when you want to instantiate an object, but its data should be immutable (unchangeable). Perhaps you have a DTO or an object representing a table in a database. An object like this might contain names of tables and fields, and therefore the object should be immutable. There was no way to initialize this kind of read-only data from outside the constructor. So in earlier versions of C#, you would declare and initialize objects in the constructor and make the property read-only by creating only a getter with no setter, similar to the following code:
class Column { public Column(string ColumnName, string DataType, string IsPrimaryKey) { ColumnName = "Column1"; DataType = "varchar(20)"; IsPrimaryKey = false; } public string ColumnName { get; } public string DataType { get; } public bool IsPrimaryKey { get; } } var c = new Column();
However, the classic way to instantiate and initialize means that the data must be set from inside the class, and the constructor is the only place. Often, data needs to be set from outside the class, particularly if using objects that represent database structures like in this example. Init-only properties allow us to create objects with read-only properties that are initialized by the calling code. It’s more flexible than before.
To modernize code to use an init-only setter, create a property with a getter and use the init
keyword, rather than set
for a setter (or delete the set
statements entirely). More likely, you’ll want to upgrade these in bulk – so press Alt+Enter and choose To init accessor, then choose the scope: file, project, or solution. Then, the consumer must initialize the class when it makes an instance of it or face a compiler error. Notice the reduction in lines of code. In the following animation, the code is grayed out as an indication that it can be safely deleted. It’s no longer needed or used.
class Column { public string ColumnName { get; init; } public string DataType { get; init; } public bool IsPrimaryKey { get; init; } } var c = new Column { ColumnName = "First_Name" };
C# 9 is the version where Init-only properties were introduced.
Records and Record Structs
Records are a way to make an entire object immutable as opposed to individual properties that are found in init-only properties. Records are distinct from classes because record types use value-based equality. Therefore, objects that need value comparisons for all properties generally migrate well to a record or record struct. Usually, issues with mutable types show up in programs with high concurrency and shared data.
Record declarations are almost syntactically identical to a class – only the keyword is different. To update older code, replace the class
keyword with record
, delete the constructor, and initialize the properties inline. All this can be done with one keystroke: Alt+Enter (make sure the caret is on a class name). Select To record. Rider does the complete conversion. Now that the class is refactored as a record
, notice that the constructor is grayed out so that it can be converted to a primary constructor (coming up next).
C# 9 brought us records, and C# 10 record structs.
Primary Constructors
As C# evolves, less code is needed to accomplish the same tasks as before. This means there’s a lot of unnecessary boilerplate code that is abstracted away, out of sight and out of mind. This is nice because that kind of code tends to get in the way and doesn’t add a whole lot of business value.
Once you’ve moved from a class to a record by changing the keyword class
to record
(or the same with struct
to record struct
), the constructor and setters are no longer needed. At this point you could just delete it. But if you need to set those values use a primary constructor.
To migrate to a primary constructor, place the caret on the existing constructor, press Alt+Enter and select the appropriate Convert to primary constructor action. Choose what you want to update: the current object, current file, project, or solution.
Primary constructors made their entrance in C# in version 9.
Modern Switch Expression
Switch statements evaluate an expression from a list of values. Types such as string
, int
, and enum
are often the types used in the evaluations and they tend to be simple. However, a switch expression evaluates an expression from a list of candidate expressions based on a pattern match. It can evaluate more expressions in different ways (e.g., using lambdas) and is more flexible than before. Not all switch statements are best suited to be a switch expression, but some developers simply enjoy the more short and concise syntax enough to warrant an upgrade.
Convert a switch statement to a switch expression by pressing Alt+Enter while the caret is on the switch
keyword and choose the option to convert.
C# 8 was the version that switched direction to bring us the switch expression.
Nullable reference types
In many languages including earlier versions of C# it was given that you would write null checks in your code. In many circles, dutifully checking for nulls is considered “good, defensive code”. But this activity can be abstracted. Compilers and tools can easily do these checks for us now in 2022. A common theme regarding application development is that programming languages should enable developers to focus on solving business problems. Otherwise they spend the bulk of their time recreating or fixing common technical scenarios: in this case, null checking.
While there are many considerations regarding nullability when updating code, there are some quick fixes that should be low hassle and get you started. For example, perhaps during refactoring you’ve applied the ?
character to an argument in a constructor to denote nullability, but forgot to apply it to the corresponding property. No worries, you’ll see an indicator reminding you of the necessary updates in the editor. Press Alt+Enter to choose an action to take. In this example, the property is set to nullable to make the constructor’s argument.
In the meantime, read more about migrating to nullable reference types, how they work under the hood, and how to upgrade your code to use nullable reference types.
Nullable reference types got their start in C# 9.
Conclusion
This blog post has shown you how to refactor code for modernization with the help of tools like ReSharper and Rider. It’s not necessary to always use the latest version of a language, but it’s important to refactor code to make it more readable and efficient. At some point, that means an update. Good tools can provide assistance with refactoring and updating legacy code, migrating, and modernizing codebases. Check out our .NET tools and let us know how you plan to modernize your code.