C# 14: The Extension Strikes Back

Introduction

C# 14 introduces extension members ("extension everything"), not just methods, but also properties, static members, even operators, with nicer grouping and syntax.

Here are some of the most interesting / awesome examples & use-cases, what they enable, plus some quirks.

Key Features of Extension Members

Here are what C# 14 lets you do with extension members that you couldn’t (or not cleanly) do before:

  • Instance extension properties (in addition to methods) e.g. myList.IsEmpty instead of myList.IsEmpty().
  • Static extension members that "live" on the type rather than instances. For example static methods/properties callable like Type.Foo(...).
  • Extension operators (e.g. operator +) in static extension context.
  • Ref returns of ref structs or its members.
  • Grouping of related extension members under a shared receiver via the extension block, so less repetition of "this ReceiverType" in each method.

Here are some simple examples of the new syntax (you can add as many extensions in one block of extension(..) as you want):

public static class ExampleExtensions
{
   extension<T>(scoped in Span<T> span)
   {
      /// Example of get only properties as extension
      public ref T LastItem => ref span[^1];
      public ref T FirstItem => ref span[0];
   }

   // example of extending any unmanged type
   extension<T>(T value)
      where T : unmanaged
   {
      // new method as extension
      public int WriteBigEndian(scoped Span<byte> buffer)
      {
         var size = Unsafe.SizeOf<T>();
         buffer = buffer[..size];
         
         MemoryMarshal.Write(buffer, in value);
         if (BitConverter.IsLittleEndian) buffer.Reverse();
         
         return size;
      }
   }
   
   extension<T>(T value)
   {
      // example if you wanted any type to have a deserialize method staticly
      public static T DeserializeMemory(scoped in ReadOnlyMemory<byte> memory)
      {
         return SerializerCache<T>.DeserializeMemory(memory.Span);       
      }
   }
}

More Complex Example with ref struct Builders

When working on a low-allocation code generation library (designed for source generators in .NET 10 and beyond), one of the biggest challenges I ran into was enabling a fluent builder pattern with ref structs

Normally, builder patterns are straightforward: each method modifies the builder's state and then returns this so that calls can be chained fluently. With ref structs, however, this is not trivial. Since ref structs cannot implement inheritance hierarchies, and since they must be passed by reference to avoid unnecessary copying, creating a shared, composable API across multiple builders becomes tricky.

For example, in a code generation scenario you might have different specialized builders:

  • FileBuilder → for writing an entire source file.
  • TypeHeaderBuilder → for building class, struct, or interface headers.
  • MethodBuilder, NamespaceBuilder, etc.

Each of these builders should expose the same set of common methods such as WriteLine, Write, Indent, or Unindent. I n a class hierarchy, you would solve this by placing shared logic in a base class. With ref structs, however, inheritance is off the table.

This is exactly where the new C# 14 extension blocks shine. Instead of duplicating boilerplate code in every builder, we can write the common methods once and extend all builder structs in a type-safe and allocation-free manner.

Defining a Shared Interface

The first step is to define an interface that all builder structs implement. This allows us to define a common contract while still keeping the performance and safety guarantees of ref struct.

public interface ICodeBuilder
{
   [MethodImpl(MethodImplOptions.AggressiveInlining)]
   internal ref CodeBuilder GetBuilder();
}

Each builder (FileBuilder, TypeHeaderBuilder, …) implements ICodeBuilder by returning a reference to its underlying CodeBuilder, which owns the actual writer state.

Extension Blocks for Fluent APIs

Now comes the interesting part. Instead of writing WriteLine and Write in every single builder, we use an extension block with the new extension<T>(ref T) syntax:

public static class CodeBuilderExtensions
{
   extension<T>(ref T builder)
      where T : struct, ICodeBuilder, allows ref struct
   {
      // ... more methods 
      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public ref T WriteLine(string line)
      {
         ref var writer = ref builder.GetBuilder().Writer;
         writer.WriteLine(line);
         
         return ref builder;
      }
      
      [MethodImpl(MethodImplOptions.AggressiveInlining)]
      public ref T WriteLine()
      {
         ref var writer = ref builder.GetBuilder().Writer;
         writer.WriteLine();
         
         return ref builder;
      }
      // ... more methods like UpIndent, DownIndent etc.
   }
}

Fluent Usage

The result is a clean and ergonomic API that looks like a normal builder pattern but internally is optimized for high performance and low GC pressure:

builder.File
   .WriteStartAutoGenerated()
      .WriteLine("// Here")
      .WriteLine("// is a message")
      .WriteLineInterpolated($"// Test {x}")
   .WriteEndAutoGenerated()
   .WriteNullableEnable()
      .WriteUsing("NameSpaceA")
      .WriteUsing("NameSpaceB.Test", true);

With this design, adding new builders or extending existing ones requires zero redundant boilerplate, while still leveraging the unique power of ref struct for performance-critical scenarios like code generation.

👉 If you are interested in the full source generation builder, you can check it out here: CodeGen.Core

...
C#
.NET
Extensions
C#
.NET

Marvin Drude

Senior Software Developer

Currently at
Technologies I love and currently use
C#
.NET
Blazor
PostgreSQL
HTML5
CSS
JavaScript
This websites also generates my CV, open it with the link below
Open CV
This page is built with Blazor and is public source

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.