C# collection expressions made collection creation much cleaner by allowing developers to use a consistent syntax such as:
int[] numbers = [1, 2, 3, 4];However, there was a limitation.
Sometimes the collection itself needs additional configuration when it is created.
For example, a List<T> may benefit from an initial capacity, while a HashSet<T> may need a specific comparer.
Previously, developers generally had to fall back to an explicit constructor:
var names = new List<string>(100);C# 15 introduces collection expression arguments, which allow constructor or factory arguments to be supplied directly inside a collection expression using with(...). The with(...) element must appear first in the collection expression.
This gives developers a way to keep the concise collection-expression syntax while still controlling how the collection is constructed.
What Are Collection Expression Arguments?
A normal collection expression looks like this:
List<string> names = ["John", "Maria", "David"];With C# 15, you can provide arguments used to construct the target collection:
List<string> names =
[
with(capacity: 100),
"John",
"Maria",
"David"
];The with(...) element supplies arguments to the constructor or collection factory used for the target type.
The important rule is that with(...) must be the first element in the collection expression.
List<string> names =
[
with(capacity: 100),
"John",
"Maria"
];This is valid.
Putting it after collection elements is not:
List<string> names =
[
"John",
with(capacity: 100),
"Maria"
];Why Was This Feature Added?
Collection expressions are intentionally concise:
var numbers = [1, 2, 3, 4];But collection types can have important construction parameters.
Consider List<T>.
It has constructors that can specify capacity:
var names = new List<string>(1000);The capacity can be useful when the application already knows approximately how many items will be added.
Without collection expression arguments, developers have to choose between concise collection syntax and explicit constructor syntax.
C# 15 combines the two ideas:
List<string> names =
[
with(capacity: 1000),
"John",
"Maria",
"David"
];The collection remains visually recognizable as a collection expression while the construction behavior is explicit.
A Simple List Example
Consider an application that imports customer records.
var customers =
new List<Customer>(1000);The initial capacity is intentionally specified because the application expects many records.
Using a collection expression with C# 15:
List<Customer> customers =
[
with(capacity: 1000),
new Customer(1, "John"),
new Customer(2, "Maria"),
new Customer(3, "David")
];The important distinction is that capacity is not an element in the list.
It is an argument used when constructing the collection.
Conceptually:
Collection Expression
|
+-- Construction arguments
| |
| +-- capacity: 1000
|
+-- Elements
|
+-- Customer 1
+-- Customer 2
+-- Customer 3How the Constructor Is Selected
The compiler uses overload resolution to select an appropriate constructor based on the arguments supplied through with(...). For a target class or struct that implements IEnumerable, the arguments can be passed to an applicable constructor.
For example, List<T> provides constructors including:
List<T>()
List<T>(int capacity)
List<T>(IEnumerable<T> collection)Therefore:
List<int> numbers =
[
with(capacity: 10),
1,
2,
3
];can use the capacity constructor.
The important part is that the compiler does not interpret:
with(capacity: 10)as an item being inserted into the collection.
It is part of collection construction.
HashSet and Comparers
One of the more practical examples is HashSet<T>.
A HashSet<string> can use a custom equality comparer.
Traditionally:
var names = new HashSet<string>(
StringComparer.OrdinalIgnoreCase);
names.Add("John");
names.Add("JOHN");
names.Add("john");The comparer determines that these strings are equal.
With C# 15 collection expression arguments:
HashSet<string> names =
[
with(StringComparer.OrdinalIgnoreCase),
"John",
"JOHN",
"john"
];The resulting set contains one logical value because the comparer treats the strings as equal. Microsoft uses this type of example in the C# 15 documentation.
This is a particularly useful scenario because the comparer is part of the collection's behavior.
Why the Comparer Matters
Consider this code:
var users =
new HashSet<string>(
StringComparer.OrdinalIgnoreCase);Now:
users.Add("Admin");
users.Add("admin");Only one value is retained.
Without the comparer:
var users =
new HashSet<string>();
users.Add("Admin");
users.Add("admin");both values can exist because the default string comparison is different.
With collection expression arguments:
HashSet<string> users =
[
with(StringComparer.OrdinalIgnoreCase),
"Admin",
"admin"
];the comparison behavior is visible directly at the point where the collection is created.
Using Spread Elements
Collection expressions can also use the spread operator .. to include values from another collection.
For example:
string[] values =
[
"one",
"two",
"three"
];
List<string> names =
[
with(capacity: values.Length * 2),
..values
];Here:
with(capacity: values.Length * 2)controls construction.
And:
..valuesadds the elements from the existing collection.
This combination is one of the practical reasons collection expression arguments are useful.
Microsoft's documentation provides a similar example using a calculated capacity and a spread element.
A Practical ASP.NET Core Example
Consider an ASP.NET Core application that builds a list of response objects.
Suppose the application receives a batch of customer records:
public record Customer(
int Id,
string Name);The application needs to transform them into API results.
A conventional approach might be:
var results =
new List<CustomerResponse>(
customers.Count);
foreach (var customer in customers)
{
results.Add(
new CustomerResponse(
customer.Id,
customer.Name));
}For a known collection, collection expressions can make the creation syntax more compact.
For example:
List<CustomerResponse> results =
[
with(capacity: customers.Count),
.. customers.Select(customer =>
new CustomerResponse(
customer.Id,
customer.Name))
];The capacity is based on the expected number of items.
The spread element then adds the generated responses.
Why Capacity Can Matter
When a List<T> grows beyond its current capacity, it may need to allocate a larger internal array and copy existing elements.
If the application already knows approximately how many elements it will add, setting an appropriate initial capacity can avoid unnecessary growth operations.
For example:
var results =
new List<CustomerResponse>(
customers.Count);or:
List<CustomerResponse> results =
[
with(capacity: customers.Count),
.. customers.Select(CreateResponse)
];This does not mean developers should specify capacity everywhere.
If the collection is small or its size is unpredictable, the added complexity may not provide meaningful value.
The important point is that C# 15 gives collection expressions access to this constructor configuration when it is useful.
Passing an Existing Collection to a Constructor
Collection expression arguments are not limited to simple scalar values.
The feature specification allows constructor arguments to participate in overload resolution, including arguments representing existing collections where the target constructor accepts them.
For example, the target type might have a constructor accepting another collection:
public sealed class CustomerCollection :
IEnumerable<Customer>
{
public CustomerCollection(
IEnumerable<Customer> customers)
{
// Store or process customers.
}
public IEnumerator<Customer> GetEnumerator()
{
throw new NotImplementedException();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}A collection expression can provide the constructor argument through with(...) where the target type and applicable collection-expression conversion support it.
The exact constructor chosen is determined by overload resolution.
Collection Builders
Collection expressions can also target types that use a collection builder.
A collection builder is a mechanism that allows a custom collection type to define how a collection expression creates its instance.
Consider:
[CollectionBuilder(
typeof(CustomerCollectionBuilder),
nameof(CustomerCollectionBuilder.Create))]
public readonly struct CustomerCollection
{
}The builder can expose a factory method for creating the collection.
Collection expression arguments can be passed to supported factory methods before the element span argument. The feature specification defines how those arguments participate in selecting and invoking collection builders.
This means the feature is not limited to built-in collection types.
It can also support carefully designed custom collection abstractions.
A Custom Collection Example
Consider a custom collection that needs a configuration value:
public sealed class BatchOptions
{
public int MaximumItems { get; }
public BatchOptions(int maximumItems)
{
MaximumItems = maximumItems;
}
}A custom collection could expose a construction mechanism that accepts options along with the elements.
Conceptually, the collection expression can then provide:
var batch =
[
with(new BatchOptions(100)),
item1,
item2,
item3
];The exact support depends on the collection's construction or builder pattern.
This is where collection expression arguments become more than a convenience feature for List<T> and HashSet<T>.
Collection Expression Arguments vs Traditional Constructors
Consider the two approaches.
Traditional Constructor
var names =
new List<string>(100);
names.Add("John");
names.Add("Maria");
names.Add("David");Collection Expression
List<string> names =
[
with(capacity: 100),
"John",
"Maria",
"David"
];Area | Constructor + Add | Collection Expression Arguments |
|---|---|---|
Construction options | Explicit | Inside |
Initial values | Added separately | Listed directly |
Readability | More verbose | More compact |
Collection shape | Clear | Clear |
Capacity configuration | Supported | Supported |
Works with builders | Depends on API | Supported where builder is compatible |
The second approach is especially attractive when the collection's contents are already known at the point of creation.
Collection Expression Arguments vs Collection Initializers
Collection initializers and collection expressions are related but different features.
A collection initializer might look like:
var names =
new List<string>
{
"John",
"Maria",
"David"
};A collection expression looks like:
List<string> names =
[
"John",
"Maria",
"David"
];With C# 15:
List<string> names =
[
with(capacity: 100),
"John",
"Maria",
"David"
];The third version combines collection expression syntax with construction arguments.
This gives developers a consistent way to express both the collection's contents and important construction configuration.
Using Collection Expressions With Dictionaries
Dictionary scenarios are particularly interesting because key comparison behavior can be important.
Suppose an application uses case-insensitive configuration keys.
A traditional dictionary might be created like this:
var settings =
new Dictionary<string, string>(
StringComparer.OrdinalIgnoreCase)
{
["Environment"] = "Production",
["Region"] = "India"
};Collection expressions already provide dictionary initialization syntax in modern C#.
With collection expression arguments, the comparer can be supplied as part of the collection expression:
Dictionary<string, string> settings =
[
with(StringComparer.OrdinalIgnoreCase),
["Environment"] = "Production",
["Region"] = "India"
];The important idea is that the comparer belongs to the collection's construction semantics.
It is therefore useful to keep it next to the collection declaration.
Case Sensitivity in Real Applications
Consider configuration keys:
ConnectionString
connectionstring
CONNECTIONSTRINGIf an application intends these keys to be treated as equivalent, the comparer needs to be configured consistently.
For example:
Dictionary<string, string> settings =
[
with(StringComparer.OrdinalIgnoreCase),
["ConnectionString"] = "...",
["Region"] = "India"
];Now lookup behavior is determined by the configured comparer.
var connectionString =
settings["connectionstring"];The collection's behavior is immediately visible from its declaration.
with(...) Must Come First
One of the most important syntax rules is that the with(...) element must be the first element in the collection expression.
Correct:
List<int> numbers =
[
with(capacity: 20),
10,
20,
30
];Incorrect:
List<int> numbers =
[
10,
with(capacity: 20),
20
];This rule keeps construction configuration separate from the elements being added.
It also makes it easy for someone reading a long collection expression to identify how the collection is constructed.
Argument Evaluation
Collection expression elements and collection arguments follow defined evaluation rules.
The arguments in with(...) are evaluated in order, and collection elements are also evaluated in order. The feature specification defines these evaluation rules so that the construction process remains predictable.
For example:
List<int> values =
[
with(capacity: CalculateCapacity()),
GetValue(1),
GetValue(2)
];The methods involved should be treated as normal expressions with their own side effects and evaluation order.
As a general best practice, keep constructor arguments simple.
Prefer:
List<int> values =
[
with(capacity: expectedCount),
10,
20,
30
];over embedding complex logic directly inside the collection declaration.
Production Example: Batch Processing
Suppose an application processes orders in batches.
public record Order(
int Id,
decimal Amount);The application receives a source collection:
IEnumerable<Order> orders = GetOrders();If the number of orders is known:
var orderArray = orders.ToArray();
List<Order> batch =
[
with(capacity: orderArray.Length),
.. orderArray
];This clearly communicates that the list is expected to contain the source collection's elements and that its capacity is intentionally configured.
The same approach can be useful in data transformation code where collection sizes are known before materialization.
Production Example: Case-Insensitive Tags
Suppose an application collects tags from multiple sources.
string[] incomingTags =
[
"CSharp",
"csharp",
"ASP.NET",
"asp.net"
];Create a case-insensitive set:
HashSet<string> tags =
[
with(StringComparer.OrdinalIgnoreCase),
.. incomingTags
];Now:
tags.Contains("CSharp");
tags.Contains("csharp");both use the same equality semantics.
This is a useful production pattern because the comparer is not hidden elsewhere in the application.
Common Mistakes
Treating with(...) as a Collection Element
This:
[
with(capacity: 100),
1,
2
]does not add a with object to the collection.
It supplies construction arguments.
Placing with(...) After Elements
The with(...) element must appear first.
Using Capacity Without a Reason
Do not specify arbitrary capacities everywhere.
If there is no meaningful expected size, the default collection behavior may be sufficient.
Assuming Every Collection Supports Every Argument
The supplied arguments must match an appropriate constructor or collection builder.
Ignoring Comparer Semantics
A custom comparer can fundamentally change how dictionaries and sets behave.
Choose it intentionally.
Overcomplicating Collection Expressions
This:
var values =
[
with(capacity: CalculateComplexCapacity()),
GetFirstValue(),
Transform(
GetSecondValue(),
GetThirdValue())
];may be technically valid but difficult to maintain.
Collection expressions should improve readability, not become miniature programs.
Troubleshooting
The Compiler Says with(...) Is Not Supported
Collection expression arguments are a C# 15 feature. Microsoft currently documents C# 15 as a preview release supported by the .NET 11 preview SDK and Visual Studio 2026 Insiders.
Make sure the project is using an appropriate SDK and language version.
For a preview environment:
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<LangVersion>preview</LangVersion>
</PropertyGroup>The Constructor Cannot Be Selected
Check the argument types.
For example:
List<int> values =
[
with("100"),
1,
2
];does not provide an appropriate List<int> constructor argument.
Use:
List<int> values =
[
with(capacity: 100),
1,
2
];with(...) Is Not the First Element
Move it to the beginning:
List<int> values =
[
with(capacity: 100),
1,
2
];A Custom Collection Does Not Work
Check whether the target type has an applicable constructor or collection builder that supports the supplied arguments.
Collection builder methods have specific requirements, including appropriate accessibility, generic arity, and a final ReadOnlySpan<T> parameter for the element data.
A Note About the with Name
There is an important language-version consideration.
In C# 15, with(...) at the beginning of a collection expression is interpreted as collection construction arguments rather than a normal method invocation. Microsoft documents this as a compiler behavior change for C# 15.
For example:
items = [with(x, y), z];may have different interpretation depending on the language version and target type.
If code previously relied on a method literally named with, the C# 15 syntax can affect how that expression is parsed or bound.
The explicit escaped form can be used when the intent is to invoke a method named with:
items = [@with(x, y), z];This is mainly relevant to migration scenarios and unusual existing code.
Best Practices
Keep Construction Arguments Meaningful
Use with(...) when the constructor argument has a real purpose.
Good:
HashSet<string> values =
[
with(StringComparer.OrdinalIgnoreCase),
.. input
];Use Capacity When the Size Is Predictable
Good:
List<Order> orders =
[
with(capacity: expectedCount),
.. source
];Avoid arbitrary numbers:
List<Order> orders =
[
with(capacity: 100000),
.. source
];unless there is a reason for that value.
Keep Comparers Explicit
For dictionaries and sets, a comparer can affect application behavior. Keeping it in the collection declaration can improve maintainability.
Keep with(...) First
This is required by the language and makes the construction configuration easy to find.
Prefer Simple Arguments
Do not move complicated business logic into a collection declaration.
Test Custom Collections
If your application uses custom collection builders, test both normal collection creation and construction arguments.
Advantages
Concise Syntax
Collection construction and initial values can be expressed together.
Constructor Configuration
Capacity, comparers, and supported constructor arguments can be specified directly.
Better Readability
Important collection behavior can remain next to the collection declaration.
Works With Spread Elements
You can combine construction arguments with .. to populate a collection from existing values.
Supports Custom Collection Patterns
Collection builders can participate in collection expression construction where the required pattern is implemented.
Disadvantages
C# 15 Dependency
The feature requires C# 15 support and is currently part of the preview language experience.
New Syntax to Learn
Developers need to understand what with(...) means when it appears as the first element of a collection expression.
Constructor Behavior Is Less Visible
With traditional syntax:
new List<string>(100)the constructor call is explicit.
With:
[
with(capacity: 100),
"A",
"B"
]developers need to understand collection expression semantics.
Not Every Collection Supports Arbitrary Arguments
The target type still needs an appropriate constructor or collection-builder implementation.
Migration Requires Attention
Existing code involving a method named with may need review when moving to C# 15 semantics.
When Should You Use Collection Expression Arguments?
The feature is most useful when three conditions are true:
You want collection-expression syntax.
The target collection needs construction configuration.
That configuration has clear value to the application.
For example:
HashSet<string> permissions =
[
with(StringComparer.OrdinalIgnoreCase),
"Read",
"Write",
"Delete"
];This is a strong use case because the comparer directly affects the collection's behavior.
Another example:
List<Customer> customers =
[
with(capacity: expectedCustomerCount),
.. sourceCustomers
];This can be useful when the expected collection size is already known.
When Should You Avoid It?
Avoid collection expression arguments when the construction configuration adds no meaningful value.
Instead of:
List<int> values =
[
with(capacity: 3),
1,
2,
3
];you may simply write:
List<int> values =
[
1,
2,
3
];The default capacity behavior is perfectly reasonable for many small collections.
Also avoid using the feature simply because it is new.
The purpose of the feature is to make collection construction more expressive, not to replace every existing collection declaration.
Practical Comparison
Scenario | Recommended Approach |
|---|---|
Small collection with no configuration | Normal collection expression |
Known large collection size | Collection expression with |
Case-insensitive | Collection expression with comparer |
Case-insensitive dictionary | Collection expression with comparer |
Complex construction logic | Explicit constructor or factory |
Database query | Explicit method or query |
Remote API call | Explicit asynchronous method |
Custom collection | Collection expression if builder supports it |
Unclear construction semantics | Prefer explicit construction |
Summary
C# 15 collection expression arguments extend the existing collection-expression syntax with a way to control how the target collection is constructed.
The key syntax is:
[
with(...),
...
]The with(...) element must be first, and its arguments are passed to an appropriate constructor or collection factory. This allows developers to specify settings such as list capacity or set and dictionary comparers while keeping the concise collection-expression syntax.
For example:
List<string> names =
[
with(capacity: 100),
"John",
"Maria",
"David"
];and:
HashSet<string> names =
[
with(StringComparer.OrdinalIgnoreCase),
"John",
"JOHN"
];The feature is particularly useful when construction configuration is part of the collection's behavior. It should not be used simply to make every collection declaration look newer.
For production applications, the best approach is to use collection expression arguments where they make intent clearer: capacity when collection size is predictable, comparers when equality behavior matters, and constructor or builder arguments when they represent meaningful collection configuration.

Join the conversation! Your thoughts help the community grow.