DEV Community

Cover image for Extension Blocks in C# 14 Are More Than New Syntax
Sukhpinder Singh
Sukhpinder Singh

Posted on • Originally published at Medium

Extension Blocks in C# 14 Are More Than New Syntax

Extension methods have been part of C# for years. Extension blocks finally make them feel like a small, organized API.

Extension methods are one of those C# features that quietly end up everywhere. LINQ depends on them, library authors use them to improve APIs they do not control, and most production codebases eventually collect a few SomethingExtensions classes.

The familiar syntax works, but it starts to feel awkward when several related operations target the same type:

public static class DateOnlyExtensions
{
    public static bool IsWeekend(this DateOnly date) =>
        date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;

    public static DateOnly StartOfMonth(this DateOnly date) =>
        new(date.Year, date.Month, 1);

    public static DateOnly NextBusinessDay(this DateOnly date)
    {
        var candidate = date.AddDays(1);

        while (candidate.IsWeekend())
        {
            candidate = candidate.AddDays(1);
        }

        return candidate;
    }
}
Enter fullscreen mode Exit fullscreen mode

There is nothing technically wrong with this code. The repetition is small, and the calling side is already clean:

var releaseDate = new DateOnly(2026, 7, 24);

Console.WriteLine(releaseDate.IsWeekend());
Console.WriteLine(releaseDate.StartOfMonth());
Console.WriteLine(releaseDate.NextBusinessDay());
Enter fullscreen mode Exit fullscreen mode

Still, the implementation reads like a static utility class rather than a coherent API for DateOnly.

C# 14 gives us another option.

The extension block syntax

C# 14 introduces extension blocks, which group members around a receiver inside a non-nested, non-generic static class. Unlike the older syntax, an extension block can contain properties as well as methods. It can also define members that appear to belong to the extended type itself rather than one particular instance. C# 14 ships with .NET 10. Microsoft’s C# 14 overview covers the complete feature set.

Here is the same DateOnly example using an extension block:

public static class DateOnlyExtensions
{
    extension(DateOnly date)
    {
        public bool IsWeekend =>
            date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;

        public DateOnly StartOfMonth =>
            new(date.Year, date.Month, 1);

        public DateOnly NextBusinessDay()
        {
            var candidate = date.AddDays(1);

            while (candidate.IsWeekend)
            {
                candidate = candidate.AddDays(1);
            }

            return candidate;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The declaration starts with:

extension(DateOnly date)
Enter fullscreen mode Exit fullscreen mode

DateOnly is the type being extended, while date is the receiver available to every instance member in the block.

The calling code now looks like this:

var releaseDate = new DateOnly(2026, 7, 24);

Console.WriteLine(releaseDate.IsWeekend);
Console.WriteLine(releaseDate.StartOfMonth);
Console.WriteLine(releaseDate.NextBusinessDay());
Enter fullscreen mode Exit fullscreen mode

The difference is not dramatic until you notice the missing parentheses on IsWeekend and StartOfMonth. They are real extension properties, not methods with property-like names.

That is the part of the feature I find more interesting than the new grouping syntax.

Extension properties improve the shape of an API

Before C# 14, developers often wrote methods such as these:

order.IsCompleted()
collection.IsEmpty()
date.IsWeekend()
response.IsSuccessful()
Enter fullscreen mode Exit fullscreen mode

They behaved like questions about the current object, but the language forced them to be methods.

With an extension property, the API can communicate that distinction more clearly:

order.IsCompleted
collection.IsEmpty
date.IsWeekend
response.IsSuccessful
Enter fullscreen mode Exit fullscreen mode

A property is a good fit when the value describes the current receiver and can be obtained cheaply. A method is still the better choice when the operation performs noticeable work, accepts parameters, mutates something or may produce an external side effect.

That is why NextBusinessDay() remains a method in the example. It has to search forward, even though the simple implementation only skips weekends.

The naming also leaves room for a more honest implementation later:

public DateOnly NextBusinessDay(
    IReadOnlySet<DateOnly> holidays)
{
    var candidate = date.AddDays(1);

    while (candidate.IsWeekend || holidays.Contains(candidate))
    {
        candidate = candidate.AddDays(1);
    }

    return candidate;
}
Enter fullscreen mode Exit fullscreen mode

The method now accepts a holiday calendar without changing the general shape of the API.

A generic extension block

Extension blocks also work with generics and constraints. A small collection example makes the syntax easier to see:

public static class CollectionExtensions
{
    extension<T>(IReadOnlyCollection<T> items)
    {
        public bool IsEmpty => items.Count == 0;

        public bool HasSingleItem => items.Count == 1;

        public T GetOnlyItem()
        {
            if (!items.HasSingleItem)
            {
                throw new InvalidOperationException(
                    $"Expected one item, but found {items.Count}.");
            }

            return items.First();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Client code treats these members as if they were declared directly on IReadOnlyCollection<T>:

IReadOnlyCollection<string> environments = ["Production"];

if (environments.HasSingleItem)
{
    Console.WriteLine(environments.GetOnlyItem());
}
Enter fullscreen mode Exit fullscreen mode

The receiver name, items, belongs to the extension declaration. We do not have to repeat this IReadOnlyCollection<T> items on every member.

That becomes useful when one type has several closely related extensions. The block makes the relationship visible without changing how callers use the API.

There is also a design detail worth noticing here: IsEmpty extends IReadOnlyCollection<T>, not IEnumerable<T>.

This version is tempting:

extension<T>(IEnumerable<T> source)
{
    public bool IsEmpty => !source.Any();
}
Enter fullscreen mode Exit fullscreen mode

It compiles, but a property can now enumerate the source. Depending on the sequence, that might execute a database query, consume a streaming iterator or repeat work the caller did not expect.

New syntax does not remove old API-design problems. If anything, extension properties make naming and cost more important because properties usually look cheap.

Static extension members

An extension block can also add members that appear on the extended type itself.

Suppose an application frequently creates dates from a fiscal year and starting month:

public static class DateOnlyExtensions
{
    extension(DateOnly)
    {
        public static DateOnly FiscalYearStart(
            int year,
            int startingMonth = 4)
        {
            ArgumentOutOfRangeException.ThrowIfLessThan(
                startingMonth,
                1);

            ArgumentOutOfRangeException.ThrowIfGreaterThan(
                startingMonth,
                12);

            return new DateOnly(year, startingMonth, 1);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Because this block does not need an instance, its receiver has a type but no parameter name:

extension(DateOnly)
Enter fullscreen mode Exit fullscreen mode

The method is called through DateOnly:

var fiscalYearStart = DateOnly.FiscalYearStart(2026);

Console.WriteLine(fiscalYearStart);
// 2026-04-01
Enter fullscreen mode Exit fullscreen mode

This reads more naturally than placing the same operation on an unrelated helper:

var fiscalYearStart =
    FiscalDateHelpers.CreateStartDate(2026);
Enter fullscreen mode Exit fullscreen mode

I would still use static extension members carefully. If a project keeps adding unrelated members to framework types, IntelliSense can become a junk drawer. The feature works best when the member feels like a natural part of the extended type and has a clear, limited scope.

What changed—and what did not

The new syntax does not turn extension members into real members of the original type. The source of DateOnly, IReadOnlyCollection<T> or any third-party class remains unchanged.

Extension members are still discovered through namespaces:

using MyApplication.Extensions;
Enter fullscreen mode Exit fullscreen mode

They also continue to have lower priority than members declared on the type itself. If the extended type already contains a matching member, the compiler binds to that member instead of the extension. Microsoft’s extension-member guide documents these lookup rules and confirms that extension-block methods and classic extension methods compile to the same IL.

This matters when naming extensions for types you do not own. A future framework or package update could add a member with the same name and change which implementation a call resolves to.

Extension members also cannot reach into private state. They only have access to members that would otherwise be accessible from the extension class. The new syntax improves organization, but it does not bypass encapsulation.

Should existing extension methods be rewritten?

Probably not just for the sake of using new syntax.

This existing method remains perfectly reasonable:

public static string Truncate(
    this string value,
    int maximumLength)
{
    ArgumentOutOfRangeException.ThrowIfNegative(maximumLength);

    return value.Length <= maximumLength
        ? value
        : value[..maximumLength];
}
Enter fullscreen mode Exit fullscreen mode

The extension-block equivalent is not automatically better:

public static class StringExtensions
{
    extension(string value)
    {
        public string Truncate(int maximumLength)
        {
            ArgumentOutOfRangeException.ThrowIfNegative(
                maximumLength);

            return value.Length <= maximumLength
                ? value
                : value[..maximumLength];
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The new form becomes more convincing when the same receiver has multiple related members, when a property communicates intent better than a method, or when a static extension member genuinely improves discoverability.

For a mature codebase, I would migrate only when a file is already being changed and the new form makes the API easier to understand. A repository-wide conversion would create a large diff without necessarily changing the experience for callers.

Microsoft’s own guidance also recommends modifying the original type when you control it and the functionality genuinely belongs there. Extensions are most useful when the source is outside your control, inheritance is unsuitable or the behavior belongs to a particular application layer.

A practical review checklist

Before adding an extension block, I would ask a few questions:

  • Does this functionality naturally belong beside the extended type?
  • Is the original source unavailable or inappropriate to modify?
  • Does a property perform cheap, unsurprising work?
  • Are related extensions grouped in a focused namespace?
  • Could the chosen name collide with a future member?
  • Would a wrapper, service or ordinary method make the dependency clearer?

If the answers are reasonable, extension blocks can produce an API that feels much more deliberate than a growing collection of utility methods.

Final thoughts

C# 14 extension blocks are easy to dismiss as rearranged extension-method syntax. For a single method, that assessment is fair. The older syntax is familiar, compact and still fully supported.

The feature starts to earn its place when a type needs a small family of related operations. Properties can express state-like questions, methods can represent actual work, and static extensions can expose narrowly scoped factory-style behavior. All of them can live together around one receiver.

That makes the implementation easier to scan and the calling code closer to the API we meant to design in the first place.

Mini-challenge

Create an extension block for HttpResponseMessage with:

  • An IsRetryable property for status codes your application considers temporary.
  • A HasJsonContent property that checks the response content type.
  • A ReadRequiredJsonAsync<T>() method that throws a useful exception when the response cannot be deserialized.

The interesting part is not getting the code to compile. It is deciding which operations deserve property syntax and which should remain methods.

Top comments (0)