C# 11: Preview of features for .NET 7

Published: Thursday 11 August 2022

C# 11 release date is just around the corner, and we will show what's new and preview some of the features that can be used for .NET 7.

C# 11 is due to be released in November 2022 alongside .NET 7. We will have a look at five new features, which include required members, auto-default structs, raw string literals, generic attributes and list patterns.

As these features are still in preview, it's possible that some of them may not make the final C# 11 release. However, it gives us a good idea of how C# 11 will look in a nutshell.

Download .NET 7 to use C# 11

To try out the new features, we first need to download Visual Studio 2022 (version 17.3.0 or above). In-addition, we also need to download the .NET 7 SDK (version 7.0.0-preview 6 or above), so we can create a .NET 7 project.

One other note is that we have to enable previews of the .NET SDK.

We can do that in Visual Studio 2022 by going to Tools and Options. Then, going into the Environment, selecting Preview Features, and finding and ticking the option that says "Use previews of the .NET SDK (requires restart)".

Enable previews of the .NET SDK in Visual Studio 2022

Enable previews of the .NET SDK in Visual Studio 2022

Feature #1: Required members

The required modifier can be used in a property member to ensure that we explictly set a value when an object is initalised.

We add it after the access modifier is set.

public class RequiredMember
{
	public required string Name { get; set; }
}

When we initalise the object, we have to ensure that we set a value for that property. Otherwise, a compile error will be thrown:

var requiredMember = new RequiredMember { Name = "Dave" };

It's also possible to set a required member inside the object's constructor. However, we have to add an additional step.

If we pass in the required property as a parameter, it would throw a compile error:

public class RequiredMember
{
	public required string Name { get; set; }

	public RequiredMember(string name)
	{
		Name = name;
	}    
}
 
var requiredMember = new RequiredMember("Dave"); // This throws a compile error

In order to fix that, we need to set the [SetsRequiredMembers] attribute above the constructor. This tells the compiler that we are setting the required members inside the constructor.

public class RequiredMember
{
	...

	[SetsRequiredMembers]
	public RequiredMember(string name) {
		...
	}
	...
}

Feature #2: Auto-default structs

In C# 10, we had to explictly set the default values for each of it's members if we include a constructor in a struct.

public struct AutoDefaultStruct
{
	public int Number { get; set; }

	public AutoDefaultStruct()
	{
		Number = 0;
	}
}

If we didn't set the Number property in the constructor, it would throw a compile error.

However, this is no longer the case with C# 11. If these members are not set in the constructor, they will be set with their default value. In this instance, the Number property will be set to 0 which is the default value of an integer.

public struct AutoDefaultStruct
{
	public int Number { get; set; }

	public AutoDefaultStruct()
	{
	}
}

Feature #3: Raw string literals

Using strings that contain quotes, or referencing code snippets like JSON have become a lot easier with C# 11.

In C# 10, compiled errors would be thrown if we copied text that contained quotes into a string. To stop this from happening, we would normally have to escape the quotes with a backslash.

Raw string literals start and end with three quotes """...""". As a result, a single quote will be seen as being part of the string.

They can also be interpolated with the $ sign. The number of $ signs that are prepended to a string represents the number of curly braces required to reference a variable.

Any few curly braces will be seen as part of the string.

public class RawStringLiteral
{
	public static int MyNumber = 1;

	public string MyJsonString =
		$$"""
			{
				"number": "{{MyNumber}}"
			}
		""";            
}

In the above instance, we have specified two $ signs at the beginning. As a result, we need to include two curly braces to specify the variable that we wish to reference.

Feature #4: Generic attributes

With attributes in C# 10, specifying the type would involve creating a member with a Type reference, and then setting that Type reference in the constructor.

We would have to specify the type when referencing the attribute.

public class Attr : Attribute { 
 
	public Type AttributeType { get; }
	 
	public Attr(Type attributeType) {
		attributeType = AttributeType;
	}
}

public class GenericAttribute
{
	[Attr(typeof(string))] 
	public void MyClass()
	{

	}
}

With C# 11, we can make these attributes generic. As a result, we don't need a separate member to reference the type. We can set the generic type to the attribute when we reference it.

public class Attr<T1> : Attribute { }
 
public class GenericAttribute<T1>
{
	[Attr<string>] 
	public void MyClass()
	{

	}
}

A note of causion that the generic types must be fully constructed. In the above example, the GenericAttribute class has a T1 generic type. If we were to include this in the attribute, a compile error would be thrown.

public class Attr<T1> : Attribute { }
 
public class GenericAttribute<T1>
{
	[Attr<T1>] // This throws a compile error
	public void MyClass()
	{

	}
}

Feature #5: List patterns

List patterns allow pattern matching for elements in an array, or in a list. We have a number of options here.

By explictly defining which values appear in the order of the array, the array will have to match the same length and have the same sequence to return true.

public class ListPatterns
{
	public bool Is_1_3_5(int[] myNumbers)
	{
		return myNumbers is [1, 3, 5]; // myNumbers must be a length of 3, and contain the values of 1, 3 and 5 to return true.
	}
}
 
var listPattern = new ListPatterns();
Console.WriteLine($"Is1_3_5 = {listPattern.Is_1_3_5(new int[] { 1, 3, 5 })}"); // returns true

If we were to specify an _, it matches a single element that can be any value.

public class ListPatterns
{
	public bool Is_1___5(int[] myNumbers)
	{
		return myNumbers is [1, _, 5]; // myNumbers must start with a value of 1, and end with a value of 5. It must also be a length of 3, but the middle index can be any value.
	}
}
 
var listPattern = new ListPatterns();
Console.WriteLine($"Is1___5 = {listPattern.Is_1___5(new int[] { 1, 9, 5 })}"); // returns true

By specifying the .., it will match zero or more elements that can be any length.

public class ListPatterns
{
	public bool Is_1_DotDot_5(int[] myNumbers)
	{
		return myNumbers is [1, .., 5]; // myNumbers must start with a value of 1, and end with a value of 5. There can be any values with any length between the start and end index to return true.
	}
}
 
var listPattern = new ListPatterns();
Console.WriteLine($"Is_1_DotDot_5 = {listPattern.Is_1_DotDot_5(new int[] { 1, 9, 3, 8, 5 })}"); // Returns true

We also can specify whether a value is less than, equal, or greater than a particular value.

public class ListPatterns
{
	public bool Is_1_Over_5(int[] myNumbers)
	{
		return myNumbers is [1, .., >=5]; // myNumbers must start with a value of 1, and the end value must be 5 or more. There can be any values with any length between the start and end index to return true.
	}
}
 
var listPattern = new ListPatterns();
Console.WriteLine($"Is_1_Over_5 = {listPattern.Is_1_Over_5(new int[] { 1, 9, 3, 8, 6 })}");

This gives us a lot of options for checking for values in a list or an array.

Watch our demo

Watch our video where we explore the C# 11 features and how to implement them into a .NET 7 project.

In-addition, download our C# 11 features code example and use these features right now.