Missed these 12 new C# features?
Published: Monday 26 January 2026
We are going to look at 12 new features from the last three editions of C# that you might have missed.
C# 12
Primary constructors
Previously, if you wanted to pass parameters into a class, you had to create a constructor, pass in the parameters and then store the values in a field.
public class Product {
private readonly int _id;
public Product(int id) {
_id = id;
}
}
With primary constructors, you can add parameters directly to the class declaration and use them throughout the class. As a result, you no longer need an explicit constructor.
public class Product(int id) {
public int GetIdNumber() {
return id;
}
}
To initialise it, you call:
var p = new Product(9);
Console.WriteLine(p.GetIdNumber());
Collection expressions
Collection expressions bring the spread operator (..) to C#. They allow you to combine collection types such as arrays, spans and lists into a single collection. You can also prepend and append values.
var arrayInt = new int[] { 1, 2, 3 };
var spanInt = new Span<int>([2, 4, 5, 4, 4]);
var listInt = new List<int> { 4, 6, 6, 5 };
int[] fullListArray = [1, ..arrayInt, ..spanInt, ..listInt, 9];
foreach (var a in fullListArray) {
Console.WriteLine(a);
}
When you run this code in a console application, it outputs:
1
1
2
3
2
4
5
4
4
4
6
6
5
9
Default lambda parameters
With lambda expressions or statements, you can set default values for parameters, making them optional.
addNumbers with a set to 4 and omit b, it outputs 5.
var addNumbers = (int a, int b = 1) => a + b;
Console.WriteLine(addNumbers.Invoke(4));
Inline arrays
On a struct, you can apply the InlineArray attribute and specify the length of the array. Inside the struct, you need a field to store the value for each array index.
[InlineArray(10)]
public struct Buffer {
private int _a;
}
To use it, create a new instance of the struct, then iterate through each index and set its value.
var buffer = new Buffer();
for (var a = 0; a <= 9; a++) {
buffer[a] = a + 9;
Console.WriteLine(buffer[a]);
}
The console application outputs:
9
10
11
12
13
14
15
16
17
18
C# 13
Now looking at new features in C# 13.
params collections
Before C# 13, when using the params keyword, the type had to be an array.
public class Product {
public List<int> RelatedIds(params int[] a) {
return a.ToList();
}
}
In C# 13, you can use a wider range of collection types, including List.
public class Product {
public List<int> RelatedIds(params List<int> a) {
return a;
}
}
This is how you would use it:
var product = new Product();
product.RelatedIds(9, 3, 4);
allows ref struct
If you use ref structs, you can now use them in generic types and methods.
public class AllowsRefStruct<T> where T : allows ref struct {
}
public ref struct Product {
public int Id { get; set; }
}
When you initialise AllowsRefStruct and use Product as the generic type, it now compiles:
var a = new AllowsRefStruct<Product>();
ref struct interfaces
You can now implement an interface on a ref struct.
public ref struct Product : IProduct {
public int Id { get; set; }
}
public interface IProduct {
}
Partial properties and indexers
Partial classes are commonly used by source generators to add declarations while allowing you to provide the implementation. With C# 13, you can now declare partial properties and indexers.
// Declaration
public partial class Product {
public partial int Id { get; }
public partial int this[int x] { get; }
}
// Implementation
public partial class Product {
public partial int Id { get => 3; }
public partial int this[int x] { get => Id; }
}
C# 14
Now looking at new features in C# 14.
Partial constructors and events
Continuing with partial classes, partial constructors and events were added in C# 14. As with properties and indexers, one partial class contains the declaration and the other contains the implementation.
// Declaration
public partial class PartialConstructorsAndEvents
{
private EventHandler _eventHandlers;
public string Name { get; }
// Partial constructor signature
public partial PartialConstructorsAndEvents(string name);
// Partial event signature
public partial event EventHandler? OnNameChange;
}
// Implementation
public partial class PartialConstructorsAndEvents
{
// Partial constructor implementation
public partial PartialConstructorsAndEvents(string name)
{
Name = name;
}
// Partial event implementation
public partial event EventHandler? OnNameChange
{
add => _eventHandlers += value;
remove => _eventHandlers -= value;
}
}
Extension members
Before extension members, you had extension methods. These required a static class and the this keyword to extend a type.
public class Product {
public decimal Price { get; set; }
}
public static class ProductExtensions {
public static decimal GetPrice(this Product product) {
return product.Price;
}
}
With extension members, you can use an extension block. The static keyword is no longer required, and the extended type is included in the block.
public class Product {
public decimal Price { get; set; }
}
public static class ProductExtensions {
extension(Product product) {
public decimal GetPrice() {
return product.Price;
}
}
}
To use this, initialise the Product class and call GetPrice:
var product = new Product();
var price = product.GetPrice();
field keyword
If logic is required when setting a property, the value previously had to be stored in a private field and returned from the getter.
public class Product {
private decimal _discount;
public decimal Discount {
get => _discount;
set => _discount = value >= 0
? value
: throw new ArgumentOutOfRangeException("Discount must be 0 or greater");
}
}
With the field keyword, the private backing field can be removed. The getter no longer references it, and the setter uses field instead.
public class Product {
public decimal Discount {
get;
set => field = value >= 0
? value
: throw new ArgumentOutOfRangeException("Discount must be 0 or greater");
}
}
Null-conditional assignment
Previously, you needed an if statement to perform a null check.
Product? product = null;
if (product != null) {
product.Discount = 3;
}
With C# 14, you can apply the null-conditional operator to the left-hand side. If the value is null, the assignment is ignored.
Product? product = null;
product?.Discount = 3;
Watch the video
Watch the video to see these C# features in action. It also shows how to use a newer C# version in an older .NET project.
Minimal API for complete beginners course
We use C# 14 and .NET 10 in our Minimal APIs for complete beginners course. The course also covers unit testing, essential features, and setting up OpenAPI/Swagger documentation.
Related pages
Are C# 14's new features worth updating your app to .NET 10?
Discover the new features in C# 14 - from extension members to the field keyword - and find out whether upgrading your app to .NET 10 is really worth it.