In this article, we are going to take a look at what’s new in C# 10. .NET 6 supports the latest C# version, and to use it in our projects, we will need the latest .NET 6 SDK or Visual Studio 2022 which includes the SDK.

To download the source code for this article, you can visit our GitHub repository.

So, let’s start.

Record Structs in C# 10

Beginning with C# 9, we could use the record keyword to define ‘out-of-the-box’ immutable reference type objects in our code with value equality semantics.

From now on, we can declare value type records with record struct keywords to have them behave like other value types. If we omit struct, they will be reference types, but we can be more explicit and write record class to achieve the same behavior.

Custom Interpolated String Handlers

The string interpolation handler is a type that processes the placeholder expression in an interpolated string:

[InterpolatedStringHandler]
public ref struct CustomInterpolatedStringHandler
{
    StringBuilder builder;

    public CustomInterpolatedStringHandler(int literalLength, int formattedCount)
    {
        builder = new StringBuilder(literalLength);
    }

    public void AppendLiteral(string s)
    {
        builder.Append(s);
    }

    public void AppendFormatted<T>(T t)
    {
        if (t is string)
        {
            var s = t?.ToString() ?? string.Empty;
            var notToken = "not ";
            var index = s.IndexOf(notToken);

            builder.Append(index < 0 ? s : s.Remove(index, notToken.Length));
        }
        else
        {
            builder.Append(t?.ToString());
        }
    }

    public string GetFormattedText() => builder.ToString();
}

We are just going to note that we need a constructor with at least two arguments. The first two arguments in the constructor are integer constants populated by the compiler, representing the literal length of the interpolation, and the number of interpolation components.

With this implementation in place, we can modify the Program class:

var attribute = "not awesome";

var processedMessage = ProcessMessage($"Code maze is {attribute}.");

string ProcessMessage(CustomInterpolatedStringHandler builder) => builder.GetFormattedText();

Now, as a result, the value of the processedMessage variable is "Code maze is awesome." and we can see that we change how the placeholder is shown in our implementation.

Global Using Directives in C# 10

We don’t need to accumulate using directives at the beginning of each file. We can extract using directives that we currently have in our Program and CustomInterpolatedStringHandler files.

To do that, let’s create the Usings.cs file with all the using directives:

global using System.Diagnostics;
global using System.Runtime.CompilerServices;
global using System.Text;
global using WhatsNewInCSharp10;

With this approach, we can extract all global usings to one file and use it in our project.

But that’s not all. A new feature in the .NET 6 SDK is to support the implicit global using directives. That is one of the reasons why we can have Minimal APIs up and running with just four lines of code.

Extended Property Patterns

We are able to reference nested properties or fields within a property pattern. You can read more about it in our Extended Property Patterns in C# article.

Improvements of Structure Types

In order to support record struct we also got improvements for ‘normal’ struct types. struct types can have a parameterless constructor and can initialize an instance field or property at its declaration:

public struct Maze
{
    // Parameterless constructor with property initialization 
    public Maze()
    {
        Size = 10;
    }
    
    // Initialization of the property at its declaration
    public int Size { get; set; } = 10;
}

File-scoped Namespace Declaration in C# 10

We don’t need to put our code in indented namespace code blocks anymore:

namespace WhatsNewInCSharp10;

public struct Maze
{
    …
} 

As we can see, we don’t have those parentheses for a namespace that we had in previous C# versions. 

Lambda Expression Improvements

Lambda expressions are now more similar to methods and local functions. They can have a natural type, and the compiler will infer a delegate type from the lambda expression or a method group. Also, we can apply attributes to lambda expressions. To show these improvements in action let’s write one lambda method:

var lambda = [DebuggerStepThrough]() => "Hello world";

In the previous version of C#, we would get a compiler error that we cannot assign lambda expression to an implicitly-typed variable and that lambda attributes are not supported.

Constant Interpolated Strings

From C#10, we are able to generate constants from interpolated constant strings.

To show that, let’s initialize $"Code maze is {constantAttribute}." interpolated string as a const:

const string constantAttribute = "awesome";
const string constantMessage = $"Code maze is {constantAttribute}.";

We are able to do that if our placeholder is also a const.

Record Types Can Seal ToString in C# 10

When we are using record types in our code, the compiler is generating an implementation of the ToString method for us.

Let’s create a simple hierarchy of record types:

public record Article(string Author, string Title);

public record CodeMazeArticle(string Author, string Title, string Comment) : Article(Author, Title);

Since we don’t want to include Comment in the representation of the CodeMazeArticle record, C# 10 allows us to include sealed modifier, which prevents the compiler from generating a ToString implementation for any derived records:

public record Article(string Author, string Title)
{
    public sealed override string ToString()
    {
        return $"{Author}: {Title}";
    }
}

And now we can call a ToString method on the CodeMazeArticle:

var codeMazeArticleRepresentation = new CodeMazeArticle("Author", "Title", "Comment").ToString();

As a result, the value of codeMazeArticleRepresentation is "Author: Title" and we have accomplished our goal to consistently represent our articles.

Assignment and Declaration in the Same Deconstruction

The nifty thing is to be able to mix assignments and declarations when deconstructing our tuples:

var articles = (new Article("Author", "Title"), new CodeMazeArticle("Author", "Title", "Comment"));
var article = new Article("Another author", "Another title");

(article, CodeMazeArticle codeMazeArticle) = articles;

After the deconstruction, the article.Author and article.Title properties hold "Author" and "Title" values respectively. Also, we can use initialized codeMazeArticle variable further in our code and compare properties:

var areAuthorsEqual = article.Author == codeMazeArticle.Author;

Before C# 10, we could only assign all variables to existing variables or initialize newly declared variables.

Worthy Mentions of Other C# 10 Improvements

In the end, we are just going to mention without further explanation that in C# 10, we got improved definite assignments. We are going to have fewer warnings for false-positive analysis. Also, we can put the AsyncMethodBuilder attribute on methods and use CallerArgumentExpression attribute diagnostics in our code.

Conclusion

In this article, we’ve seen new C# 10 features that we can use in our projects.