From f02f78db577925609c08989b170f057f5dc05842 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 15 Jul 2026 10:29:34 -0400 Subject: [PATCH 1/3] Add C# 15 extension indexers to extension members tutorial Update the extension members tutorial to cover the C# 15 addition of extension indexers. Reframe the tutorial for C# 14 and C# 15, bump the sample to .NET 11 preview with LangVersion preview, and add an "Add extension indexers" section. The new section introduces a Path type that stores (dX, dY) offsets and adds an extension indexer whose getter computes the absolute Point at an index (from Point.Origin) and whose setter adjusts the offset at that index. Build and run verified on the .NET 11 preview SDK. Closes #54658 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fb88c54-e7d1-47af-b8f2-d60b34e50426 --- .../whats-new/tutorials/extension-members.md | 73 +++++++++++++------ .../ExtensionMemberDemonstrations.cs | 23 +++++- .../PointExtensions/NewExtensionsMembers.cs | 48 +++++++++++- .../snippets/PointExtensions/Path.cs | 20 +++++ .../PointExtensions/PointExtensions.csproj | 3 +- 5 files changed, 142 insertions(+), 25 deletions(-) create mode 100644 docs/csharp/whats-new/tutorials/snippets/PointExtensions/Path.cs diff --git a/docs/csharp/whats-new/tutorials/extension-members.md b/docs/csharp/whats-new/tutorials/extension-members.md index 539ba6d78d29f..990f894cf004f 100644 --- a/docs/csharp/whats-new/tutorials/extension-members.md +++ b/docs/csharp/whats-new/tutorials/extension-members.md @@ -1,33 +1,35 @@ --- -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 `preview` because extension indexers are a C# 15 preview feature. ## Create the sample application @@ -40,6 +42,10 @@ Start by creating a console application that demonstrates both traditional exten 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"::: @@ -135,27 +141,45 @@ 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: -So that it gives a name to the `Point` instance: +:::code language="csharp" source="snippets/PointExtensions/ExtensionMemberDemonstrations.cs" id="InstanceMethods"::: -```csharp - extension (Point point) -``` +The key difference is syntax: extension members use `extension (Type variableName)` instead of `this Type variableName`. -Now, the code compiles. You can call these new instance methods exactly as you accessed traditional extension methods: +## Add extension indexers -:::code language="csharp" source="snippets/PointExtensions/ExtensionMemberDemonstrations.cs" id="InstanceMethods"::: +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. -The key difference is syntax: extension members use `extension (Type variableName)` instead of `this Type variableName`. +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. + +The sample defines `Path` in the `ExtensionMembers` namespace. That keeps the sample type separate from . 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`: + +:::code language="csharp" source="snippets/PointExtensions/NewExtensionsMembers.cs" id="PathIndexer"::: + +Indexers are always instance members, so the extension block names 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 throw when the index 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: @@ -171,6 +195,7 @@ 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 @@ -178,8 +203,9 @@ When migrating from traditional extension methods to extension members, you gain 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. @@ -187,4 +213,7 @@ Run the complete application to see both approaches side by side and observe how - [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) diff --git a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/ExtensionMemberDemonstrations.cs b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/ExtensionMemberDemonstrations.cs index 6f7fb74cbd0d7..5e1ca44f14ac9 100644 --- a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/ExtensionMemberDemonstrations.cs +++ b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/ExtensionMemberDemonstrations.cs @@ -1,6 +1,7 @@ using System.Drawing; using System.Numerics; using ExtensionMembers; +using Path = ExtensionMembers.Path; public static class ExtensionMemberDemonstrations { @@ -13,6 +14,7 @@ public static void NewExtensionMembers() ArithmeticWithPoints(); DiscreteArithmeticWithPoints(); ExtensionMethods(); + PathIndexer(); MoreExamples(); } @@ -117,10 +119,29 @@ static void ExtensionMethods() // } + static void PathIndexer() + { + // + 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(); + // + } + static void MoreExamples() { // - Console.WriteLine("5. Complex Scenarios"); + Console.WriteLine("6. Complex Scenarios"); Console.WriteLine("-------------------"); // Combining operators and methods diff --git a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs index de80d85881d67..11df6072a4573 100644 --- a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs +++ b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs @@ -5,7 +5,7 @@ namespace ExtensionMembers; public static class PointExtensions { - extension(ref Point point) + extension(Point) { public static Point Origin => Point.Empty; @@ -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); // + } + extension(ref Point point) + { // public Vector2 ToVector() => new Vector2(point.X, point.Y); @@ -55,6 +58,49 @@ public void Rotate(int angleInDegrees) point.Y = (int)newY; } // + } + + 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."); + } } } diff --git a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/Path.cs b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/Path.cs new file mode 100644 index 0000000000000..b4a32ff935140 --- /dev/null +++ b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/Path.cs @@ -0,0 +1,20 @@ +namespace ExtensionMembers; + +// +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; +} +// diff --git a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/PointExtensions.csproj b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/PointExtensions.csproj index ed9781c223ab9..1de1aa47a7270 100644 --- a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/PointExtensions.csproj +++ b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/PointExtensions.csproj @@ -2,7 +2,8 @@ Exe - net10.0 + net11.0 + preview enable enable From 71292949fe6f3bccf235cec70d99352ed81e53ad Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 15 Jul 2026 11:57:12 -0400 Subject: [PATCH 2/3] Proofread and update Proofread the article, update the text description and edit for style. --- .../whats-new/tutorials/extension-members.md | 20 +++++++++++-------- .../PointExtensions/NewExtensionsMembers.cs | 4 ++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/csharp/whats-new/tutorials/extension-members.md b/docs/csharp/whats-new/tutorials/extension-members.md index 990f894cf004f..79aa02b1cecbb 100644 --- a/docs/csharp/whats-new/tutorials/extension-members.md +++ b/docs/csharp/whats-new/tutorials/extension-members.md @@ -33,9 +33,9 @@ In this tutorial, you: ## 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 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 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 @@ -101,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"::: @@ -153,17 +153,21 @@ The key difference is syntax: extension members use `extension (Type variableNam 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. +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. + +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. -The sample defines `Path` in the `ExtensionMembers` namespace. That keeps the sample type separate from . The demo file uses a `using Path = ExtensionMembers.Path;` alias, so every `Path` in the demo means the sample path type. +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 . 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`: +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 the extension block names the receiver: `extension(Path path)`. A block written as `extension(Path)` wouldn't provide a `path` variable for the indexer body. +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. @@ -199,7 +203,7 @@ This example demonstrates how extension members create a cohesive API that feels ### 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. diff --git a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs index 11df6072a4573..4a4a2ef782774 100644 --- a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs +++ b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs @@ -60,9 +60,9 @@ public void Rotate(int angleInDegrees) // } + // extension(Path path) { - // public Point this[int index] { get @@ -92,8 +92,8 @@ public Point this[int index] path.SetOffset(index, (value.X - previousPoint.X, value.Y - previousPoint.Y)); } } - // } + // private static void ValidatePathIndex(Path path, int index) { From 4e60f72d39722f42d272fa21633547e30cabb263 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 15 Jul 2026 12:57:38 -0400 Subject: [PATCH 3/3] Show ValidatePathIndex helper in the indexer snippet Extend the PathIndexer snippet region to include the private ValidatePathIndex helper the accessors call, so readers see the complete code. Add a sentence noting the shared helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fb88c54-e7d1-47af-b8f2-d60b34e50426 --- docs/csharp/whats-new/tutorials/extension-members.md | 2 +- .../tutorials/snippets/PointExtensions/NewExtensionsMembers.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/csharp/whats-new/tutorials/extension-members.md b/docs/csharp/whats-new/tutorials/extension-members.md index 79aa02b1cecbb..f312cdfcbe00f 100644 --- a/docs/csharp/whats-new/tutorials/extension-members.md +++ b/docs/csharp/whats-new/tutorials/extension-members.md @@ -179,7 +179,7 @@ Now, use the indexer to read and write points along the path: :::code language="csharp" source="snippets/PointExtensions/ExtensionMemberDemonstrations.cs" id="PathIndexerUse"::: -Both accessors throw when the index doesn't refer to an offset in the path. +Both accessors share a small private `ValidatePathIndex` helper in the same `PointExtensions` class that bounds-checks the index and throws when it doesn't refer to an offset in the path. ## Completed sample diff --git a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs index 4a4a2ef782774..26c2964911269 100644 --- a/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs +++ b/docs/csharp/whats-new/tutorials/snippets/PointExtensions/NewExtensionsMembers.cs @@ -93,7 +93,6 @@ public Point this[int index] } } } - // private static void ValidatePathIndex(Path path, int index) { @@ -103,4 +102,5 @@ private static void ValidatePathIndex(Path path, int index) "Index must refer to an offset in the path."); } } + // }