Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 60 additions & 27 deletions docs/csharp/whats-new/tutorials/extension-members.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,51 @@
---
title: Explore extension members in C# 14 to enhance existing types
description: "C# 14 provides new syntax for extensions that support properties and operators, and enables extensions on a type as well as an instance. Learn to use them, and how to migrate existing extension methods to extension members"
title: Explore extension members in C# 14 and C# 15 to enhance existing types
description: "C# 14 extension members add properties and operators to existing types. C# 15 extension indexers add indexed access. Learn both with runnable Point and Path examples."
author: billwagner
ms.author: wiwagn
ms.service: dotnet-csharp
ms.topic: tutorial
ms.date: 10/06/2025
ms.date: 07/15/2026
ai-usage: ai-assisted
#customer intent: As a C# developer, I reduce repeated code by introducing extension members for common tasks
---
# Tutorial: Explore extension members in C# 14
# Tutorial: Explore extension members in C# 14 and C# 15

C# 14 introduces extension members, an enhancement to the existing extension methods. Extension members enable you to add properties and operators. You can also extend types in addition to instances of types. This capability allows you to create more natural and expressive APIs when extending types you don't control.
C# 14 introduced extension members, an enhancement to the existing extension methods. Extension members enable you to add properties and operators. You can also extend types as well as instances of types. C# 15 adds extension indexers, so an existing type can support indexed access from an extension block.

In this tutorial, you explore extension members by enhancing the `System.Drawing.Point` type with mathematical operations, coordinate transformations, and utility properties. You learn how to migrate existing extension methods to the new extension member syntax and understand when to use each approach.
In this tutorial, you explore extension members by enhancing the `System.Drawing.Point` type with mathematical operations, coordinate transformations, and utility properties. Then you add indexed access to a `Path` type that stores point-to-point offsets. You learn how to migrate existing extension methods to the new extension member syntax and when each approach fits.

In this tutorial, you:

> [!div class="checklist"]
>
> * Create extension members with static properties and operators.
> * Create C# 14 extension members with static properties and operators.
> * Implement coordinate transformations using extension members.
> * Migrate traditional extension methods to extension member syntax.
> * Add a C# 15 extension indexer that reads and updates absolute points in a path.
> * Compare extension members with traditional extension methods.

## Prerequisites

- The .NET 10 SDK. Download it from the [.NET download site](https://dotnet.microsoft.com/download/dotnet/10.0).
- Visual Studio 2026. Download it from the [Visual Studio page](https://visualstudio.microsoft.com).
- The .NET 11 preview SDK. Download it from the [.NET download site](https://dotnet.microsoft.com/download/dotnet/11.0).
- Visual Studio 2026 with preview features enabled. Download it from the [Visual Studio page](https://visualstudio.microsoft.com).
- This sample sets `<LangVersion>preview</LangVersion>` because extension indexers are a C# 15 preview feature.
Comment thread
BillWagner marked this conversation as resolved.

## Create the sample application

Start by creating a console application that demonstrates both traditional extension methods and the new extension members syntax. You'll create extensions for the <xref:System.Drawing.Point?displayProperty=fullName> type. This type comes from the `System.Drawing` namespace and is typically used in Windows Forms applications.
Start by creating a console application that demonstrates both traditional extension methods and the new extension members syntax. You create extensions for the <xref:System.Drawing.Point?displayProperty=fullName> type. This type comes from the `System.Drawing` namespace and is typically used in Windows Forms applications.

1. Create a new console application:
1. Create a new console application.

```dotnetcli
dotnet new console -n PointExtensions
cd PointExtensions
```

1. Update the project file so the sample targets .NET 11 and uses preview language features:

:::code language="xml" source="snippets/PointExtensions/PointExtensions.csproj":::

1. Copy the following code into a new file named `ExtensionMethods.cs`:

:::code language="csharp" source="snippets/PointExtensions/ExtensionMethods.cs":::
Expand Down Expand Up @@ -95,11 +101,11 @@ Next, examine the following code that performs arithmetic with points:

:::code language="csharp" source="snippets/PointExtensions/IncludedElements.cs" id="PointArithmetic":::

Traditional extension methods can't add operators to existing types. You must implement arithmetic operations manually, making the code verbose and harder to read. The algorithm gets duplicated whenever the operation is needed, which creates more opportunities for small mistakes to enter the code base. It's better to place that code in one location. Add the following operators to your extension block in `NewExtensionsMembers.cs`:
Traditional extension methods can't add operators to existing types. You must implement arithmetic operations manually, which makes the code verbose and harder to read. The algorithm gets duplicated whenever you need the operation, which creates more opportunities for small mistakes to enter the code base. It's better to place that code in one location. Add the following operators to your extension block in `NewExtensionsMembers.cs`:

:::code language="csharp" source="snippets/PointExtensions/NewExtensionsMembers.cs" id="ArithmeticOperators":::

Extension members enable you to add operators directly to existing types. Now you can perform arithmetic operations using natural syntax:
By using extension members, you can add operators directly to existing types. Now you can perform arithmetic operations by using natural syntax:

:::code language="csharp" source="snippets/PointExtensions/ExtensionMemberDemonstrations.cs" id="PointArithmeticWithOperators":::

Expand Down Expand Up @@ -135,27 +141,49 @@ Extension members use a different syntax but provide the same functionality. Add

:::code language="csharp" source="snippets/PointExtensions/NewExtensionsMembers.cs" id="TransformationMethods":::

The preceding code doesn't compile yet. It's the first extension you wrote that extends an *instance* of the `Point` class, instead of the type itself. To support instance extensions, your extension block needs to name the receiver parameter. Edit the following line:
These methods extend an instance of the `Point` struct, not the `Point` type. The extension block names the receiver parameter so the method body can read that point. The sample uses `extension(ref Point point)` because `Translate`, `Scale`, and `Rotate` change the caller's `Point`. Without `ref`, those methods would update a copy of the struct, and the caller wouldn't see the change.

```csharp
extension (Point)
```
You can call these new instance methods exactly as you accessed traditional extension methods:

:::code language="csharp" source="snippets/PointExtensions/ExtensionMemberDemonstrations.cs" id="InstanceMethods":::

So that it gives a name to the `Point` instance:
The key difference is syntax: extension members use `extension (Type variableName)` instead of `this Type variableName`.

```csharp
extension (Point point)
```
## Add extension indexers

Now, the code compiles. You can call these new instance methods exactly as you accessed traditional extension methods:
C# 15 adds indexers to `extension` blocks. An indexer has no name. Code accesses it with `this[...]` in the declaration and with indexed syntax at the call site.

:::code language="csharp" source="snippets/PointExtensions/ExtensionMemberDemonstrations.cs" id="InstanceMethods":::
Imagine a path type that stores each step as a relative offset. When you ask for `path[i]`, you want the absolute point at that step. When you assign `path[i] = target`, you want the type to update the one offset that gets you there. Indexed access reads like "the point at this step" and keeps the offset-to-point math in one place.

The key difference is syntax: extension members use `extension (Type variableName)` instead of `this Type variableName`.
Imagine that `Path` came from a library. If you don't own the type, you can't add an indexer to its source. Before C# 15, you could add methods, but you couldn't add `this[...]` indexed access to a type you don't control. This tutorial defines `Path` so the sample is runnable; think of it as standing in for that library type.

For this section, add a `Path` type. The type stores a sequence of `(dX, dY)` offsets. Each offset says how far to move from the previous point. The first offset starts at `Point.Origin`, the static extension property you added earlier.

Create a new file named `Path.cs` in the same project as the other sample files. Add the `Path` type to the `ExtensionMembers` namespace in that file. Keeping `Path` in this namespace makes it your sample type, not <xref:System.IO.Path>. The demo file uses a `using Path = ExtensionMembers.Path;` alias, so every `Path` in the demo means the sample path type.

:::code language="csharp" source="snippets/PointExtensions/Path.cs" id="PathType":::

Now add an indexer for `Path`. Put this code in the existing `PointExtensions` static class in `NewExtensionsMembers.cs`. Add it as a new `extension(Path path)` block, separate from the `extension(Point)` block for static members and operators and separate from the `extension(ref Point point)` block for instance methods:

:::code language="csharp" source="snippets/PointExtensions/NewExtensionsMembers.cs" id="PathIndexer":::

Indexers are always instance members, so this new extension block must name the receiver: `extension(Path path)`. A block written as `extension(Path)` wouldn't provide a `path` variable for the indexer body.

`Path` is a class that owns a list of offsets. The indexer doesn't need a `ref` receiver because the setter changes the contents of that existing `Path` object.

The getter starts at `Point.Origin`, then adds the offsets from index `0` through the requested index. With offsets `(2, 3)`, `(1, 1)`, and `(-1, 4)`, the absolute points are `(2, 3)`, `(3, 4)`, and `(2, 8)`.

The setter receives a target absolute point. It leaves earlier offsets alone and changes only the offset at the requested index. The new offset is the target point minus the absolute point at the previous index. Later points shift because they remain relative to the changed offset.

Now, use the indexer to read and write points along the path:

:::code language="csharp" source="snippets/PointExtensions/ExtensionMemberDemonstrations.cs" id="PathIndexerUse":::

Both accessors share a small private `ValidatePathIndex` helper in the same `PointExtensions` class that bounds-checks the index and throws <xref:System.ArgumentOutOfRangeException> when it doesn't refer to an offset in the path.

## Completed sample

The final example shows the advantages when you combine static properties, operators, and instance methods to create comprehensive type extensions.
The final example shows the advantages when you combine static properties, operators, instance methods, and an indexer to create comprehensive type extensions.

Compare the extension member version:

Expand All @@ -171,20 +199,25 @@ This example demonstrates how extension members create a cohesive API that feels
- Apply mathematical operators naturally (`point + offset`, `point * scale`)
- Chain transformations using both operators and methods
- Convert between related types (`ToVector()`)
- Read and update absolute points along a path with `path[index]`

### Migration benefits

When migrating from traditional extension methods to extension members, you gain:
When you migrate from traditional extension methods to extension members, you get:

1. **Static properties**: Add constants and computed values to types.
1. **Operators**: Enable natural mathematical and logical operations.
1. **Indexers**: Add C# 15 indexed access that can compute values from an existing type and update its stored state.
1. **Unified syntax**: All extension logic uses the same `extension` declaration.
1. **Type-level extensions**: Extend the type itself, not just instances.
1. **Type-level extensions**: Extend the type itself, not only instances.

Run the complete application to see both approaches side by side and observe how extension members provide a more integrated development experience.

## Related content

- [Extension methods (C# Programming Guide)](../../programming-guide/classes-and-structs/extension-methods.md)
- [What's new in C# 14](../csharp-14.md)
- [What's new in C# 15](../csharp-15.md)
- [`extension` keyword (C# reference)](../../language-reference/keywords/extension.md)
- [Extension indexers feature specification](~/_csharplang/proposals/extension-indexers.md)
- [Operator overloading (C# reference)](../../language-reference/operators/operator-overloading.md)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Drawing;
using System.Numerics;
using ExtensionMembers;
using Path = ExtensionMembers.Path;

public static class ExtensionMemberDemonstrations
{
Expand All @@ -13,6 +14,7 @@ public static void NewExtensionMembers()
ArithmeticWithPoints();
DiscreteArithmeticWithPoints();
ExtensionMethods();
PathIndexer();
MoreExamples();
}

Expand Down Expand Up @@ -117,10 +119,29 @@ static void ExtensionMethods()
// </InstanceMethods>
}

static void PathIndexer()
{
// <PathIndexerUse>
Console.WriteLine("5. Path Indexer");
Console.WriteLine("---------------");

Path path = new([(dX: 2, dY: 3), (dX: 1, dY: 1), (dX: -1, dY: 4)]);
Console.WriteLine($"First point: {path[0]}");
Console.WriteLine($"Second point: {path[1]}");
Console.WriteLine($"Third point: {path[2]}");

path[1] = new Point(10, 10);
Console.WriteLine("After setting the second point to {X=10,Y=10}:");
Console.WriteLine($"Second point: {path[1]}");
Console.WriteLine($"Third point: {path[2]}");
Console.WriteLine();
// </PathIndexerUse>
}

static void MoreExamples()
{
// <FinalScenarios>
Console.WriteLine("5. Complex Scenarios");
Console.WriteLine("6. Complex Scenarios");
Console.WriteLine("-------------------");

// Combining operators and methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace ExtensionMembers;

public static class PointExtensions
{
extension(ref Point point)
extension(Point)
{
public static Point Origin => Point.Empty;

Expand All @@ -27,7 +27,10 @@ public static class PointExtensions
public static Point operator -(Point left, (int dx, int dy) scale) =>
new Point(left.X - scale.dx, left.Y - scale.dy);
// </TupleBasedXYOperators>
}

extension(ref Point point)
{
// <TransformationMethods>
public Vector2 ToVector() =>
new Vector2(point.X, point.Y);
Expand Down Expand Up @@ -55,6 +58,49 @@ public void Rotate(int angleInDegrees)
point.Y = (int)newY;
}
// </TransformationMethods>
}

// <PathIndexer>
extension(Path path)
{
public Point this[int index]
{
get
{
ValidatePathIndex(path, index);

Point absolutePoint = Point.Origin;
for (int current = 0; current <= index; current++)
{
var offset = path.GetOffset(current);
absolutePoint += offset;
}

return absolutePoint;
}
set
{
ValidatePathIndex(path, index);

Point previousPoint = Point.Origin;
for (int current = 0; current < index; current++)
{
var offset = path.GetOffset(current);
previousPoint += offset;
}

path.SetOffset(index, (value.X - previousPoint.X, value.Y - previousPoint.Y));
}
}
}

private static void ValidatePathIndex(Path path, int index)
{
if (index < 0 || index >= path.Count)
{
throw new ArgumentOutOfRangeException(nameof(index), index,
"Index must refer to an offset in the path.");
}
}
// </PathIndexer>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace ExtensionMembers;

// <PathType>
public sealed class Path
{
private readonly List<(int dX, int dY)> offsets = [];

public Path(IEnumerable<(int dX, int dY)> offsets)
{
this.offsets.AddRange(offsets);
}

public int Count => offsets.Count;

internal (int dX, int dY) GetOffset(int index) => offsets[index];

internal void SetOffset(int index, (int dX, int dY) offset) =>
offsets[index] = offset;
}
// </PathType>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net11.0</TargetFramework>
<LangVersion>preview</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Loading