Extension Blocks in C# 14 Are More Than New Syntax

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 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(IReadOnlyCollection 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: IReadOnlyCollection 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 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, not IEnumerable. This version is tempting: extension(IEnumerable 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 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 () 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.

Original Source

Read the full article at Dev →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.