C# 15 ships with .NET 11.
You can now pass constructor arguments inside a collection expression, give an existing type another indexer, and target an outer loop with break or continue.
If your project targets net11.0 with the .NET 11 SDK, you can use all three without setting LangVersion to preview.
Can a Collection Expression Set the Sort Order?
A support-ticket dispatcher needs each priority once, with the most urgent tickets first.
A SortedSet<int> removes duplicates, but it puts priority 2 ahead of priority 5 by default.
Pass it a descending comparer when you create the set.
Save this as Priorities.cs and run dotnet run --file Priorities.cs with the .NET 11 SDK.
int[] incoming = [2, 5, 2, 4];
var highestFirst = Comparer<int>.Create((left, right) => right.CompareTo(left));
SortedSet<int> priorities = [with(highestFirst), .. incoming];
Console.WriteLine(string.Join(", ", priorities));
The output is 5, 4, 2.
The set removes the second 2, and its comparer puts 5 first.
If you remove with(highestFirst), the set uses the default comparer and prints 2, 4, 5.
The square brackets and the .. spread were already available in C# 12.
C# 15 lets you pass collection expression arguments to the target collection's constructor or factory.
Put them inside with(...) before the first element or spread.
Here the type on the left tells the compiler which constructor to use.
Use a HashSet<T> when you only need to detect duplicate entries.
The dispatcher also needs to read the priorities in order.
Give the Rota a Two-Argument Indexer
The on-call rota maps a weekday and priority to a team.
The library that supplies it exposes an IReadOnlyDictionary<(DayOfWeek, int), string>.
A caller can already look up a tuple key with rota[(day, priority)].
With a C# 15 extension indexer, callers can write rota[day, priority] and leave the tuple construction to the accessor.
Run this as dotnet run --file Rota.cs.
IReadOnlyDictionary<(DayOfWeek, int), string> rota =
new Dictionary<(DayOfWeek, int), string>
{
[(DayOfWeek.Monday, 5)] = "Primary",
[(DayOfWeek.Monday, 2)] = "Triage"
};
Console.WriteLine(rota[DayOfWeek.Monday, 5]);
public static class RotaExtensions
{
extension(IReadOnlyDictionary<(DayOfWeek, int), string> rota)
{
public string this[DayOfWeek day, int priority] => rota[(day, priority)];
}
}
The output is Primary.
The two-argument accessor builds a tuple and calls the dictionary's existing one-argument indexer.
A missing key still throws KeyNotFoundException.
If your caller expects an empty slot, use TryGetValue so that case stays visible in the calling code.
An extension indexer needs a named receiver (rota here), because each lookup runs against a particular instance.
If you place RotaExtensions in a namespace, callers need that namespace in scope for the new syntax.
Stop at the First Responder, or Pick One per Team
An escalation policy checks teams in order. Within each team, it checks candidates until it finds someone on call. Once it finds the first responder, it can stop both loops.
Save this as Escalate.cs and run dotnet run --file Escalate.cs.
(string Team, string[] Responders)[] teams =
[
("Orders", ["Ana", "Tom"]),
("Payments", ["Jo", "Sam"]),
("Search", ["Mia"])
];
HashSet<string> onCall = ["Tom", "Jo", "Mia"];
escalation: foreach (var (team, responders) in teams)
{
foreach (string name in responders)
{
if (!onCall.Contains(name)) continue;
Console.WriteLine($"{team}: {name}");
break escalation;
}
}
The only line printed is Orders: Tom.
The ordinary continue skips Ana and tries the next person on the same team.
Because escalation labels the outer foreach, break escalation skips the other teams once it finds Tom.
Now suppose an incident needs one responder per team.
Replace break escalation; with continue escalation; and run it again.
Orders: Tom
Payments: Jo
Search: Mia
The labeled continue skips the remaining candidates on the current team and starts the next team.
Place the label directly on its target loop.
A labeled break can also leave an enclosing switch.
A labeled continue always targets a loop.
Summary
When you review a labeled jump, read the label and its target together.
In the escalation loop, changing break escalation to continue escalation pages a responder from every team.
That difference is easy to miss if you're used to break and continue affecting only the innermost loop.
Frequently Asked Questions
Do these C# 15 features require preview settings?
No. Target net11.0 with the .NET 11 SDK and its default language version for collection expression arguments, extension indexers, and labeled jumps.
Where does with(...) go in a C# collection expression?
Place it before the first element or spread. Its arguments configure the constructor or factory used for the target collection, such as a comparer for a SortedSet.
Can an extension indexer use more than one key?
Yes. Declare the indexer in an extension block with a named receiver and give the indexer multiple parameters. Its accessor can translate them into the key expected by the underlying collection.
What is the difference between break label and continue label?
A labeled break exits the enclosing loop or switch identified by the label. A labeled continue starts the next iteration of the enclosing loop identified by the label.

