Overview
This article clarifies what does and does not work when a C# 14 (.NET 10) extension block targets a static class such as System.IO.Directory.
The short answer: a static class can be the target of an extension block, but only static members can be added. Declaring an instance member is a compile error.
Which of the two you get is decided by the receiver form — whether you write the type alone or give the receiver a parameter name.
The article also covers the alternatives when instance-style calls are required, and the evolution of extension member support from C# 3.0 to C# 14.
extension block can declare when the target is a static class, verified by building against .NET SDK 10.0.302 with LangVersion 14.0. Once the receiver has a name, the block fails with CS0721 regardless of the members that follow.Prerequisites / Environment
- Language: C# 14 (
LangVersionset to14.0) - Framework: .NET 10 (verified with SDK 10.0.302)
- Target feature: Extension members (
extensionblock syntax) - Reference: Classic extension methods (
thisparameter syntax) - Verification environment: .NET SDK 10.0.302 / Windows 11 (the compilation results in this article were obtained here)
Problem
Whether an extension block targeting a static class compiles depends on how it is written.
Here is the form that does not compile.
using System.IO;
public static class DirectoryExtensions
{
// error CS0721: 'Directory': static types cannot be used as parameters
extension(Directory directory)
{
public void DeleteIfExists(string path)
{
try
{
Directory.Delete(path, true);
}
catch (DirectoryNotFoundException)
{
// Nothing to do when it is already gone.
}
}
}
}
The error is reported at Directory inside extension(Directory directory).
In other words, the receiver declaration itself is rejected, regardless of what the members contain.
Remove the parameter name directory from the receiver and make the member static, and the same Directory target compiles.
using System.IO;
public static class DirectoryExtensions
{
// This one compiles
extension(Directory)
{
public static void DeleteIfExists(string path)
{
try
{
Directory.Delete(path, true);
}
catch (DirectoryNotFoundException)
{
// Nothing to do when it is already gone.
}
}
}
}
At the call site it resolves as though it were a static method that Directory always had.
Directory.DeleteIfExists(@"C:\Temp\TargetDir");
The restriction is sometimes summarized as “extension members cannot be added to static classes,” but what actually cannot be added is instance members.
Cause
The receiver can be written in two forms
The receiver of an extension block can be written either as a type alone or as a parameter.
This is not a stylistic choice: it changes which kinds of members can be declared.
| Receiver form | Members that can be declared | Static class allowed as target |
|---|---|---|
extension(Directory) |
Static members only | Yes |
extension(Directory directory) |
Instance members (and static members) | No |
The language specification states that if the receiver parameter is named, the receiver type may not be static.
Once the receiver has a name it is an ordinary parameter, so the existing rule that a static class cannot be used as a parameter type applies unchanged.
Why a static class cannot be a parameter type
C# forbids using the name of a static class as the type of anything that holds a value.
All of the following are compile errors.
| Usage | Error |
|---|---|
Directory myDir; |
CS0723: cannot declare a variable of static type |
List<Directory> list; |
CS0718: static types cannot be used as type arguments |
void M(Directory d) |
CS0721: static types cannot be used as parameters |
extension(Directory directory) is the same situation as the last row, and the reported error is likewise CS0721.
Note that the name of a static class is not entirely unusable as a type.
typeof(Directory) is legal, and member access such as Directory.Exists(path) obviously works.
What is forbidden is using it as the type of a value-holding location: a variable, a parameter, or a type argument.
extension(Directory) compiles precisely because it declares no location that receives the receiver value.
An unnamed receiver cannot carry instance members
Conversely, declaring an instance member inside the extension(Directory) form produces a different error.
extension(Directory)
{
// error CS9303: cannot declare instance members in an extension block
// with an unnamed receiver parameter
public void DeleteIfExists(string path) { }
}
The body of an instance member refers to the receiver value, so a name for that value is required.
Without a name, no instance member can be declared.
Caught between these two errors, there is no way to write an instance-style extension member on a static class.
Measured: Compilation Result per Combination
Every combination of receiver form and member kind was compiled.
net10.0 with .NET SDK 10.0.302 and LangVersion 14.0. The last row is a control showing that a named receiver can carry an instance member as long as the type is not static (DirectoryInfo).The extension(Directory directory) rows report CS0721 regardless of member kind.
Naming the receiver settles the error before the block body is considered.
The restriction to static members on a static class is the combination of these two errors.
As the last row shows, the same “named receiver plus instance member” shape compiles without issue when the target is not a static class.
The limitation does not come from the extension block itself but from the pre-existing C# rule that a static class cannot be used as a parameter type.
Three Options
The right option depends on what you are trying to achieve.
- Option A: Call it as
Directory.Xxx(...)— declare a static extension member with a type-only receiver. - Option B: Call it as
xxx.Yyy()— declare an extension member on the corresponding instance type (DirectoryInfo). - Option C: Support C# 13 or earlier — use a plain static helper class.
Option A: Static extension member
On C# 14 or later this is the most direct approach. It adds a member that looks as though Directory always had it.
using System.IO;
namespace MyLib;
public static class DirectoryExtensions
{
extension(Directory)
{
/// <summary>
/// Deletes the directory at the specified path if it exists.
/// </summary>
public static void DeleteIfExists(string path)
{
try
{
Directory.Delete(path, true);
}
catch (DirectoryNotFoundException)
{
// Nothing to do when it is already gone.
}
}
/// <summary>The default working directory.</summary>
public static string DefaultRoot => @"C:\Temp";
}
}
The call site looks as follows.
using System;
using System.IO;
using MyLib; // omit this and you get CS0117
Directory.DeleteIfExists(@"C:\Temp\TargetDir");
Console.WriteLine(Directory.DefaultRoot);
As the comment notes, without a using for the namespace that declares the extension members, the compiler reports CS0117 — Directory does not contain a definition for DeleteIfExists.
This is the same constraint that applies to classic extension methods, not something specific to static extension members.
Option B: Extension member on DirectoryInfo
DirectoryInfo is an ordinary instantiable type, so its receiver can carry a parameter name.
This is the option to use when calls should be consistently instance-style.
using System.IO;
public static class DirectoryInfoExtensions
{
// DirectoryInfo is not a static class, so the parameter form is allowed
extension(DirectoryInfo directoryInfo)
{
/// <summary>
/// Deletes the directory if it exists.
/// </summary>
public void DeleteIfExists()
{
try
{
directoryInfo.Delete(true);
}
catch (DirectoryNotFoundException)
{
// Nothing to do when it is already gone.
}
}
}
}
The call site looks as follows.
var dir = new DirectoryInfo(@"C:\Temp\TargetDir");
dir.DeleteIfExists();
Creating a DirectoryInfo instance is an extra step, but it is convenient when the path is resolved once and several operations follow.
Option C: Static helper class
When C# 13 or earlier must be supported, extension blocks are unavailable, so a conventional static class is the fallback.
using System.IO;
public static class DirectoryHelper
{
/// <summary>
/// Deletes the directory at the specified path if it exists.
/// </summary>
public static void DeleteIfExists(string path)
{
try
{
Directory.Delete(path, true);
}
catch (DirectoryNotFoundException)
{
// Nothing to do when it is already gone.
}
}
}
DirectoryHelper.DeleteIfExists(@"C:\Temp\TargetDir");
The Directory.DeleteIfExists(...) form is not available, but the location of the method is unambiguous.
How to Choose
Which of the three applies is settled by the call form and the target version.
Calling it as Directory.Xxx(path) calls for Option A.
It matches the shape of the standard API, so it blends into existing code. The receiver carries only the type, and instance members cannot be declared there.
Unifying on the xxx.Yyy() instance form calls for Option B.
It extends a corresponding instance type such as DirectoryInfo. Resolving a path once and performing several operations benefits from holding on to the instance. The API shapes genuinely differ, though: the counterpart of Directory.Exists(path) is the directoryInfo.Exists property, one taking an argument and the other not.
Supporting C# 13 or earlier calls for Option C.
The extension block is available only from C# 14 (.NET 10). Anything earlier falls back to a conventional static helper class. The call form no longer matches the standard API, but the location is the most obvious of the three.
Options A and B both require the declaring namespace to be imported at the call site. Forgetting it yields CS0117, which does not obviously indicate a failed extension member lookup.
Comparing the Options
| Approach | Call form | Pros | Cons | Best suited for |
|---|---|---|---|---|
| Static extension member (Option A) | Directory.DeleteIfExists(path) |
Looks identical to the standard API | C# 14 or later only; a missing using is easy to misdiagnose |
Filling gaps in a static class API on C# 14 or later |
Extension member on DirectoryInfo (Option B) |
dir.DeleteIfExists() |
Consistent instance-style calls | Requires an instance; C# 14 or later only | Resolving a path once and performing several operations |
| Static helper class (Option C) | DirectoryHelper.DeleteIfExists(path) |
No version dependency; obvious location | Call form differs from the standard API | Supporting C# 13 or earlier |
Notes
- An
extensionblock that extends a static class cannot contain user-defined operators (CS9321).
An operator must take the extended type as a parameter, and a static class cannot be one. - The classic
this-parameter syntax declares instance extension methods only; it cannot attach static members.
Option A is therefore available only from C# 14, where theextensionblock was introduced.
Supplementary: Evolution of Extension Members from C# 3.0 to C# 14
Support for attaching members to existing types has expanded incrementally across C# versions.
| Version | Platform | Key change |
|---|---|---|
| C# 3.0 | .NET Framework 3.5 | Extension methods introduced. A this-prefixed first parameter inside a public static class allows instance methods to be attached to existing types. This feature underpins LINQ. |
| C# 7.2 | .NET Core 2.0 / .NET Framework 4.7.2 | Improved support for value types. The ref this and in this modifiers became available, enabling extension methods on large structs to pass the receiver by reference without copying. |
| C# 14 | .NET 10 | Extension member syntax (extension block) introduced. In addition to the this-parameter form, members can be declared inside an extension(Type) block, which supports properties, indexers, and operators as well as methods — and static members. |
The this-parameter syntax can only declare instance extension methods; before C# 14 there was no way to attach a static member at all.
Adding a member such as Directory.DeleteIfExists(...) to a static class first became possible with the C# 14 extension block.
Summary
Targeting a static class with an extension block is possible.
What is not possible is adding an instance member: naming the receiver turns the static class into a parameter type, which produces CS0721.
The deciding factors are the call form and the target version.
Option A matches the standard API’s shape, Option B unifies on instance-style calls, and Option C covers C# 13 or earlier.