C# Features Demo
Setup
We are gonna create a few projets to demonstrate the new features in C# 11, 12, and 13.
-
Create a new console app called CSharp11
mkdir CSharp
cd CSharp
mkdir CSharp11
cd CSharp11
dotnet new console -
Create a new console app called CSharp12
mkdir CSharp12
cd CSharp12
dotnet new console -
Create a new console app called CSharp13
mkdir CSharp13
cd CSharp13
dotnet new console -
Create a new solution
dotnet new sln
dotnet sln add ./CSharp11
dotnet sln add ./CSharp12
dotnet sln add ./CSharp13 -
Open the solution in your IDE of choice.
C# 11
Records
Create a person record
The default is a class record but can also be a struct record.
Record features:
- compiler generates a special kind of class
- fields will by default be public properties
- value based semantics - if I have 2 records with the same values and I compare them, c# will say they are the same. If I had 2 classes, they won't
- immutability - once created can't change values
Records make it harder to make mistakes - so your code is more robust and maintainable.
record Person(string FirstName, string LastName);
create a new person
var p1 = new Person("Gert", "Marx");
Records are immutable and the following will not work
p1.FirstName = "Fred";
However, we can update a copy of the person using 'with'
var p2 = p1 with { FirstName = "Fred" };
We also get value based equality
var doppleGanger = new Person("Gert", "Marx");
if (p1 == doppleGanger)
{
Console.WriteLine("Equal!");
}
Real world example
var originalTx = new BankTransaction(
TransactionId: "TX123",
Amount: 250.00m,
FromAccount: "123-456",
ToAccount: "789-012",
Timestamp: DateTime.UtcNow
);
// Create a reversed transaction (e.g. refund)
var refundTx = originalTx with {
TransactionId = "TX124",
FromAccount = originalTx.ToAccount,
ToAccount = originalTx.FromAccount,
Amount = -originalTx.Amount,
Timestamp = DateTime.UtcNow
};
Can't I do the same thing with a class?
Yes, but look at all the boilerplate code we have to write to get the same functionality:
Records are designed to reduce boilerplate code and make your code cleaner and more maintainable.
Record
public record Person(string FirstName, string LastName);
Class
public class Person
{
public string FirstName { get; }
public string LastName { get; }
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public override bool Equals(object? obj)
{
if (obj is not Person other) return false;
return FirstName == other.FirstName
&& LastName == other.LastName;
}
public override int GetHashCode() =>
HashCode.Combine(FirstName, LastName);
public override string ToString() =>
$"Person: {FirstName} {LastName}";
}
Required and Init Properties - code more robust
Create the following class
public class Foo
{
public string Bar { get; set; }
}
Notice how we get a warning around 'Bar' being uninitialized. This is because we have nullable reference types turned on by default. This means the compiler will try to warn us against any properties that could possibly be null.
We can get around this by using the required keyword.
public required string Bar { get; set; }
Now this removes the warning, but it also means we can set this property anywhere in code (which we often don't want).
var foo = new Foo { Bar = "Bar " };
foo.Bar = "Bar 2"; // 🤢
To get around this we can use the init keyword.
public class Foo
{
public required string Bar { get; init; }
}
Now we have a nicely constructed object that is guaranteed to have a value for Bar and we can't change it after construction.
For now, we'll move the class to another file so it doesn't break our demo.
Raw String Literals - 3 or more qoutes
Raw string literals were created to make it easier to have nicely formatted blocks of code from other formats.
var xml = """
<html>
<body>
<p>Hello, World!</p>
</body>
</html>
""";
We can also use interpolation with these strings.
We can use any number of quotes to start and end the string, as long as the start and ends match.
var name = "bar";
var sql =
$$"""""
SELECT *
FROM Foo
WHERE Bar = '{{name}}'
""""";
When printed out, all the white space gets trimmed.
Patterns
Next we're going to move onto patterns. There are many different types of patterns in C#. Some you've most likely used before, but may not realise they're patterns.
Null Pattern
First is the null pattern.
if (foo is not null)
{
Console.WriteLine(foo.Bar);
}
Type pattern
if (foo is Foo)
{
Console.WriteLine("Foo");
}
Property pattern
if (foo is { Bar: "Bar " })
{
Console.WriteLine("Bar");
}
Switch expression / Discard Pattern
var result1 = foo switch
{
{ Bar: "Foo " } => "Foo",
{ Bar: "Bar" } => "Bar",
_ => "Default" // Discard Pattern
};
Relational Pattern
int age = 30;
var result2 = age switch
{
< 18 => "Child",
>= 18 and <= 65 => "Adult",
> 65 => "Senior",
};
Positional Pattern
Positional patterns allow you to destructure and match patterns based on the properties or elements of an object or tuple
var result3 = foo switch
{
(string bar, _) => bar,
_ => "Default"
};
These patterns can also be combined. For example, you may use the positional pattern to destructure an object, then the relational pattern to perform tests on the properties.
C# 12
Primary Constructors
The major new C# 12 feature added is Primary Constructors. Let's take a look at what a class looks like with and without this feature.
Create a hero class as follows:
public class Hero
{
private string _name;
private int _age;
private string _superPower;
public Hero(string name, int age, string superPower)
{
_name = name;
_age = age;
_superPower = superPower;
}
public override string ToString()
{
return $"Hero: {_name}, Age: {_age}, SuperPower: {_superPower}";
}
}
Now, if we refactor this to use Primary Constructors, it looks like this:
public class Hero2(string name, int age, string superPower)
{
public override string ToString()
{
return $"Hero: {name}, Age: {age}, SuperPower: {superPower}";
}
}
This can be especially useful to remove code we'd usually have to deal with dependency injection.
NOTE: Primary Constructors may look similar to records, but they operate quite differently. With records the params are turned into properties. With Primary Constructors, they are parameters.
You can use the parameters directly in methods like ToString(), but you can’t access them from outside or through this unless you expose them.
Show how we can't use 'this' to reference the parameters.
Collection Expressions
Over the years arrays and lists have been getting easier to initialize in C# by becoming simpler to write.
Collection expressions are the next (and probably the last) evolution of this.
We can initialize a list of simple numbers as follows.
List<int> numbers = [1, 2, 3, 4, 5];
Show how we can't use var - It needs to know the type of the list at compile time.
Now let's create 3 functions that take in different types of lists
void Foo(IEnumerable<int> numbers)
{
}
void Foo2(List<int> numbers)
{
}
void Foo3(int[] numbers)
{
}
we can use list expressions for all of these functions as follows:
Foo([1,2,3]);
Foo([]);
Foo2([1,2,3]);
Foo3([1,2,3]);
Spread Operator
The spread operator is a new operator that allows us to join two lists together.
List<int> lowNumbers = [1, 2, 3];
List<int> highNumbers = [4, 5, 6];
List<int> allNumbers = [..lowNumbers, ..highNumbers];
Lambda defaults
The last feature we'll look at is lambda defaults. This is not a huge feature, but may be handy from time to time.
var lambda = (int start = 0, int end = 10) => Console.WriteLine("Start: {0}, End: {1}", start, end);
lambda();
lambda(2, 5);
C#13
Param Collections
C# 13 introduces a new way to define methods that can take a variable number of parameters using the params keyword. This allows you to pass an array of arguments to a method without explicitly creating an array.
public void PrintNumbers(params int[] numbers)
{
Console.WriteLine("Numbers Count: " + numbers.Length);
foreach (var number in numbers)
{
Console.WriteLine(number);
}
}
Field (Preview)
C# 13 introduces a new feature called "Field" that allows you to define fields in a class or struct. This feature is currently in preview and may change in future versions of C#.
Lets you reference the backing field without having to define it explicitly.
<LangVersion>preview</LangVersion>
public class Person
{
public required string Name { get; set; }
public required int Age
{
get;
set => field = value < 0 ? 0 : value;
}
public required string Address { get; set; }
public required string PhoneNumber { get; set; }
}
Partial Properties
- Probably only useful fror source generated code
- Separates Definition from Implementation - source gen might define a property, and then you can implement behavior in another file.
partial class Person
{
public partial string Name { get; set; } // definition
}
partial class Person
{
public partial string Name
{
get;
set => field = value; // implementation
}
}
New Lock Type
C# 13 introduces a new lock type that allows you to lock on a specific object instead of the entire class. This feature is useful for improving performance and reducing contention in multi-threaded applications
var counterWithoutLock = 0;
Console.WriteLine("Running demo...");
// No lock (should be incorrect)
var noLockTasks = new Task[10];
for (int i = 0; i < noLockTasks.Length; i++)
{
noLockTasks[i] = Task.Run(() =>
{
for (int j = 0; j < 1000; j++)
{
counterWithoutLock++; // Race condition!
}
});
}
await Task.WhenAll(noLockTasks);
// With lock (should be correct)
var counterWithLock = 0;
var myLock = new Lock();
var lockTasks = new Task[10];
for (int i = 0; i < lockTasks.Length; i++)
{
lockTasks[i] = Task.Run(() =>
{
for (int j = 0; j < 1000; j++)
{
lock (myLock)
{
counterWithLock++;
}
}
});
}
await Task.WhenAll(lockTasks);
Console.WriteLine($"Without lock: {counterWithoutLock} ❌ (should be 10,000)");
Console.WriteLine($"With lock : {counterWithLock} ✅ (correct)");
Init Array with Index Operator (^)
C# 13 introduces a new way to initialize arrays using the index operator ^. This allows you to create arrays with a specific size and initialize them with default values.
var timer = new CountdownTimer()
{
Buffer =
{
[^1] = 0,
[^3] = 2,
[^2] = 1,
[^4] = 3,
[^5] = 4,
}
};
Console.WriteLine(string.Join(", ", timer.Buffer));
class CountdownTimer
{
public int[] Buffer { get; set; } = new int[5];
}