Key New Features in C# 13

Share This Post

Since its start in 2000, C# has evolved significantly. Currently, it is a favored language among many software developers. For more than twenty years, numerous functionalities have been implemented to facilitate coding more easily, more understandable, and quicker. Whenever a new version is released, the .NET team pushes C# to enhance it without making it hard to use.

C# 12 had a strong launch featuring enhancements such as readonly record structs, default interface implementations, required members, and more effective application of interpolated strings. But C# 13 is a considerable advance in development. Rather than simply extending the language, this release aims to reinforce modern development practices and facilitate when applying clean architectures, design patterns like DDD (Domain-Driven Design), and performance optimization methods such as using Span.

A primary goal of C# 13 is to make the programming language richer in expression, reliable, and with better performance, while maintaining comprehensibility. In order to achieve this, the language incorporates functionalities like principal constructors within classes, params Span for high-performance collections, better lambda expressions, collection expressions, and a significant extension in how pattern matching is used. These functionalities are not only syntactically pleasing, furthermore facilitate less complex and more manageable solutions in real-world applications.

The purpose of this article is to investigate the new features in C# 13.  

Primary Constructors for Classes

C# 13 provides Primary Constructors to classes (which have been available for structs in terms of earlier releases). This implementation represents a significant advancement in the way we arrange and begin classes. By using this option, you are able to define the constructor directly within the class definition. This reduces code that is repeated and simplifies the process, primarily for unchangeable types or classes that only require a basic setup.

public class Product(string name, decimal price)
{
    public string Name { get; } = name;
    public decimal Price { get; } = price;
}

In this case, name and price constitute primary constructor parameters that you can utilize as variables throughout the class.

What distinctive features do you differ from typical construction businesses?

Before C# 13, you were required to code something like this:

public class Product
{
    public string Name { get; }
    public decimal Price { get; }

    public Product(string name, decimal price)
    {
        Name = name;
        Price = price;
    }
}

Primary constructors decrease boilerplate and contribute to the object creation contract being more understandable.

Applied Case in an Actual Project

Suppose you are developing an e-commerce app. There is an OrderLine entity that represents one line item of an order.

With primary constructors:

public class OrderLine(int quantity, Product product)
{
    public int Quantity { get; } = quantity;
    public Product Product { get; } = product;

    public decimal Subtotal => Quantity * Product.Price;
}

Advantages:

  • Acquire a comprehensive understanding of the data that is needed for an item immediately.
  • Unchangeable attributes are simple to establish.
  • Appropriate for models in DDD and Clean Architecture.

Compatibility with other language elements
required & init:

Despite the fact that primary constructors make code much simpler, they also still work with required and init fields.

Attributes:

It is possible to incorporate attributes into the main constructor’s parameters if you intend to use them for serializing or validating data.

public class Customer([Required] string name)
{
    public string Name { get; } = name;
}

Dependency Injection (DI):

Primary constructors are well-suited for classes instantiated by IoC containers, as long as the parameters align with the registered services.

Params Spans for Better Performance

¿What is params Span<T> and why is it relevant?

C# 13 allows you to employ params Span<T> and params ReadOnlySpan<T>. This is a substantial upgrade in terms of execution speed and how well memory is used. Using this functionality, methods that take an indefinite number of arguments can avoid generating a new array (T[]) in the heap, which was previously necessary with params T[].

Rather than requesting memory each time a parameterized method is invoked, it is now possible to interact directly with the stack or existing data in memory. This considerably alleviates pressure on the garbage collector.

Comparative Syntax

Comparative Syntax Before C# 13 (including heap allocation):

public void LogMessages(params string[] messages)
{
    foreach (var message in messages)
        Console.WriteLine(message);
}

In C# 13 (without needless allocation):

public void LogMessages(params ReadOnlySpan<string> messages)
{
    foreach (var message in messages)
        Console.WriteLine(message);
}

Applied Example: Tracking Several Mistakes

public static class Logger
{
    public static void LogErrors(params ReadOnlySpan<string> errors)
    {
        foreach (var error in errors)
            Console.WriteLine($"[ERROR] {error}");
    }
}

Using it from somewhere else:

Logger.LogErrors(["Error en login", "Falta conexión", "Usuario no autorizado"]);

This example performs more efficiently since it avoids creating a new array every time LogErrors is called.

Lambda Enhancements

What are the updates with Lambda expressions in C# 13?

Lambdas have represented a crucial component of C# over many years, and C# 13 brings significant enhancements that allow them to be with enhanced expressiveness, clear, and composable. These modifications allow developers to create more intuitive and understandable functional code, which is beneficial in LINQ, reactive programming, and declarative logic.

  Migrate from JavaScript to TypeScript

Main news

 1. Type inference in multiple parameters.

Developers are no longer required to specify the type of each parameter in lambdas that accept several arguments if the compiler can deduce it.

// Before
Func<int, int, int> sum = (int x, int y) => x + y;

// Now in C# 13
Func<int, int, int> sum = (x, y) => x + y;

 2. Lambdas with attributes.

It is now possible to apply attributes directly to lambda parameters. This method is useful for validations, serialization, or custom annotations.

var handler = ([MyCustom] string name) => Console.WriteLine(name);

3. Static Lambdas with improvements.

In C# 13, static lambdas can now be inferred under more circumstances. This approach helps reduce errors and accelerates the process more rapidly since context is not required to be captured.

Span<int> numbers = stackalloc[] { 1, 2, 3 };
var doubled = numbers.ToArray().Select(static n => n * 2);

4. Delegate & method group compatibility

Improved lambdas are now simpler to use in situations where only explicit delegates had been accepted previously.

A practical instance: Filtering using complex logic

Consider that there is a set of orders and need to locate those items that satisfy several requirements:

var filtered = orders.Where((order) =>
    order.Status == OrderStatus.Paid &&
    order.Items.Any(item => item.Quantity > 10)
);

With C# 13, you can enhance performance by implementing a static lambda in cases where outside variables are not captured.

var filtered = orders.Where(static order =>
    order.Status == OrderStatus.Paid &&
    order.Items.Any(item => item.Quantity > 10)
);

This instruction informs the compiler to exclude the lambda’s surrounding environment, resulting in quicker execution.

Lambdas and LINQ: More Potent Than Ever

Featuring cleaner lambdas, type inference, and attributes, LINQ becomes considerably more expressive.

var results = data
    .Where((x, i) => i % 2 == 0)
    .Select((x, i) => $"Item #{i}: {x.Name.ToUpper()}");

Developers are also able to create more complex expression trees easily for dynamic queries.

Collection Expressions

What are Collection Expressions in C# 13?

Collection expressions represent a feature which enables initializing collections more compact, expressive, and reliable. Available with C# 13, these features unify and simplify collection creation and combination using syntax similar to arrays, applicable to nearly any compatible collection type.

This is a key improvement in the language, aiming to remove the need to call constructors or methods like .Add() directly, and it offers a cleaner, more declarative syntax.

Basic example of syntax

int[] numbers = [1, 2, 3, 4]; // instead of: new int[] { 1, 2, 3, 4 }

This pattern also applies to:

List<string> names = ["Alice", "Bob", "Charlie"];

The compiler infers the correct type as a result of the [] operator, leading to the code more straightforward and more understandable.

How it works internally

By making use of a Collection Expression like [a, b, c], the compiler translates this to a call to the constructor for the requested type and does an initialization similar to a collection initializer.

In C# 13, it is also possible to utilize these collection constructors in complex compositions like:

var allValues = [..primaryValues, ..fallbackValues, 99];

Here:

  • ..primaryValues unpacks a collection.
  • 99 adds a literal element
  • This approach creates a new collection without requiring to write imperative logic.

Practical Example: Combining Collections

IEnumerable<string> defaultTags = ["core", "base"];
IEnumerable<string> customTags = ["api", "v1"];

List<string> finalTags = [..defaultTags, ..customTags, "latest"];

This action avoids the need to do:

var finalTags = new List<string>();
finalTags.AddRange(defaultTags);
finalTags.AddRange(customTags);
finalTags.Add("latest");

Span<t> and Array<t> Compatibility

Developers are also able to use collection expressions with low-level structures.

Span<byte> header = [0x01, 0xFF, 0x00];

And combine them with data while in operation:


var dynamicData = new byte[] { 0xA1, 0xB2 };
var fullPayload = [0x00, ..dynamicData, 0xFF];

Application with custom types.

If you define a class or struct which implements ICollection<T>, or has a constructor that accepts a params T[], you’re able to use collection expressions with custom types.

Example with a record:

public record MyContainer(params string[] Values);
var container = new MyContainer(["one", "two", "three"]);

Interceptors (Experimental)

What are Interceptors in C# 13?

Interceptors are an experimental feature in C# 13 that let you intercept and replace methods made by the compiler. This includes methods in constructor calls, auto-initialization methods, records, and deconstruction patterns.

The main reason for this is to give developers a way to add their own logic instead of the compiler’s default behavior, without writing the same code over and over or using IL weaving techniques (like Fody).

Why does it matter?

Usually, developers couldn’t control what the compiler did when it automatically created methods. Interceptors open the door to:

*   Tailor record constructors.

*   Change equality and hashCode methods.

*   Add validations or logs without changing business logic.

*   Tune auto-generated code.

It’s like a native compiler AOP (Aspect-Oriented Programming).

Basic Syntax

First, mark the interceptor with a special attribute:

[Experimental("Interceptors")]
public static class MyInterceptors
{
    [Interceptor]
    public static Person InterceptConstructor(string name, int age)
    {
        Console.WriteLine($"Intercepted constructor: {name}, {age}");
        return new Person(name.ToUpper(), age); // custom logic
    }
}

Then intercept it like this:

var person = new Person("alice", 30); // Call the interceptor if it’s registered.

Practical Example: Validation in Constructor

  Functional Programming in JavaScript

Imagine wanting to check data when creating a User object without repeating logic or making manual factories.

[Experimental("Interceptors")]
public static class UserInterceptors
{
    [Interceptor]
    public static User InterceptUser(string username, string email)
    {
        if (!email.Contains("@"))
            throw new ArgumentException("Invalid email");

        return new User(username, email.ToLower());
    }
}

Now, every time you call a new User(…), the interceptor activates.

Where can interceptors be used?

Interceptors are for:

• Constructors

• Auto-implemented methods

• Deconstruction methods

• Record methods like Deconstruct or With

This means you can have total control over objects created at compile time, without manually generating boilerplate.

Primary Constructors for Classes

What are Primary Constructors?

Primary constructors are a C# 13 feature that expands on an idea from C# 9 records: defining constructor parameters directly in the class declaration.

This simplifies classes by cutting down on the need for extra fields, properties, and constructors. What used to take many lines can now be defined in a single declarative line.

Basic Example

Before C# 13, creating a class with a constructor and properties looked something like this:

public class Person
{
    public string Name { get; }
    public int Age { get; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

With C# 13:

public class Person(string name, int age)
{
    public string Name => name;
    public int Age => age;
}

Much cleaner!

Key differences compared to records

AspectClass in Primary ConstructorRecord Constructor’s 
Typeclass, no recordOnly on record
ImmutabilityOptionalImplicit
DeconstructingAutomatic NoYes
Value semanticsNoYes (by default in records)

Practical Example: Configuration Class

public class DbConfig(string connectionString, int timeout)
{
    public string ConnectionString => connectionString;
    public int Timeout => timeout;
}

You can use this class like so:

var config = new DbConfig("Server=myDb;", 30);
Console.WriteLine(config.ConnectionString); // Output: Server=myDb;

How to Get to Parameters

Parameters set in the main constructor behave like variables within the class. You can:

  • Show them as properties.
  • Use them in methods.
  • Pass them to base classes or interfaces.

Basic class example:

public abstract class Entity(Guid id)
{
    public Guid Id => id;
}

public class Customer(Guid id, string name) : Entity(id)
{
    public string Name => name;
}

Uses within the class body

public class LogEntry(DateTime timestamp, string message)
{
    public string FullLog => $"[{timestamp:u}] {message}";

    public void Print()
    {
        Console.WriteLine(FullLog);
    }
}

Default Lambda Parameters

What are Default Lambda Parameters?

In C# 13, you can now assign default values to the parameters of a lambda expression. This makes lambdas act like normal methods, where this has been possible for a while.

This change makes lambdas more versatile and reusable, and it means you don’t have to write multiple versions of the same anonymous function.

Basic Syntax

Before C# 13:

Func<int, int> square = x => x * x;

But you couldn’t before:

Func<int, int> square = (int x = 5) => x * x; // Error in older versions

Now, in C# 13:

Func<int, int> square = (int x = 5) => x * x;  //  Valid

You can call it with no arguments if you use a dynamic invocation or an overload:

var result = square(); // result = 25

Note: This works when you invoke the lambda dynamically, or when you define a function that encapsulates it.

Practical Example: Message Generator

Func<string, string> greet = (string name = "Guest") => $"Hello, {name}!";
Console.WriteLine(greet());           // Output: Hello, Guest!
Console.WriteLine(greet("Alice"));    // Output: Hello, Alice!

Lambdas with Default Values

You can use lambdas with parameters that have default values inside other functions.

Action<string, int> log = (message = "No message", level = 1) =>
{
    Console.WriteLine($"[Level {level}] {message}");
};

log();                         // Output: [Level 1] No message
log("Fatal error", 5);         // Output: [Level 5] Fatal error
log("Minor warning");          // Output: [Level 1] Minor warning

How the Compiler Handles It:

The compiler treats parameters with default values the same way it would in a traditional method. If an argument isn’t given, it uses the value that was set as the default.

Keep in Mind: You can only use this when you call the lambda directly, or when you put its call inside a function that keeps the default values.

Using Aliases in Patterns 

What are Aliases in Patterns?

In C# 13, you can now assign a name to an expression inside a pattern of type is or switch. This lets you refer to the pattern’s result without having to re-evaluate it or write extra logic.

This makes complex patterns clearer, faster, and easier to read.

Basic Example

Before C# 13:

if (obj is string s && s.Length > 5)
{
    Console.WriteLine(s.ToUpper());
}

With pattern aliases (C# 13):

if (obj is string s and { Length: > 5 } as longString)
{
    Console.WriteLine(longString.ToUpper());
}

Here, longString works as a named alias that stands for the value checked by the pattern.

What issue does it fix?

In tricky patterns with nested properties or many checks, it was common to:

  • Repeat accessing the same property.
  • Use extra variables.
  • Re-access an object after a pattern.

You’re now able to grab a reference with ‘as’ directly within switch or if statements.

Practical Example: Shape Analysis

object shape = new Rectangle(Width: 100, Height: 200);

if (shape is Rectangle { Width: > 50, Height: > 100 } as largeRectangle)
{
    Console.WriteLine($"Large shape: {largeRectangle.Width}x{largeRectangle.Height}");
}

This proves quite useful when you need to re-access the validated object, saving you from writing various conditions or castings.

  Implementing Google OAuth to use Google API in Cloudflare Workers

Switch Example:

string Process(object input) => input switch
{
    string { Length: > 10 } as longString => $"Long string: {longString}",
    int n => $"Integer: {n}",
    _ => "Unknown"
};

Improvements in params and Collection Expressions

What improvements does C# 13 bring to params and collections?

Features in C# 13 bring key improvements to using params with collection expressions. Now, you can pass collections in more flexible and expressive ways.

This makes the language more functional and expressive when building complex lists, arrays, and collections with less syntax and more clarity.

Reminder: params in C#

public void PrintMessages(params string[] messages)
{
    foreach (var msg in messages)
    {
        Console.WriteLine(msg);
    }
}

Call

PrintMessages("Hello", "World");

What’s new in C# 13?

You can now pass collection expressions directly as an argument for a params parameter.

Collection expressions ([…]) as params arguments

PrintMessages(["Hello", "world", "from C# 13"]);
This: PrintMessages(["A", "B", "C"]);
Is equal to: PrintMessages(new string[] { "A", "B", "C" });

It’s much cleaner, especially for literals or dynamic results.

You can use params with other values and mix individual values with collections.

PrintMessages("First", ..["Second", "Third"], "Last");

This uses the spread operator (..), like in JavaScript or Python, to expand the collection within the params.

This behavior is part of the new Collection Expressions system introduced in C# 12 and expanded in C# 13.

Practical example with parameters and collections.

void LogErrors(params string[] errors)
{
    Console.WriteLine("Errors:");
    foreach (var e in errors) Console.WriteLine($"- {e}");
}

You can call it this:

LogErrors(["Invalid token", "Timeout", "Unauthorized"]);

Collection Expressions in Other Contexts

Besides params, you can use [ … ] to:

Create lists with literals: var nums = [1, 2, 3, 4];

Combine collections: var combined = [..firstList, 42, ..secondList];

Functions that return arrays: int[] GetDefaults() => [0, 1, 2];

Requirements and Support

FeatureRequirement
[ … ] syntaxC# 12 or higher
Spread .. in paramsC# 13
Collection expressionsSystem.Span<T> and supported arrays 

Interpolated String Improvements

What Are Interpolated Strings?

Interpolated strings ($…) let you embed variables and expressions directly into text strings, which is important for logs, messages, SQL queries, and more.

C# 13 makes this feature even better by making interpolations more efficient, secure, and customizable, especially when FormattableString, InterpolatedStringHandler, and custom APIs are used.

New Features in C# 13

Smarter Interpolated String Handlers

This makes performance better in logging, conditional formatting, and memory use.

C# 13 keeps making the use of InterpolatedStringHandler better, allowing you to catch interpolations without building the entire string.

logger.LogDebug($"Request took {elapsedMilliseconds} ms"); // Efficient string creation

During compilation, this instruction can be tweaked to skip string creation if debug logging isn’t on.

Why does it matter?

In prior versions, interpolations made a final string whether it was used or not. With handlers, the string portion is built only when needed, which:

  • Lowers memory allocation.
  • Improves logging output.
  • Permits advanced customization (such as translation or censorship).

Example with InterpolatedStringHandler

public void LogInfo([InterpolatedStringHandlerArgument("")] ref CustomLoggerHandler handler)
{
    Console.WriteLine(handler.GetFormattedText());
}

This pattern is used in logging systems like Serilog and Microsoft.Extensions.Logging.

Advanced customization.

You can create your own InterpolatedStringHandler to oversee and manage how strings are built.

[InterpolatedStringHandler]
public ref struct MyHandler
{
    private StringBuilder _sb;
    public MyHandler(int literalLength, int formattedCount)
    {
        _sb = new();
    }

    public void AppendLiteral(string s) => _sb.Append(s);
    public void AppendFormatted<T>(T value) => _sb.Append($"**{value}**");
    public string GetResult() => _sb.ToString();
}

Use:

var handler = new MyHandler(0, 0);
handler.AppendFormatted("Hello");
Console.WriteLine(handler.GetResult()); // Output: **Hello**

Interpolated Strings with Span<char> & stackalloc

These improvements let you mix interpolated strings with Span<char>, which cuts down on extra heap use and keeps unnecessary object allocations from happening.

Span<char> buffer = stackalloc char[100];
var writer = new SpanWriter(buffer);
writer.Write($"Data: {value}, Time: {timestamp}");

Conclusions: Features in C# 13

C# 13 isn’t just about adding more features to the language. It’s about continuing to improve C# into a more expressive, safer, more efficient, and easier-to-use development setting for teams creating modern apps.

Throughout this article, we looked at 9 features in C# 13 and what they mean for you as a software developer or designer.

  • C# 13 improves productivity by letting you do more with less code, which is helpful for clarity and maintenance, mainly in sizable settings.
  • These improvements offer clear gains in projects with Domain-Driven Design (DDD), Clean Architecture, or Hexagonal Architecture.
  • Many of the upgrades aim to refine runtime execution and the developer experience.
  • It is in line with how the .NET environment is changing.

A programming language isn’t just a tool; it’s a way of thinking.”

These features in C# 13 introduce some fresh perspectives on coding. It doesn’t disrupt what you already understand, and there’s no need to rewrite everything. You can benefit from these changes without microservices, big frameworks, or project makeovers.

These improvements are here to assist you in writing cleaner, easier-to-maintain, and efficient code, whether you’re building simple REST APIs, complex business applications, or distributed systems.

Author

  • IleanaDiaz

    I am a Computer Engineer by training, with more than 20 years of experience working in the IT sector, specifically in the entire life cycle of a software, acquired in national and multinational companies, from different sectors.

    View all posts

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Subscribe To Our Newsletter

Get updates from our latest tech findings

About Apiumhub

Apiumhub brings together a community of software developers & architects to help you transform your idea into a powerful and scalable product. Our Tech Hub specialises in Software ArchitectureWeb Development & Mobile App Development. Here we share with you industry tips & best practices, based on our experience.

Estimate Your Project

Request
Popular posts​
Get our Book: Software Architecture Metrics

Have a challenging project?

We Can Work On It Together

apiumhub software development projects barcelona
Secured By miniOrange