Hey there! ππ»
Just used the "Generate Image" function of Dev to generate this cover, and yes, I know it says C 14 rather than C# 14.
If you've written C# code before, there's a good chance you've come across extension methods. They've been around since C# 3.0 back in 2007, and they're pretty much everywhere in .NETβespecially in LINQ. But with C# 14 and .NET 10, things just got a whole lot more interesting with the introduction of Extension Blocks, or as some folks call it, Extension Everything.
So what are extension blocks? Why should you care? And how can you use them to write cleaner, more expressive code? Let's dive in and find out!
Prerequisites π οΈ
The code examples in this article require .NET 10 and C# 14, which were officially released on November 11, 2025. If you haven't already installed .NET 10, you can download it from:
Official Download: https://dotnet.microsoft.com/download/dotnet/10.0
What's New Documentation:
With that out of the way, now we can dive in!!
A Quick Refresher on Extension Methods π
Before we jump into the new stuff, let's quickly recap how extension methods work. If you've used LINQ, you've definitely used extension methods without even knowing it.
βΉοΈ I did write an old post about extenion methods that contains some examples as well, which you can read here if you're interested.
Here's a classic example:
public static class StringExtensions
{
public static bool IsEmpty(this string str)
{
return string.IsNullOrEmpty(str);
}
}
The magic here is the this keyword in front of the first parameter. That's what makes this a special method that can be called as if it were an instance method on the string type:
string message = "Hello World";
if (!message.IsEmpty())
{
Console.WriteLine(message);
}
Pretty neat, right? This has been incredibly useful for extending types you don't own, like built-in types or types from third-party libraries.
The Problem With Traditional Extension Methods π€
While extension methods are awesome, they've always had some limitations:
- You can only extend with methods, not properties or operators
-
Repetitive code - if you have multiple extensions for the same type, you have to keep repeating
this TypeName parameterin every method - No static extensions - you couldn't add static-like members to types
- Organization issues - grouping related extensions wasn't as clean as it could be
Let's say you wanted to add an IsEmpty property to IEnumerable<T>. With traditional extension methods, you'd have to write it as a method:
// This works, but feels awkward
if (myList.IsEmpty())
{
Console.WriteLine("List is empty");
}
Wouldn't it be nicer to write it like a property?
// This is what we really want!
if (myList.IsEmpty)
{
Console.WriteLine("List is empty");
}
Well, C# 14 heard our prayers!
Introducing Extension Blocks π
Extension blocks are the new way to define extensions in C# 14. They give you the power to extend types with not just methods, but also properties, operators, and even static members. Let me show you what this looks like.
Converting a Traditional Extension Method
Let's take our IsEmpty extension method and convert it to the new syntax:
Old way (still works):
public static class EnumerableExtensions
{
public static bool IsEmpty(this IEnumerable source)
{
return !source.Any();
}
}
New way with extension blocks:
public static class EnumerableExtensions
{
extension(IEnumerable source)
{
public bool IsEmpty => !source.Any();
}
}
Notice the differences:
- We use the
extensionkeyword followed by parentheses - The type we're extending (
IEnumerable<T>) goes inside the parentheses along with a parameter name (source) - Inside the curly braces, we can define our extensions - and now it can be a property instead of a method!
- We no longer need the
thiskeyword or repeatpublic staticfor each member
The best part? From the caller's perspective, it works exactly the same way:
var numbers = new List { 1, 2, 3 };
// Both of these work!
bool isEmpty1 = numbers.IsEmpty(); // Old method syntax
bool isEmpty2 = numbers.IsEmpty; // New property syntax
Real-World Example: String Extensions π
Let me show you a practical example that demonstrates the power of extension blocks. Imagine you want to extend the string type with several useful utilities:
public static class StringExtensions
{
extension(string str)
{
// Extension property - no parentheses needed!
public bool IsEmpty => string.IsNullOrEmpty(str);
// Extension property with logic
public bool IsValidEmail => str.Contains("@") && str.Contains(".");
// Extension method
public string Truncate(int maxLength)
{
if (str.Length <= maxLength)
return str;
return str.Substring(0, maxLength) + "...";
}
// Another extension method
public string Repeat(int count)
{
return string.Concat(Enumerable.Repeat(str, count));
}
}
}
Now look how clean the calling code becomes:
string email = "user@example.com";
string name = "Bob";
string longText = "This is a very long text that needs truncating";
// Using extension properties
if (!email.IsEmpty && email.IsValidEmail)
{
Console.WriteLine("Valid email!");
}
// Using extension methods
Console.WriteLine(name.Repeat(3)); // BobBobBob
Console.WriteLine(longText.Truncate(20)); // This is a very lon...
See how natural that feels? No awkward parentheses on IsEmpty() and IsValidEmail() - they're actual properties now!
Multiple Extensions in One Block ποΈ
One of the coolest features of extension blocks is that you can group multiple related extensions together. This makes your code much more organized and eliminates repetition:
public static class ListExtensions
{
extension(List list)
{
// Property to check if list has any items
public bool HasItems => list.Count > 0;
// Property to get the last item safely
public T? LastOrDefault => list.Count > 0 ? list[^1] : default;
// Method to add multiple items
public void AddRange(params T[] items)
{
foreach (var item in items)
{
list.Add(item);
}
}
// Method to shuffle the list
public void Shuffle()
{
Random rng = new Random();
int n = list.Count;
while (n > 1)
{
n--;
int k = rng.Next(n + 1);
(list[k], list[n]) = (list[n], list[k]);
}
}
}
}
Usage:
var tasks = new List();
tasks.AddRange("Write code", "Test code", "Deploy code");
if (tasks.HasItems)
{
Console.WriteLine($"Last task: {tasks.LastOrDefault}");
tasks.Shuffle();
Console.WriteLine("Tasks shuffled!");
}
Static Extensions - The Game Changer π
Here's where things get really interesting. Extension blocks also support static extensions. These are extensions that apply to the type itself, not to instances of the type.
To define static extensions, you omit the parameter name in the extension block:
public static class ListExtensions
{
// Static extension block - no parameter name!
extension(List)
{
// Static property
public static List Empty => new List();
// Static method
public static List CreateWithCapacity(int capacity)
{
return new List(capacity);
}
}
}
Now you can call these as if they were static members of the List<T> type:
// Using static extensions
var emptyList = List.Empty;
var capacityList = List.CreateWithCapacity(100);
Console.WriteLine($"Empty list count: {emptyList.Count}");
Console.WriteLine($"Capacity list capacity: {capacityList.Capacity}");
This is particularly powerful when you want to add factory methods or utility functions that are logically related to a type but don't operate on a specific instance.
A Practical Example: Building a Validator π
Let me show you how extension blocks can make validation code much cleaner. Imagine you're building a simple user registration system:
public class User
{
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public int Age { get; set; }
}
public static class UserExtensions
{
extension(User user)
{
// Validation properties
public bool HasValidUsername =>
!string.IsNullOrWhiteSpace(user.Username) &&
user.Username.Length >= 3;
public bool HasValidEmail =>
!string.IsNullOrWhiteSpace(user.Email) &&
user.Email.Contains("@");
public bool IsAdult => user.Age >= 18;
public bool IsValid =>
user.HasValidUsername &&
user.HasValidEmail &&
user.IsAdult;
// Validation method with detailed errors
public List GetValidationErrors()
{
var errors = new List();
if (!user.HasValidUsername)
errors.Add("Username must be at least 3 characters");
if (!user.HasValidEmail)
errors.Add("Email must be valid");
if (!user.IsAdult)
errors.Add("User must be 18 or older");
return errors;
}
}
}
Using these extensions becomes super clean:
var newUser = new User
{
Username = "Bo", // Too short!
Email = "invalid-email",
Age = 16
};
if (!newUser.IsValid)
{
Console.WriteLine("User validation failed:");
foreach (var error in newUser.GetValidationErrors())
{
Console.WriteLine($"- {error}");
}
}
// Output:
// User validation failed:
// - Username must be at least 3 characters
// - Email must be valid
// - User must be 18 or older
Working With Generic Types π―
Extension blocks work beautifully with generic types, and the syntax stays clean and readable:
public static class DictionaryExtensions
{
extension(Dictionary dict)
where TKey : notnull
{
// Property to check if dictionary is empty
public bool IsEmpty => dict.Count == 0;
// Safe get method that returns default if key doesn't exist
public TValue? GetOrDefault(TKey key)
{
return dict.TryGetValue(key, out var value) ? value : default;
}
// Method to merge another dictionary
public void Merge(Dictionary other)
{
foreach (var kvp in other)
{
dict[kvp.Key] = kvp.Value;
}
}
}
}
Usage:
var settings = new Dictionary
{
["MaxConnections"] = 100,
["Timeout"] = 30
};
var additionalSettings = new Dictionary
{
["RetryCount"] = 3
};
settings.Merge(additionalSettings);
// Safe access without throwing exceptions
int retries = settings.GetOrDefault("RetryCount"); // 3
int unknown = settings.GetOrDefault("Unknown"); // 0 (default)
if (!settings.IsEmpty)
{
Console.WriteLine($"Settings loaded: {settings.Count} items");
}
Mixing Old and New Syntax π
Here's the best part - you don't have to convert all your existing extension methods to use the new syntax! Both styles work perfectly together in the same static class:
public static class StringExtensions
{
// Old style extension method
public static string Reverse(this string str)
{
return new string(str.Reverse().ToArray());
}
// New style extension block
extension(string str)
{
public bool IsEmpty => string.IsNullOrEmpty(str);
public string Capitalize()
{
if (str.IsEmpty)
return str;
return char.ToUpper(str[0]) + str.Substring(1).ToLower();
}
}
}
Both styles are compiled to the same IL code, so there's no performance difference. You can migrate to the new syntax gradually, or keep using the old syntax if you prefer - it's totally up to you!
Important Things to Know β οΈ
While extension blocks are powerful, there are a few limitations you should be aware of:
1. No Extension Fields
You can't add fields to types through extensions. This means auto-implemented properties won't work:
extension(User user)
{
// β This won't work - it would need a backing field
public string FullName { get; set; }
// β
This works - no backing field needed
public string FullName => $"{user.FirstName} {user.LastName}";
}
2. Generic Type Parameters Must Be Used
If you have generic type parameters, they all need to be used in the receiver type:
// β This won't work - T2 is not used in the receiver
extension(List list)
{
public void DoSomething() { }
}
// β
This works - all type parameters are in the receiver
extension(Dictionary dict)
{
public bool IsEmpty => dict.Count == 0;
}
3. Can't Override Existing Members
Extension members can't override actual members of the type. If the type already has a method or property with the same name, the extension won't be accessible (the type's own member takes priority):
extension(string str)
{
// This won't override string.Length!
public int Length => str.Length * 2;
}
Real-World Scenario: Query Builder ποΈ
Let me show you a more complex real-world example - a query builder for filtering collections. This demonstrates how extension blocks can make your code more fluent and easier to read:
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public bool InStock { get; set; }
}
public static class ProductQueryExtensions
{
extension(IEnumerable products)
{
public IEnumerable InStockOnly =>
products.Where(p => p.InStock);
public IEnumerable Expensive =>
products.Where(p => p.Price > 100);
public IEnumerable Cheap =>
products.Where(p => p.Price < 50);
public IEnumerable ByPriceRange(decimal min, decimal max)
{
return products.Where(p => p.Price >= min && p.Price <= max);
}
public IEnumerable SearchByName(string keyword)
{
return products.Where(p =>
p.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase));
}
}
}
Now look how beautiful the calling code becomes:
var products = new List
{
new() { Id = 1, Name = "Laptop", Price = 1200, InStock = true },
new() { Id = 2, Name = "Mouse", Price = 25, InStock = true },
new() { Id = 3, Name = "Keyboard", Price = 75, InStock = false },
new() { Id = 4, Name = "Monitor", Price = 300, InStock = true }
};
// Chain extensions naturally!
var affordableInStock = products
.InStockOnly
.ByPriceRange(20, 200);
foreach (var product in affordableInStock)
{
Console.WriteLine($"{product.Name}: ${product.Price}");
}
// Output:
// Mouse: $25
// Monitor: $300
Why You Should Care About Extension Blocks π‘
Let me tell you why this feature is a big deal:
- Cleaner syntax - Properties look like properties, not methods with empty parentheses
- Better organization - Group related extensions in one place
- Static extensions - Add factory methods and utilities to types naturally
- Less repetition - Define the receiver once, use it for multiple members
- Forward compatible - Works alongside existing extension methods
Extension blocks don't replace everything you can do with extension methods, but they make the common cases much more elegant and intuitive.
Conclusion β
Extension blocks in C# 14 are a significant evolution of the extension method concept. They give you the flexibility to extend types with properties, operators, and static members, all while maintaining the simplicity and clarity that makes C# such a joy to work with.
Whether you're building utility libraries, creating fluent APIs, or just trying to make your code more readable, extension blocks give you powerful new tools to work with. And the best part? You don't have to change any of your existing code - the new syntax works alongside the old, so you can adopt it at your own pace.
I encourage you to play around with extension blocks when .NET 10 drops, and see how they can improve your codebase. Start with small utilities, try converting some of your existing extension methods, and explore the possibilities of static extensions and extension properties.
I hope this post gave you a solid understanding of what extension blocks are and how to use them. Now equipped with this knowledge, you can write cleaner, more expressive C# code!
Top comments (0)