Blog

Beyond Extension Methods: Exploring Extension Members in C# 14

C# 14 offers a cool new feature called extension members. This blogs dives into what extension members are and how they differ from extension methods.

1️⃣Intro

C# has long supported extension methods as a way to add functionality to types you don’t own. Since their introduction in C# 3.0 in 2007, they’ve have been used a lot for LINQ and utility libraries. But with C# 14 (will be released this November), Microsoft is taking this idea much further with the introduction of extension members—including the ability to define static members on existing types. In this post, we’ll break down:

  • How extension types differ from extension methods
  • What new capabilities C# 14 introduces to use extension members
  • A demo use cases and syntax examples

2️⃣A Quick Recap: Extension Methods

Extension methods are static methods that appear as if they’re instance methods. They are declared in static classes using the this keyword before a parameter of the method:

public static class StringExtensions
{
    public static int WordCount(this string str)
        => str.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length;
}

This extension method would let you write the following code:

string s = "Hello world!";
int count = s.WordCount(); // Looks like an instance method  

Limitations:

  • Only methods (no properties, indexers, or events). This is because it’s an extension method. An extension method is declared by placing the this keyword before the first parameter in the method definition. This isn’t possible with properties, for example, because properties don’t take parameters.
  • Only instance-level behavior (no static extension methods on the type itself)

3️⃣ Extension members: What’s New in C# 14

C# 14 introduces extension members, a powerful way to logically extend existing types with more than just instance methods. The table below shows what is possible with extension methods and what is possible with extension members.

FeatureExtension MethodsExtension Types (C# 14)
Methods✅ Yes✅ Yes
Properties❌ No✅ Yes
Indexers❌ No✅ Yes
Static Members❌ No✅ Yes

 

4️⃣ Syntax Examples

1. Instance member extension
An extension must just as with extension methods always be declared in a static class. The example below shows how you can define extensions in C# 14. This example contains a new property WordCount.

public static class Extensions
{
    extension(string source)
    {
        public int WordCount => source.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length; // Wordcount is now a get property
    }
}

Now you can call:

string message = "Hello from C# 14";
Console.WriteLine(message.WordCount); // Wordcount is accessed like an instance property of string

2. Static member extension
You can now even add static methods to existing types through extensions (notice that only the type int is used here):

public static class Extensions
{
    extension(int)
    {
        public static int Max(int a, int b) => a > b ? a : b;
    }
}

You can then use it like:

int larger = int.Max(5, 10); // this is used as a static method on the type int and will output ‘10’

This feels exactly like it’s part of the int type. This was not possible before with extension methods. The fact that only the type is used here is called ReceiverTypeOnly. This is handy when you just extend a type with some static methods. Let’s look at another example:

extension(DateTime)
{
    public static DateTime StartOfFiscalYear(int year) => new DateTime(year, 4, 1);
}

This example only defines DateTime inside the extension. Then it adds a static method to the type DateTime to get the start of the fiscal year (that always returns the same date in this case). This can be used in your code just as the examples before:

var today = DateTime.Now;
Console.WriteLine(DateTime.StartOfFiscalYear(2025)); // Static extension

Previously, extension methods only worked on instances of a type. There was no way to add a static method to something like DateTime, Guid, or int directly. You had to create external helpers, which polluted your codebase. A disadvantage was that you need to now what helper to use and IntelliSense on the type did not work. With this your APIs feel more cohesive and natural—especially for framework-style libraries or DSLs (domain-specific languages).
 

5️⃣ Namespace usage

Just as with extension methods extension types don’t automatically apply globally. You still have to bring them into scope with a using directive, just like with extension methods. This avoids accidental conflicts between libraries.

 

6️⃣ Example: Extending DateOnly

The code below shows just another example how you could extend the DateOnly struct with some methods that can be handy.

public static class Extensions
{
    extension(DateOnly source)
    {
        public int FiscalQuarter => (source.Month - 1) / 3 + 1;
        public static DateOnly StartOfFiscalYear(int year) => new DateOnly(year, 4, 1);
    }
}

Use this like:

DateOnly today = DateOnly.FromDateTime(DateTime.Now);
Console.WriteLine(today.FiscalQuarter);              // Instance property
Console.WriteLine(DateOnly.StartOfFiscalYear(2025)); // Static extension

 

7️⃣ Example: Extending ArgumentException

Since .NET 7, there are several ThrowIf*** static methods defined on:

  • ArgumentException
  • ArgumentNullException
  • ArgumentOutOfRangeException

However using this in older frameworks like .NET 4.8 or .NET 6 is not possible.

Since 2022, a simple validation NuGet existed which provided some Guard methods like:

  • Guard.NotNull(...)
  • Guard.NotNullOrEmpty(...)
  • . . .

With the new support for static extension methods, this project can be updated to provide (beside the existing Guard-methods) also support for the new ThrowIf*** static methods on ArgumentException, ArgumentNullException and ArgumentOutOfRangeException.

This can be implemented as:

public static class ArgumentExceptionExtensions
{
    extension(ArgumentException)
    {
        public static void ThrowIfNullOrEmpty(string? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
        {
            _ = Guard.NotNullOrEmpty(value, parameterName);
        }

        public static void ThrowIfNullOrEmpty<T>(IEnumerable<T?>? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
        {
            _ = Guard.NotNullOrEmpty(value!, parameterName);
        }

        public static void ThrowIfNullOrWhiteSpace(string? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
        {
            _ = Guard.NotNullOrWhiteSpace(value, parameterName);
        }

        public static void ThrowIfHasNulls<T>(IEnumerable<T?>? value, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
        {
            _ = Guard.HasNoNulls(value!, parameterName);
        }

        public static void ThrowIfNotCondition<T>(T value, Predicate<T> predicate, [CallerArgumentExpression(nameof(value))] string? parameterName = null)
        {
            _ = Guard.Condition(value, predicate, parameterName);
        }
    }

    extension(ArgumentNullException)
    {
        public static void ThrowIfNull(object? argument, [CallerArgumentExpression(nameof(argument))] string? parameterName = null)
        {
            _ = Guard.NotNull(argument, parameterName);
        }
    }

    extension(ArgumentOutOfRangeException)
    {
        public static void ThrowIfEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? parameterName = null) where T : IEquatable<T>?
        {
            _ = Guard.Condition(value, v => !v!.Equals(other), parameterName);
        }

        public static void ThrowIfGreaterThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? parameterName = null) where T : IComparable<T>
        {
            _ = Guard.Condition(value, v => v.CompareTo(other) <= 0, parameterName);
        }

        public static void ThrowIfGreaterThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? parameterName = null) where T : IComparable<T>
        {
            _ = Guard.Condition(value, v => v.CompareTo(other) < 0, parameterName);
        }

        public static void ThrowIfLessThan<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? parameterName = null) where T : IComparable<T>
        {
            _ = Guard.Condition(value, v => v.CompareTo(other) >= 0, parameterName);
        }

        public static void ThrowIfLessThanOrEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? parameterName = null) where T : IComparable<T>
        {
            _ = Guard.Condition(value, v => v.CompareTo(other) > 0, parameterName);
        }

        public static void ThrowIfNotEqual<T>(T value, T other, [CallerArgumentExpression(nameof(value))] string? parameterName = null) where T : IEquatable<T>?
        {
            _ = Guard.Condition(value, v => v!.Equals(other), parameterName);
        }
    }
}

As you can see in the above code, all ThrowIf*** methods are defined which just use the existing Guard.*** methods. In this way no extra code is needed and these static extension methods act as a “PolyFill” for older frameworks. For more details, see GitHub Project: Stef.Validation.

 

🚫 Limitations: Extension Types Cannot Override Existing Members

One last thing to mention is that here is one important rule developers must know about. Extension types cannot override or replace existing members of a type.

🔍 What Does That Mean?
If the original type (e.g., string, int, or any class) already defines a method, property, or indexer, your extension cannot:

  • Replace it
  • Change its behavior
  • Hide it in any meaningful way

📌 Example: Why This Won’t Work
Suppose you try to extend string with a new indexer to access words by index:

extension StringExtensions for string
{
    public string this[int index] => 
        this.Split(' ')[index];
}

This will compile, but never be used, because string already defines an indexer that returns a char. The compiler always resolves to the built-in member first.

💬 Why Is This the Case?
The extension model in C# is non-invasive by design. It’s meant to safely enhance existing types without breaking or modifying their behavior.
This ensures:

  • Backward compatibility
  • No accidental shadowing of built-in members

📌 Summary

C# 14’s extension members go beyond what extension methods offered. You now get:

  • Full object-oriented extensions (not just utility methods)
  • Static member injection
  • Cleaner and more modular codebases
  • Better discoverability and tooling support (IntelliSense, documentation)

With extension members, developers can build APIs that feel native—even when enhancing types they don’t own. Whether you’re crafting DSLs, writing framework-style libraries, or simply organizing code more cleanly, this feature can make your life as developer easier.

 

 

* Some parts of this blog are generated using AI. Every section where this is used is carefully reviewed by the author.

Written by: Hans Enthoven

Hans is a software developer with over 20 years of experience. As a software developer he has lots of experience developing for the Microsoft platform. In these 20 years Hans has carried out many different projects in different environments. Hans describes himself as a professional software engineer with a lot of experience within Microsoft technology. Next to developing Hans also coaches people on the job and likes to give presentations to share his knowledge with other people.

Mission: Building structured, reusable and maintainable software that that matters in this society.

Want to know more about our experts? Contact us!