proposals/extension-indexers.md
[!INCLUDESpecletdisclaimer]
Champion issue: https://github.com/dotnet/csharplang/issues/9856
Extension indexers are added to the set of permitted members inside an extension declaration by extending the grammar as follows (relative to proposals/csharp-14.0/extensions.md):
extension_member_declaration
: method_declaration
| property_declaration
| indexer_declaration // new
| operator_declaration
;
Like ordinary indexers, extension indexers have no identifier and are identified
by their parameter list. Extension indexers may use the full set of features that
ordinary indexers support today (accessor bodies, expression-bodied members,
ref-returning accessors, scoped parameters, attributes, etc.).
Because indexers are always instance members, an extension block that declares an indexer must provide a named receiver parameter.
The existing restrictions on extension members continue to apply: indexers inside an
extension declaration cannot specify abstract, virtual, override, new,
sealed, partial, protected (or any of the related accessibility modifiers),
or init accessors.
public static class BitExtensions
{
extension(int i)
{
public bool this[int index]
{
get => ...;
}
}
}
All rules from the C# standard that apply to ordinary indexers apply to extension indexers,
but extension members do not have an implicit or explicit this.
The existing extension member inferrability rule still applies: For each non-method extension member, all the type parameters of its extension block must be used in the combined set of parameters from the extension and the member.
IndexerName attributeIndexerNameAttribute may be applied to an extension indexer. The attribute is
not emitted in metadata, but its value affects conflicts between members,
it determines the name of the property and accessors in metadata,
and is used when emitting [DefaultMemberAttribute] (see Metadata).
The rules in Indexer access are updated: if the normal processing of the indexer access finds no applicable indexer, an attempt is made to process the construct as an extension indexer access.
System.Index/System.Range indexer access
(which relies on Length/Count plus this[int]/Slice(int, int)).Note: the element access section handles the case where an argument has type dynamic,
so it never gets processed as an indexer access.
Extension members, including extension indexers, are never considered when the receiver is a base_access expression.
Note: we only process an element_access as an indexer access if the receiver is a variable or value, so extension indexers are never considered when the receiver is a type.
Given an element_access E[A], the objective is to identify an extension indexer.
A candidate extension indexer is applicable with respect to receiver E and argument list A
if an expanded signature, comprised of the type parameters of the extension block and
a parameter list combining the extension parameter with the indexer's parameters, is applicable
with respect to an argument list combining the receiver E with the argument list A.
We reuse the extension method scope-walk: we traverse the same scopes consulted for
extension method invocation, including the current and enclosing lexical scopes
and using namespace or using static imports.
Considering each scope in turn:
System.Index/System.Range indexer access
(which relies on Length/Count plus this[int]/Slice(int, int)) using extension members in the current scope.
Length/Count part and the this[int]/Slice(int, int) part),
then we consider there was an applicable extension implicit indexer and this is the last scope we will consider.
Using this single best indexer identified at the previous step, the indexer access is then processed as a static method invocation.
Depending on the context in which it is used, an indexer access causes invocation of either
the get_accessor or the set_accessor of the indexer.
If the indexer access is the target of an assignment, the set_accessor static implementation method
is invoked to assign a new value.
In all other cases, the get_accessor static implementation method is invoked
to obtain the current value.
Either way, the invocation will use generic arguments inferred during the applicability check and
the receiver as the first argument.
Any construct that defers to element-access binding (null-conditional element access or assignments, index assignments in object initializers, or list and spread patterns) automatically participates in the extension indexer resolution described above.
Length or Count extension properties is considered countable for the purpose of
those patterns.System.Index/System.Range fallback indexers can also be extensions,
but the two parts (Length/Count and this[int]/Slice(int, int)) must be found in the same scope.Note: Since a list-pattern with a spread-pattern needs to bind a Length/Count, a real or implicit this[Index] and a real or implicit this[Range],
it may use the Length from one scope, the this[int] and corresponding Length from another scope
and the Slice(int, int) and Length from yet another scope.
The compiler assumes that the Length/Count properties are well-behaved and give the same result.
Extension indexers cannot be captured in expression trees.
CREF syntax allows referring to an extension indexer and its accessors, as well as its implementation methods.
Example:
/// <see cref="E.extension(int).this[string]"/>
/// <see cref="E.extension(int).get_Item(string)"/>
/// <see cref="E.extension(int).get_Item"/>
/// <see cref="E.extension(int).set_Item(string, int)"/>
/// <see cref="E.extension(int).set_Item"/>
/// <see cref="E.get_Item(int, string)"/>
/// <see cref="E.get_Item"/>
/// <see cref="E.set_Item(int, string, int)"/>
/// <see cref="E.set_Item"/>
public static class E
{
extension(int i)
{
/// <summary></summary>
public int this[string s]
{
get => throw null;
set => throw null;
}
}
}
Extension indexers follow the same lowering model as extension properties. For each CLR-level extension grouping type that contains at least one indexer, the compiler emits:
Item (or the value supplied by
IndexerNameAttribute) with accessor bodies that throw NotImplementedException()
and an [ExtensionMarkerName] attribute referencing the appropriate extension
marker type.get_Item/set_Item in the enclosing static
class. These methods prepend the receiver parameter to the parameter list and
contain the user-defined bodies. They are static and participate in overload
resolution in the same way as implementation methods for extension properties.To mirror the behavior of ordinary indexers, the compiler also emits
[DefaultMemberAttribute] on any extension grouping type that contains one or
more extension indexers. The attribute’s MemberName equals the metadata name of
the indexer (Item by default, or the value from IndexerNameAttribute).
Source code:
static class BitExtensions
{
extension<T>(T t)
{
public bool this[int index]
{
get => ...;
set => ...;
}
}
}
Emitted metadata (simplified to C#-like syntax):
[Extension]
static class BitExtensions
{
[Extension, SpecialName, DefaultMember("Item")]
public sealed class <G>$T0 // grouping type
{
[SpecialName]
public static class <M>$T_t // marker type
{
[SpecialName]
public static void <Extension>$(T t) { } // marker method
}
[ExtensionMarkerName("<M>$T_t")]
public bool this[int index] // extension indexer
{
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}
}
// accessor implementation methods
public static bool get_Item<T>(T t, int index) => ...;
public static void set_Item<T>(T t, int index, bool value) => ...;
}
paramsIf you have an extension indexer with params, such as int this[int i, params string[] s] { get; set; },
there are three ways you could use it:
receiver[i: 0, "Alice", "Bob"]E.get_Item(receiver, i: 0, "Alice", "Bob")E.set_Item(...)But what is the signature of the setter implementation method?
It only makes sense for the last parameter of a user-invocable method signature to have params,
so it serves no purpose in E.set_Item(... extension parameter ..., this i, params string[] s, int value).
Some options:
params for extension indexers that have a setter[ParamArray] attribute on the setter implementation method[ParamArray])I would propose option 2, as it maximizes params usefulness. The cost is only a small difference between
extension indexing and disambiguation syntax.
Decision (LDM 2026-02-02): emit the [ParamArray] and verify no negative impact on tooling
int i = 0;
i[42, null] = new object(); // fails inference
E.set_Item(i, 42, null, new object()); // infer `E.set_Item<object>`
public static class E
{
extension<T>(int i)
{
public T this[int j, T t] { set { } }
}
}
#nullable enable
int i = 0;
i[new object()] = null; // infer `E.extension<object!>` and warn on conversion of null literal to `object!`
E.set_Item(i, new object(), null); // infer `E.set_Item<object?>`
public static class E
{
extension<T>(int i)
{
public T this[T t] { set { } }
}
}
Decision (LDM 2026-02-02): the indexer is inferred only given the receiver and arguments in the argument list (ie. the assigned value doesn't contribute).
Length/Count properties make a type countable?As a reminder, extensions do not come into play when binding implicit Index or Range indexers:
C c = new C();
_ = c[..]; // Cannot apply indexing with [] to an expression of type 'C'
class C
{
public int Length => 0;
}
static class E
{
public static C Slice(this C c, int i, int j) => null!;
}
So our position so far has been that extensions properties should not count as "countable properties" in list-patterns, collection expressions and implicit indexers.
If we expose this[Index] or this[Range] extension indexers in element access scenarios,
it is natural to expect the target type to work in list patterns.
List patterns, however, require a Length or Count property.
Should extension properties satisfy that requirement? (that would seem natural)
C c = new C();
var x1 = c[^1];
var x2 = c[1..];
if (c is [.., var y1]) { }
if (c is [_, .. var y2]) { }
class C { }
static class E
{
extension(C c)
{
object this[System.Index] => ...;
C this[System.Range] => ...;
int Length => ...;
}
}
But then, should those properties also contribute to the implicit indexer fallback
(Length/Count + Slice) that is used when an explicit Index/Range indexer
is missing?
C c = new C();
if (c is [var y1, .. var y2]) { }
class C
{
C Slice(int i, int j) => ...;
}
static class E
{
extension(C c)
{
object this[System.Index] => ...;
int Length => ...;
}
}
Decision (LDM 2026-02-02): extensions should contribute everywhere, including countable properties and implicit indexer fallback.
C c = new C();
_ = c[^1];
class C
{
public int Length => ...;
public int this[int i] => ...;
}
static class E
{
extension(C c)
{
public int this[System.Index i] => ...;
}
}
I've spec'ed and implemented extension indexer access as having priority over implicit indexers, but now think they should come after to avoid unnecessary compat breaks.
Update (LDM 2026-02-02): this needs further investigation. Yes, extensions should come after non-extension members, but beyond that we need some concrete proposals in light of above decision to allow extensions to contribute to implicit indexer fallback.
We also have an existing fallback: Length is prioritized over Count property.
Should an extension Length come before or after a non-extension Count property?
Answer: the proposal is to look up scope by scope. Instance scope comes before extension scopes.
Within each scope, we prefer Length over Count.
We look scope-by-scope, starting from instance scope and then proceeding to extension scopes.
Within a scope:
Decision (LDM 2026-03-09): no, once we move into extension scopes, we're going to use all-the-way-through extension resolution.
For a list-pattern with a spread, we will look for:
Length/Countthis[Index]this[Range]We will look for each independently.
But when we look for an implicit this[Index] or this[Range], the two parts must come from the same scope:
Length/Countthis[int]/Slice(int, int)Note: the non-negative handling for Length patterns kicks in when a type can be used in a list-pattern (ie. it is countable and indexable).
Decision (LDM 2026-03-09): no, we'll follow this lookup order instead:
Slice method also contribute?_ = c[1..^1];
static class E
{
extension(C c)
{
public int Length => 3;
}
public static C Slice(this C c, int i, int j) => ...;
}
Decision (LDM 2026-03-09): yes, we're treating classic and new extension methods exactly the same.
Length contribute to spread optimization?C c = new C();
int[] i = [0, .. c]; // Uses Length, if available, to allocate the right size
Decision (LDM 2026-03-09): no, it's unlikely that an extension would be able to implement this in a performant way, so it would not help for optimization.