Here are some of my favorite new features in C# 10 and how I see myself using them.
In a previous post, I talked about all of the new features of C# 9. With the release of .NET 6 recently, I wanted to share some of the new language features of C# 10.
Let’s take a look at some of the new language features.
.NET added quite a few features to the language that can save you a lot of time.
In my opinion, file-scoped namespaces are a great way to organize your code. They allow you to organize your code into logical groups and keep your code from being too cluttered.
File-scoped namespaces allow you to save some keystrokes and indentation in your code. Now, you can declare your namespace at the top of your file, assuming you only have one namespace for your file. Which I believe you should always do.
Old code:
namespace MyNamespace
{
class MyClass
{
public void MyMethod()
{
// ...
}
}
}
Now becomes:
namespace MyNamespace;
class MyClass
{
public void MyMethod()
{
// ...
}
}
Now we save two curly braces and one indentation level. I kind of wish this feature was in .NET 1, since you really should only have one namespace per file. 😄
How often do you see or type the same namespaces over and over again? using System;
, for me, is declared in almost every file in my project. With C# 10’s Global Using Directives, you can declare your using
directives at the top of your file and then use them throughout your file. Now I can add global using System;
to one file in my project, and the using
statement will be referenced throughout all my files/classes.
I see myself using the following code in my project regularly now:
global using System;
global using System.Collections.Generic;
global using System.Linq;
While not required, I recommend that you place all of your global using directives in a standard filename across your projects. I plan on using GlobalUsings.cs
, but feel free to use whatever you want.
If putting your global using
directives in a file is not your preference, you can also add them to your .csproj
file. If I wanted to include the three global using
directives above in my .csproj
file, I would add the following to my .csproj
file:
<ItemGroup>
<Using Include="System" />
<Using Include="System.Collections.Generic" />
<Using Include="System.Linq" />
</ItemGroup>
Either approach will work, but the .csproj
approach seems to be easier to discover.
If global using
is not your or your team’s thing, you can disable it by adding the following to your .csproj
file:
<PropertyGroup>
<ImplicitUsings>disable</ImplicitUsings> // Can also be set to `false`
</PropertyGroup>
Pattern Matching was introduced in C# 7. It allows you to match the properties of an object against a pattern. Pattern matching is a great way to write cleaner code. In C# 8, the Property Patterns feature was added, which enabled you to match against properties of an object like this:
Person person = new Person {
FirstName = "Joe",
LastName = "Guadagno",
Address = new Address {
City = "Chandler",
State = "AZ"
}
}
// Other code
if (person is Person {Address: {State: "AZ"}})
{
// Do something
}
Now with C# 10, you can reference nested properties of objects with dot notation. For example, you can match against the City
and State
properties of a Person
object like this:
if (person is Person {Address.State: "AZ"})
{
// Do something
}
C# 10 made improvements to interpolated strings in C# 10. const
variables can now be used with interpolated strings.
I have trouble finding a “real world” example of this, so here is an example of how it works:
const string greeting = "Hello";
const string name = "Joe";
const string message = $"{greeting}, {name}!";
The message
variable will be the value of Hello, Joe!
.
Interpolation has not just been improved for const
s but for variables that can be determined at compile time. Let’s say you maintain a library, and you decide to obsolete a method named OldMethod
. In the past, you would have to do something like this:
public class MyClass
{
[Obsolete($"Use NewMethod instead", true)]
public void OldMethod() { }
public void NewMethod() { }
}
But now, you can do this:
public class MyClass
{
[Obsolete($"Use {nameof(NewMethod)} instead", true)]
public void OldMethod() { }
public void NewMethod() { }
}
This makes it easier to update your code when you need to. Now you don’t have to remember everywhere you used hardcoded name of the method you want to obsolete.
CallerArgumentExpression
attribute is a new feature of C# 10 that enables you to capture the expression that is passed into a method which is useful for debugging purposes.
Let’s say we have a method called IsValid
that checks and validates assorted properties of a Person
object.
public static class Validation {
public static book IsValid(Person person)
{
Debug.Assert(person != null);
Debug.Assert(!string.IsNullOrEmpty(person.FirstName));
Debug.Assert(!string.IsNullOrEmpty(person.LastName));
Debug.Assert(!string.IsNullOrEmpty(person.Address.City));
Debug.Assert(person.Age > 18);
return true;
}
}
Now we have the following code that calls the IsValid
method:
Person person;
var result = Validation.IsValid(person); // Fails: person != null
Person person = new Person{
FirstName = "Joe",
LastName = "Guadagno",
Address = new Address {
City = "Chandler",
State = "AZ"
},
Age = 17
};
result = Validation.IsValid(person); // Fails: person.Age > 18
Each call will fail because at least one assertion fails. But which one failed? That is where CallerArgumentExpression
comes into play. To fix this, we’ll create a custom Assert
method and add the CallerArgumentExpression
attribute to the method:
public static void Assert(bool condition, [CallerArgumentExpression("condition")] string expression = default)
{
if (!condition)
{
Console.WriteLine($"Condition failed: {expression}");
}
}
Now if we call the IsValid
method with the above sample, we’ll get the following output:
Condition failed: person != null
and
Condition failed: person.Age > 18)
The introduction of CallerArgumentExpression
attribute has enabled a few new extensions methods to the framework. For example, there is now a ThrowIfNull
extension method that can be used to throw an ArgumentNullException
if the argument is null.
We no longer have to write this:
if (argument is null)
{
throw new ArgumentNullException(nameof(argument));
}
We can now write this:
ArgumentNullException.ThrowIfNull(argument);
The method, behind the scenes, looks like this:
public static void ThrowIfNull(
[NotNull] object? argument,
[CallerArgumentExpression("argument")] string? paramName = null)
{
if (argument is null)
{
throw new ArgumentNullException(paramName);
}
}
This is not an exhaustive list of new language features introduced in C# 10. To see what else was added to C# 10, check out What’s new in C# 10.0.
Joe Guadagno is a Director of Engineering at Rocket Mortgage, the US’s largest mortgage lender, based in Detroit, Michigan. He has been writing software for over 20 years, has been an active member of the .NET community, serving as a Microsoft MVP in .NET for more than 10 years. At Rocket Mortgage, he leads three software development teams building and modernizing internal services. He has spoken throughout the United States and at international events on topics ranging from Microsoft .NET, Microsoft Azure, Ionic, Bootstrap and many others (see the complete list). When not sitting at a computer, Joe loves to hang out with his family and play games, as well as checking out the latest in Home Automation. You can connect with Joe on Twitter at @jguadagno, Facebook at JosephGuadagnoNet, and on his blog at https://www.josephguadagno.net.